@morphemecms/cms 0.11.0 → 0.13.0-next-20261106203613

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 (186) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/astro.mjs +2 -1
  3. package/defaults/blockTexts.ts +9 -9
  4. package/defaults/collections.ts +257 -257
  5. package/defaults/colors.ts +74 -74
  6. package/defaults/company.ts +9 -9
  7. package/defaults/editor.ts +65 -65
  8. package/defaults/i18n/da-DK/blockTexts.ts +12 -0
  9. package/defaults/i18n/da-DK/collections.ts +220 -0
  10. package/defaults/i18n/da-DK/colors.ts +102 -0
  11. package/defaults/i18n/da-DK/company.ts +7 -0
  12. package/defaults/i18n/da-DK/editor.ts +13 -0
  13. package/defaults/i18n/da-DK/index.ts +31 -0
  14. package/defaults/i18n/da-DK/internalLinks.ts +12 -0
  15. package/defaults/i18n/da-DK/opens.ts +7 -0
  16. package/defaults/i18n/da-DK/popup.ts +9 -0
  17. package/defaults/i18n/da-DK/privacy.ts +38 -0
  18. package/defaults/i18n/da-DK/rules.ts +56 -0
  19. package/defaults/i18n/da-DK/sitedata.ts +8 -0
  20. package/defaults/i18n/da-DK/standard.ts +8 -0
  21. package/defaults/i18n/da-DK/standardPages.ts +17 -0
  22. package/defaults/internalLinks.ts +8 -8
  23. package/defaults/opens.ts +2 -2
  24. package/defaults/popup.tsx +7 -7
  25. package/defaults/privacy.ts +37 -37
  26. package/defaults/rules.ts +68 -66
  27. package/defaults/schema.ts +1 -1
  28. package/defaults/sitedata.ts +4 -4
  29. package/defaults/standard.ts +9 -6
  30. package/defaults/standardPages.ts +14 -14
  31. package/defaults/texts.ts +7 -510
  32. package/lib/appId.d.mts +1 -0
  33. package/lib/appId.mjs +6 -0
  34. package/lib/appRoute.d.mts +2 -2
  35. package/lib/appRoute.mjs +3 -3
  36. package/lib/appRoute.test.mjs +8 -8
  37. package/lib/apps.mjs +13 -3
  38. package/lib/apps.test.mjs +10 -1
  39. package/lib/contracts.d.mts +3 -2
  40. package/lib/contracts.mjs +3 -2
  41. package/lib/folderChanges.d.mts +29 -0
  42. package/lib/folderChanges.mjs +34 -0
  43. package/lib/licenses.mjs +2 -2
  44. package/lib/licenses.owner.test.mjs +9 -0
  45. package/lib/menu.mjs +4 -4
  46. package/lib/menu.test.mjs +101 -101
  47. package/lib/scheduled.mjs +1 -1
  48. package/lib/section-ranges.d.mts +1 -1
  49. package/lib/section-ranges.mjs +8 -14
  50. package/lib/section-ranges.test.mjs +26 -5
  51. package/lib/storeEntries.d.mts +3 -1
  52. package/lib/storeEntries.mjs +22 -8
  53. package/lib/storeEntries.test.mjs +59 -7
  54. package/lib/text-sections.d.mts +11 -1
  55. package/lib/text-sections.mjs +18 -8
  56. package/lib/text-sections.test.mjs +34 -1
  57. package/lib/translate.d.mts +11 -0
  58. package/lib/translate.mjs +13 -1
  59. package/lib/translate.test.mjs +23 -2
  60. package/lib/video-id.d.mts +6 -2
  61. package/lib/video-id.mjs +5 -1
  62. package/lib/video.d.mts +6 -2
  63. package/lib/video.mjs +5 -1
  64. package/lib/without-h1.d.mts +1 -0
  65. package/lib/without-h1.mjs +19 -4
  66. package/lib/without-h1.test.mjs +30 -1
  67. package/package.json +3 -2
  68. package/scripts/app-screenshots.mjs +9 -8
  69. package/scripts/check-content.mjs +12 -2
  70. package/scripts/check-language.mjs +0 -1
  71. package/scripts/check-site-types.mjs +26 -0
  72. package/scripts/editor-screens.mjs +3 -3
  73. package/scripts/site-types.mjs +14 -0
  74. package/scripts/site-types.test.mjs +21 -0
  75. package/site/activeTheme.test.mjs +52 -0
  76. package/site/appSelf.d.mts +2 -0
  77. package/site/appSelf.mjs +23 -0
  78. package/site/appSelf.test.mjs +35 -0
  79. package/site/content.ts +5 -2
  80. package/site/defineSite.mjs +2 -2
  81. package/site/schema.ts +3 -3
  82. package/site/siteModules.mjs +2 -1
  83. package/site/theme.mjs +19 -8
  84. package/src/Advanced.tsx +2 -2
  85. package/src/App.tsx +79 -78
  86. package/src/AppAdds.tsx +1 -1
  87. package/src/AppList.tsx +39 -24
  88. package/src/BlockDialog.tsx +39 -18
  89. package/src/BlockParts.tsx +6 -6
  90. package/src/BlockSettings.tsx +2 -2
  91. package/src/BrandForm.tsx +2 -2
  92. package/src/Colors.tsx +6 -6
  93. package/src/CompanyForm.tsx +1 -1
  94. package/src/Consents.tsx +3 -3
  95. package/src/CreateFab.tsx +8 -0
  96. package/src/DataForm.tsx +10 -20
  97. package/src/Deleted.tsx +3 -3
  98. package/src/EditorFooter.tsx +2 -2
  99. package/src/EntryForm.tsx +3 -3
  100. package/src/FooterSettings.tsx +2 -2
  101. package/src/FormFrame.tsx +4 -3
  102. package/src/HeaderSettings.tsx +3 -3
  103. package/src/Keywords.tsx +5 -5
  104. package/src/Library.tsx +75 -82
  105. package/src/Licenses.tsx +8 -8
  106. package/src/ListFab.tsx +13 -0
  107. package/src/MainDrawerParts.tsx +3 -3
  108. package/src/MainMenu.tsx +1 -1
  109. package/src/MarkdownEditor.tsx +88 -47
  110. package/src/Media.tsx +11 -20
  111. package/src/MediaTags.tsx +2 -2
  112. package/src/Notifications.tsx +2 -2
  113. package/src/Parts.tsx +17 -17
  114. package/src/Redirects.tsx +14 -22
  115. package/src/Screen.tsx +16 -6
  116. package/src/SeoScores.tsx +6 -6
  117. package/src/SideColumn.tsx +2 -2
  118. package/src/StartFab.tsx +6 -8
  119. package/src/ThemeStore.tsx +37 -16
  120. package/src/Tokens.tsx +2 -2
  121. package/src/apps.ts +7 -8
  122. package/src/checkSite.ts +86 -20
  123. package/src/drawerYield.d.mts +3 -0
  124. package/src/drawerYield.mjs +10 -0
  125. package/src/drawerYield.test.mjs +22 -0
  126. package/src/editor.css +59 -38
  127. package/src/examples.ts +1 -1
  128. package/src/fabScreens.test.mjs +28 -0
  129. package/src/fabScreens.ts +16 -0
  130. package/src/i18n/da-DK/library.ts +2 -2
  131. package/src/i18n/da-DK/text.ts +1 -1
  132. package/src/i18n/en-US/apps.ts +1 -1
  133. package/src/i18n/en-US/common.ts +8 -8
  134. package/src/i18n/en-US/library.ts +5 -5
  135. package/src/i18n/en-US/parts.ts +3 -3
  136. package/src/i18n/en-US/text.ts +1 -1
  137. package/src/i18n/index.ts +4 -8
  138. package/src/images.ts +2 -2
  139. package/src/legacyMove.test.mjs +137 -0
  140. package/src/legacyMove.ts +131 -0
  141. package/src/licenseData.ts +3 -3
  142. package/src/markdown.mjs +10 -7
  143. package/src/markdown.test.mjs +16 -2
  144. package/src/menuChildren.ts +5 -5
  145. package/src/menuOpen.ts +20 -4
  146. package/src/movedRoutes.d.mts +8 -1
  147. package/src/movedRoutes.mjs +102 -21
  148. package/src/movedRoutes.test.mjs +128 -6
  149. package/src/nav.ts +1 -1
  150. package/src/notificationData.ts +1 -1
  151. package/src/pageH1Panel.d.mts +4 -0
  152. package/src/pageH1Panel.mjs +31 -0
  153. package/src/pageH1Panel.test.mjs +56 -0
  154. package/src/pagePreview.tsx +22 -12
  155. package/src/schema.ts +2 -2
  156. package/src/sectionFrame.ts +3 -3
  157. package/src/sectionState.d.mts +3 -3
  158. package/src/sectionState.mjs +6 -6
  159. package/src/sectionState.test.mjs +14 -2
  160. package/src/sheetPlacement.d.mts +6 -4
  161. package/src/sheetPlacement.mjs +15 -10
  162. package/src/sheetPlacement.test.mjs +16 -9
  163. package/src/sidePanel.ts +38 -0
  164. package/src/site.ts +37 -13
  165. package/src/standard/accordion/index.tsx +1 -1
  166. package/src/standard/accordion/shared.ts +1 -1
  167. package/src/standard/buttons/index.tsx +1 -1
  168. package/src/standard/cards/index.tsx +1 -1
  169. package/src/standard/cards/shared.ts +1 -1
  170. package/src/standard/cookies/index.tsx +1 -1
  171. package/src/standard/posts/index.tsx +1 -1
  172. package/src/standard/testimonials/index.tsx +1 -1
  173. package/src/standard/testimonials/list.ts +1 -1
  174. package/src/store.ts +36 -31
  175. package/src/tagUses.d.mts +1 -1
  176. package/src/tagUses.mjs +2 -2
  177. package/src/tagUses.test.mjs +1 -1
  178. package/src/topBarSlot.tsx +7 -15
  179. package/src/ui.tsx +33 -22
  180. package/src/virtual.d.ts +6 -0
  181. package/starter/package.json +1 -1
  182. package/theme/components/PageHeading.astro +22 -0
  183. package/theme/layouts/Base.astro +2 -2
  184. package/theme/layouts/PageLayout.astro +37 -20
  185. package/theme/styles/global.css +2 -1
  186. package/theme/styles/ui.css +40 -10
@@ -1,7 +1,7 @@
1
- // "Privatliv": what the site asks and tells its visitors about their data, as screens of their own (CompanyForm.tsx),
2
- // at #privatliv/<id>, and the cookie list's row: the cookie banner (src/data/cookies.json, read by packages/editor/theme/components/CookieConsent.astro),
3
- // "Privatlivssider", which pages are the privacy policy and the cookie policy, as a list like "Standardsider" (in the
4
- // same file, config.standardPages; standardPages.ts), and "Samtykker", the visitors' answers in the banner.
1
+ // "Privacy": what the site asks and tells its visitors about their data, as screens of their own (CompanyForm.tsx),
2
+ // at #privacy/<id>, and the cookie list's row: the cookie banner (src/data/cookies.json, read by packages/editor/theme/components/CookieConsent.astro),
3
+ // "Privacy pages", which pages are the privacy policy and the cookie policy, as a list like "Default pages" (in the
4
+ // same file, config.standardPages; standardPages.ts), and "Consents", the visitors' answers in the banner.
5
5
  import { Cookie, FileText, ListChecks } from '@phosphor-icons/react';
6
6
  import type { CookieCategory } from '../lib/cookieCategories.mjs';
7
7
  import type { Field, PartScreen } from '../src/schema';
@@ -9,74 +9,74 @@ import { standardPageFieldsOf } from '@site/collections';
9
9
  import { config } from '@site/config';
10
10
 
11
11
  /** The fields of a category of cookies in the banner: its name and what it is for. */
12
- function categoryFields(category: CookieCategory, place: string, hint = 'Vises, når en app beder om den.'): Field[] {
12
+ function categoryFields(category: CookieCategory, place: string, hint = 'Shown when an app asks for it.'): Field[] {
13
13
  return [
14
- { name: category, label: `Navn (${place})`, type: 'string', required: true, max: 30, hint },
15
- { name: `${category}Text`, label: `Tekst (${place})`, type: 'text', required: true },
14
+ { name: category, label: `Name (${place})`, type: 'string', required: true, max: 30, hint },
15
+ { name: `${category}Text`, label: `Text (${place})`, type: 'text', required: true },
16
16
  ];
17
17
  }
18
18
 
19
19
  export const privacy: PartScreen = {
20
- id: 'privatliv',
21
- label: 'Privatliv',
20
+ id: 'privacy',
21
+ label: 'Privacy',
22
22
  parts: [
23
23
  {
24
- id: 'cookiebanner',
25
- label: 'Cookiebanner',
24
+ id: 'cookie-banner',
25
+ label: 'Cookie banner',
26
26
  icon: Cookie,
27
27
  file: 'src/data/cookies.json',
28
28
  fields: [
29
29
  {
30
30
  name: 'enabled',
31
- label: 'Aktiveret',
31
+ label: 'Activated',
32
32
  type: 'boolean',
33
- toggle: ['Til', 'Fra'],
34
- hint: 'Når banneret er fra, er der ingen måling eller annoncer.',
35
- edit: { to: 'src/content/pages/privatlivspolitik.md', text: 'Rediger privatlivspolitik' },
33
+ toggle: ['On', 'Off'],
34
+ hint: 'When the banner is off, there is no analytics or ads.',
35
+ edit: { to: 'src/content/pages/privatlivspolitik.md', text: 'Edit privacy policy' },
36
36
  },
37
- { name: 'heading', label: 'Overskrift', type: 'string', required: true },
38
- { name: 'text', label: 'Tekst', type: 'text', required: true },
37
+ { name: 'heading', label: 'Heading', type: 'string', required: true },
38
+ { name: 'text', label: 'Text', type: 'text', required: true },
39
39
  {
40
40
  name: 'more',
41
- label: 'Linktekst',
41
+ label: 'Link text',
42
42
  type: 'string',
43
43
  required: true,
44
- hint: 'Linket går til cookiepolitikken.',
45
- edit: { to: 'privatliv/privatlivssider', text: 'Rediger privatlivssider' },
44
+ hint: 'The link goes to the cookie policy.',
45
+ edit: { to: 'privacy/privacy-pages', text: 'Edit privacy pages' },
46
46
  },
47
- { name: 'no', label: 'Knaptekst (nej)', type: 'string', required: true, max: 20 },
48
- { name: 'yes', label: 'Knaptekst (ja)', type: 'string', required: true, max: 20 },
49
- { name: 'choose', label: 'Knaptekst (avanceret)', type: 'string', required: true, max: 30 },
50
- { name: 'save', label: 'Knaptekst (gem valg)', type: 'string', required: true, max: 30 },
47
+ { name: 'no', label: 'Button text (no)', type: 'string', required: true, max: 20 },
48
+ { name: 'yes', label: 'Button text (yes)', type: 'string', required: true, max: 20 },
49
+ { name: 'choose', label: 'Button text (advanced)', type: 'string', required: true, max: 30 },
50
+ { name: 'save', label: 'Button text (save choice)', type: 'string', required: true, max: 30 },
51
51
  {
52
52
  name: 'consentId',
53
- label: 'Tekst (samtykke-id)',
53
+ label: 'Text (consent ID)',
54
54
  type: 'string',
55
55
  required: true,
56
56
  max: 30,
57
- hint: 'Vises med den besøgendes id efter et svar.',
57
+ hint: 'Shown with the visitor’s ID after an answer.',
58
58
  },
59
- // Each category of cookies, with what it is for, when the banner's "Avanceret" shows them (the necessary ones
59
+ // Each category of cookies, with what it is for, when the banner's "Advanced" shows them (the necessary ones
60
60
  // always, the others when an app asks about them, packages/editor/site/consent.ts).
61
- ...categoryFields('necessary', 'nødvendige', 'Vises altid; de kan ikke slås fra.'),
62
- ...categoryFields('preferences', 'præferencer'),
63
- ...categoryFields('statistics', 'statistik'),
61
+ ...categoryFields('necessary', 'necessary', 'Always shown; they cannot be switched off.'),
62
+ ...categoryFields('preferences', 'preferences'),
63
+ ...categoryFields('statistics', 'statistics'),
64
64
  ...categoryFields('marketing', 'marketing'),
65
- ...categoryFields('targeting', 'målrettede annoncer'),
65
+ ...categoryFields('targeting', 'targeted ads'),
66
66
  ],
67
67
  },
68
- // The site's list of cookies ("Cookies", the standard block "Cookieliste"), a screen of its own under "Lister"'s
69
- // address space (#liste/<id>): only its row is here, and the list goes back to this screen (Library.tsx).
70
- { id: 'cookies', label: 'Cookies', icon: Cookie, href: `#liste/${config.lists.cookies}` },
68
+ // The site's list of cookies ("Cookies", the standard block "Cookie list"), a screen of its own under "Lists"'s
69
+ // address space (#list/<id>): only its row is here, and the list goes back to this screen (Library.tsx).
70
+ { id: 'cookies', label: 'Cookies', icon: Cookie, href: `#list/${config.lists.cookies}` },
71
71
  {
72
- id: 'privatlivssider',
73
- label: 'Privatlivssider',
72
+ id: 'privacy-pages',
73
+ label: 'Privacy pages',
74
74
  icon: FileText,
75
75
  file: config.standardPages,
76
76
  fields: standardPageFieldsOf('privacy', 'cookies'),
77
77
  rows: true,
78
78
  },
79
79
  // The visitors' answers in the banner, kept by the Worker (packages/editor/worker/consent.js): the editor's own screen (Consents.tsx).
80
- { id: 'samtykker', label: 'Samtykker', icon: ListChecks, opens: 'consents' },
80
+ { id: 'consents', label: 'Consents', icon: ListChecks, opens: 'consents' },
81
81
  ],
82
82
  };
package/defaults/rules.ts CHANGED
@@ -20,7 +20,7 @@ const fm = (file: RepoFile) => readFields(splitFrontmatter(file.text).yaml);
20
20
  // A page's address: `path`, or /<file name>/ for pages made in the editor.
21
21
  const pagePath = (f: RepoFile) => String(fm(f).path ?? `/${slugOf(f.path, config.folders.pages)}/`);
22
22
  const isPage = (f: RepoFile) => f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md');
23
- // The addresses of the standard pages, by their key ("Indstillinger" › "Udseende" › "Standardsider", standardPages.ts).
23
+ // The addresses of the standard pages, by their key ("Settings" › "Appearance" › "Default pages", standardPages.ts).
24
24
  const standardOf = (files: { path: string; text: string }[]) => standardPaths(files.find((f) => f.path === config.standardPages)?.text);
25
25
  // The page that is a standard page (the front page: 'front'), if it is there.
26
26
  const standardFile = (files: RepoFile[], key: string) => {
@@ -64,7 +64,7 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
64
64
  return {
65
65
  // Pages are stored by their address: `path`, or /<file name>/ for pages made in the editor.
66
66
  pages: inFolder(config.folders.pages, '.md').map(page).sort(byLabel),
67
- // The pages that can be a standard page ("Indstillinger" › "Udseende" › "Standardsider"): those on the site, not a
67
+ // The pages that can be a standard page ("Settings" › "Appearance" › "Default pages"): those on the site, not a
68
68
  // draft, one waiting for its time or a new version of a page (/ny/).
69
69
  standardPages: inFolder(config.folders.pages, '.md')
70
70
  .filter((f) => !['draft', 'scheduled'].includes(String(fm(f).status ?? '')) && !pagePath(f).startsWith('/ny/'))
@@ -72,7 +72,7 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
72
72
  .sort(byLabel),
73
73
  // The front page (/) and the blog (/blog/), whichever pages they are, the blog by its page's title.
74
74
  fixed: [
75
- { value: '/', label: siteText('Forside'), inMenu: menu.has('/') },
75
+ { value: '/', label: siteText('Front page'), inMenu: menu.has('/') },
76
76
  {
77
77
  value: '/blog/',
78
78
  label: String((standardFile(files, 'blog') && fm(standardFile(files, 'blog')!).title) || siteText('Blog')),
@@ -93,9 +93,9 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
93
93
  testimonials: inFolder('src/data/testimonials', '.json')
94
94
  .map((f) => ({ value: slugOf(f.path, 'src/data/testimonials'), label: String(json(f).name) }))
95
95
  .sort(byLabel),
96
- // The promotions turned on that a button can open ("Knapper": "Åbn promovering", opens.ts), by what opens with
97
- // the promotion's name ("Bookingarket (Calendly bookingark)"), where they are shown (a button offers those on its
98
- // page) and the button's text: a pop-up's heading, or that of the target it stands for ("Book en tid").
96
+ // The promotions turned on that a button can open ("Buttons": "Open promotion", opens.ts), by what opens with
97
+ // the promotion's name ("Booking sheet (Calendly booking sheet)"), where they are shown (a button offers those on its
98
+ // page) and the button's text: a pop-up's heading, or that of the target it stands for ("Book an appointment").
99
99
  promotions: inFolder('src/data/popups', '.json')
100
100
  .flatMap((f) => {
101
101
  const d = json(f);
@@ -112,7 +112,7 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
112
112
  ];
113
113
  })
114
114
  .sort(byLabel),
115
- // The PDFs in "Filer", by their address on the site (/filer/<name>.pdf), named by their file name.
115
+ // The PDFs in "Files", by their address on the site (/filer/<name>.pdf), named by their file name.
116
116
  files: inFolder(config.folders.files, '.pdf')
117
117
  .map((f) => ({ value: f.path.replace(/^public/, ''), label: f.path.split('/').pop()! }))
118
118
  .sort(byLabel),
@@ -121,16 +121,16 @@ export function relationsOf(files: RepoFile[]): Record<Relation, Option[]> {
121
121
 
122
122
  const frontPage = (files: RepoFile[]) => ({
123
123
  to: standardFile(files, 'front')?.path ?? config.standardPages,
124
- text: siteText('Rediger forsiden'),
124
+ text: siteText('Edit the front page'),
125
125
  });
126
126
 
127
127
  // Where a button that opens a promotion is: a page (`path`), the shared buttons (on any page: neither), or an action bar
128
128
  // (`bar`: the pages it is shown on, or none for every page or the posts with its tags).
129
129
  type Opener = { file: string; title: string; path?: string; bar?: { pages?: string[] } };
130
130
 
131
- // The buttons that open a promotion ("Knapper": "Åbn promovering", the button's `open`), where they are: the page with
131
+ // The buttons that open a promotion ("Buttons": "Open promotion", the button's `open`), where they are: the page with
132
132
  // the block (its file, title and the address it is shown at: the front page's /), the shared buttons
133
- // ("Standardblokke" › "Knapper", on any page: no address), or an action bar ("Promovering").
133
+ // ("Standard blocks" › "Buttons", on any page: no address), or an action bar ("Promotion").
134
134
  function openersOf(slug: string, files: { path: string; text: string }[]): Opener[] {
135
135
  const opens = (items: unknown) =>
136
136
  Array.isArray(items) &&
@@ -143,7 +143,7 @@ function openersOf(slug: string, files: { path: string; text: string }[]): Opene
143
143
  const found: Opener[] = [];
144
144
  for (const f of files) {
145
145
  if (f.path.startsWith(`${config.folders.pages}/`) && f.path.endsWith('.md')) {
146
- // Also a "Knapper" block in a block's text (a "Banner"'s), where its quotes are written as &quot;.
146
+ // Also a "Buttons" block in a block's text (a "Banner"'s), where its quotes are written as &quot;.
147
147
  if (!/data-block=(?:"|\\&quot;)buttons/.test(f.text)) continue;
148
148
  const d = fm(f as RepoFile);
149
149
  if (
@@ -155,12 +155,12 @@ function openersOf(slug: string, files: { path: string; text: string }[]): Opene
155
155
  if (d.type === 'bookingbar' && d.enabled !== false && opens(d.buttons))
156
156
  found.push({
157
157
  file: f.path,
158
- title: siteText('Handlingslinje'),
158
+ title: siteText('Action bar'),
159
159
  bar: { pages: d.where === 'all' || d.where === 'tags' ? undefined : Array.isArray(d.pages) ? d.pages.map(String) : [] },
160
160
  });
161
161
  } else if (f.path === config.blockTexts) {
162
162
  if (opens((json(f as RepoFile).buttons as Record<string, unknown> | undefined)?.items))
163
- found.push({ file: 'blokke/buttons', title: siteText('Standardknapper') });
163
+ found.push({ file: 'blocks/buttons', title: siteText('Default buttons') });
164
164
  }
165
165
  }
166
166
  return found;
@@ -180,20 +180,20 @@ function shownFor(d: Record<string, unknown>, at: Opener) {
180
180
  const openerText = (at: Opener) =>
181
181
  at.bar
182
182
  ? siteText(
183
- 'En knap i handlingslinjen åbner promoveringen, så den skal være slået til og vises alle steder, hvor linjen vises. Ret knappen først.',
183
+ 'A button in the action bar opens the promotion, so it must be turned on and shown everywhere the bar is shown. Change the button first.',
184
184
  )
185
185
  : at.path
186
- ? siteText('En knap på siden "{side}" åbner promoveringen, så den skal være slået til og vises dér. Ret knappen først.', {
186
+ ? siteText('A button on the page "{side}" opens the promotion, so it must be on and shown there. Change the button first.', {
187
187
  side: at.title,
188
188
  })
189
- : siteText('Standardknapperne åbner promoveringen, så den skal være slået til og vises på alle sider. Ret dem først.');
189
+ : siteText('The default buttons open the promotion, so it must be on and shown on every page. Change them first.');
190
190
  const openerEdit = (at: Opener) => ({
191
191
  to: at.file,
192
- text: siteText(at.bar ? 'Rediger handlingslinjen' : at.path ? 'Rediger siden' : 'Rediger standardknapperne'),
192
+ text: siteText(at.bar ? 'Edit the action bar' : at.path ? 'Edit the page' : 'Edit the default buttons'),
193
193
  });
194
194
 
195
195
  /** Why a promotion cannot be saved like this (turned off, or no longer shown on a page where a button opens it,
196
- * "Knapper": "Åbn promovering"), with a link to the button, or the standard pages (the same page twice, or a page
196
+ * "Buttons": "Open promotion"), with a link to the button, or the standard pages (the same page twice, or a page
197
197
  * without the block it must have, with a link to it), or null. Before it is saved (DataForm.tsx; the standard pages in
198
198
  * CompanyForm.tsx, without a collection). */
199
199
  export function keptBy(collection: Collection | null, path: string, data: Record<string, unknown>, files: RepoFile[]): Status {
@@ -208,18 +208,18 @@ export function keptBy(collection: Collection | null, path: string, data: Record
208
208
  function standardKept(data: Record<string, unknown>, files: RepoFile[]): Status {
209
209
  const chosen = standardPages.map((p) => ({ ...p, path: String(data[p.key] ?? '') })).filter((p) => p.path);
210
210
  if (!differentPages(Object.fromEntries(chosen.map((p) => [p.key, p.path]))))
211
- return { text: siteText('Standardsiderne skal være forskellige sider.'), error: true };
211
+ return { text: siteText('The default pages must be different pages.'), error: true };
212
212
  for (const p of chosen) {
213
213
  const page = files.find((f) => isPage(f) && pagePath(f) === p.path);
214
214
  if (!p.needs || !page || blocksIn(page.text).some((b: { type: string }) => b.type === p.needs!.type)) continue;
215
215
  return {
216
- text: siteText('Siden "{side}" har ikke blokken {blok}, som {hvad} skal have. Sæt den ind på siden først.', {
216
+ text: siteText('The page "{side}" does not have the block {blok}, which {hvad} must have. Put it in on the page first.', {
217
217
  side: String(fm(page).title),
218
218
  blok: siteText(p.needs.label),
219
219
  hvad: siteText(p.the).toLowerCase(),
220
220
  }),
221
221
  error: true,
222
- edit: { to: page.path, text: siteText('Rediger siden') },
222
+ edit: { to: page.path, text: siteText('Edit the page') },
223
223
  };
224
224
  }
225
225
  return null;
@@ -230,16 +230,16 @@ function standardKept(data: Record<string, unknown>, files: RepoFile[]): Status
230
230
  * shown on its page. */
231
231
  export function siteProblems(files: { path: string; text: string }[]): string[] {
232
232
  const problems: string[] = [];
233
- // The standard pages are pages on the site ("Indstillinger" › "Udseende" › "Standardsider"), different ones (but for
233
+ // The standard pages are pages on the site ("Settings" › "Appearance" › "Default pages"), different ones (but for
234
234
  // the cookie policy, which may be the privacy policy).
235
235
  if (files.some((f) => f.path === config.standardPages)) {
236
236
  for (const p of standardPages) {
237
237
  const page = standardFile(files as RepoFile[], p.key);
238
238
  if (!page || ['draft', 'scheduled'].includes(String(fm(page).status ?? '')))
239
- problems.push(`${config.standardPages}: ${siteText('{side} skal være en side, der er udgivet.', { side: siteText(p.the) })}`);
239
+ problems.push(`${config.standardPages}: ${siteText('{side} must be a published page.', { side: siteText(p.the) })}`);
240
240
  }
241
241
  if (!differentPages(standardOf(files)))
242
- problems.push(`${config.standardPages}: ${siteText('Standardsiderne skal være forskellige sider.')}`);
242
+ problems.push(`${config.standardPages}: ${siteText('The default pages must be different pages.')}`);
243
243
  }
244
244
  const promotions = files.filter((f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json'));
245
245
  const slugs = new Set<string>();
@@ -249,8 +249,8 @@ export function siteProblems(files: { path: string; text: string }[]): string[]
249
249
  for (const at of openersOf(slug, files)) {
250
250
  if (file && shownFor(json(file as RepoFile), at)) continue;
251
251
  problems.push(
252
- `${at.file.startsWith('blokke/') ? config.blockTexts : at.file}: ${
253
- file ? openerText(at) : siteText('En knap åbner promoveringen "{navn}", som ikke findes.', { navn: slug })
252
+ `${at.file.startsWith('blocks/') ? config.blockTexts : at.file}: ${
253
+ file ? openerText(at) : siteText('A button opens the promotion "{navn}", which does not exist.', { navn: slug })
254
254
  }`,
255
255
  );
256
256
  }
@@ -272,16 +272,16 @@ const TAG_FOLDERS: TagFolders = {
272
272
  export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]): Status {
273
273
  if (collection.kind === 'files') return null;
274
274
  const slug = slugOf(file.path, collection.folder);
275
- // A promotion a button opens ("Knapper": "Åbn promovering").
275
+ // A promotion a button opens ("Buttons": "Open promotion").
276
276
  if (collection.id === 'popups') {
277
277
  const [at] = openersOf(slug, files);
278
278
  if (at)
279
279
  return {
280
280
  text: at.bar
281
- ? siteText('En knap i handlingslinjen åbner promoveringen. Ret knappen først.')
281
+ ? siteText('A button in the action bar opens the promotion. Change the button first.')
282
282
  : at.path
283
- ? siteText('En knap på siden "{side}" åbner promoveringen. Ret knappen først.', { side: at.title })
284
- : siteText('Standardknapperne åbner promoveringen. Ret dem først.'),
283
+ ? siteText('A button on the page "{side}" opens the promotion. Change the button first.', { side: at.title })
284
+ : siteText('The default buttons open the promotion. Change them first.'),
285
285
  error: true,
286
286
  edit: openerEdit(at),
287
287
  };
@@ -290,69 +290,71 @@ export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]
290
290
  const front = Object.fromEntries(
291
291
  (home ? (blocksIn(home.text) as { type: string; props: Record<string, unknown> }[]) : []).map((b) => [b.type, b.props]),
292
292
  );
293
- if (collection.id === 'kategorier') {
293
+ if (collection.id === 'tags') {
294
294
  const { posts, pages, testimonials: quotes, records } = taggedWith(files, slug, TAG_FOLDERS);
295
295
  if (posts.length)
296
296
  return {
297
297
  text:
298
298
  posts.length === 1
299
- ? siteText('Tagget bruges på et blogindlæg. Fjern det fra det først.')
300
- : siteText('Tagget bruges på {antal} blogindlæg. Fjern det fra dem først.', { antal: posts.length }),
299
+ ? siteText('The tag is used on a blog post. Remove it from the post first.')
300
+ : siteText('The tag is used on {antal} blog posts. Remove it from them first.', { antal: posts.length }),
301
301
  error: true,
302
302
  };
303
303
  if (pages.length)
304
304
  return {
305
305
  text:
306
306
  pages.length === 1
307
- ? siteText('Tagget bruges på en side. Fjern det fra den først.')
308
- : siteText('Tagget bruges på {antal} sider. Fjern det fra dem først.', { antal: pages.length }),
307
+ ? siteText('The tag is used on a page. Remove it from the page first.')
308
+ : siteText('The tag is used on {antal} pages. Remove it from them first.', { antal: pages.length }),
309
309
  error: true,
310
- ...(pages.length === 1 ? { edit: { to: pages[0].path, text: siteText('Rediger siden') } } : {}),
310
+ ...(pages.length === 1 ? { edit: { to: pages[0].path, text: siteText('Edit the page') } } : {}),
311
311
  };
312
312
  if (quotes.length)
313
313
  return {
314
314
  text:
315
315
  quotes.length === 1
316
- ? siteText('Tagget bruges på en udtalelse. Fjern det fra den først.')
317
- : siteText('Tagget bruges på {antal} udtalelser. Fjern det fra dem først.', { antal: quotes.length }),
316
+ ? siteText('The tag is used on a testimonial. Remove it from the testimonial first.')
317
+ : siteText('The tag is used on {antal} testimonials. Remove it from them first.', { antal: quotes.length }),
318
318
  error: true,
319
319
  };
320
320
  if (records.length)
321
321
  return {
322
322
  text:
323
323
  records.length === 1
324
- ? siteText('Tagget bruges på en fil. Fjern det fra den først.')
325
- : siteText('Tagget bruges på {antal} filer. Fjern det fra dem først.', { antal: records.length }),
324
+ ? siteText('The tag is used on a file. Remove it from the file first.')
325
+ : siteText('The tag is used on {antal} files. Remove it from them first.', { antal: records.length }),
326
326
  error: true,
327
- ...(records.length === 1 ? { edit: { to: recordUse(records[0], config.fileRecords)!.path, text: siteText('Åbn filen') } } : {}),
327
+ ...(records.length === 1 ? { edit: { to: recordUse(records[0], config.fileRecords)!.path, text: siteText('Open the file') } } : {}),
328
328
  };
329
- // A promotion shown on the posts with the tag (Promovering, "Blogindlæg med tags").
329
+ // A promotion shown on the posts with the tag (Promotion, "Blog posts with tags").
330
330
  const promotion = files.find(
331
331
  (f) => f.path.startsWith('src/data/popups/') && f.path.endsWith('.json') && promotionTags(json(f)).includes(slug),
332
332
  );
333
333
  if (promotion) {
334
334
  const d = json(promotion);
335
335
  return {
336
- text: siteText('Tagget bruges i promoveringen "{navn}". Fjern det fra den først.', { navn: String(d.heading ?? d.name ?? '') }),
336
+ text: siteText('The tag is used in the promotion "{navn}". Remove it from the promotion first.', {
337
+ navn: String(d.heading ?? d.name ?? ''),
338
+ }),
337
339
  error: true,
338
- edit: { to: promotion.path, text: siteText('Rediger promoveringen') },
340
+ edit: { to: promotion.path, text: siteText('Edit the promotion') },
339
341
  };
340
342
  }
341
343
  const fp = front.posts as Record<string, unknown> | undefined;
342
344
  if ((fp?.categories as string[] | undefined)?.includes(slug) || fp?.link === slug)
343
345
  return {
344
- text: siteText('Tagget vælger indlæggene på forsiden under "Fra bloggen". Vælg et andet først.'),
346
+ text: siteText('The tag picks the posts on the front page under "From the blog". Pick another one first.'),
345
347
  error: true,
346
348
  edit: frontPage(files),
347
349
  };
348
350
  }
349
351
  if (collection.id === 'blog' && ((front.posts as Record<string, unknown>)?.picked as string[])?.includes(slug))
350
352
  return {
351
- text: siteText('Indlægget er valgt til forsiden under "Fra bloggen". Vælg et andet først.'),
353
+ text: siteText('The post is picked for the front page under "From the blog". Pick another one first.'),
352
354
  error: true,
353
355
  edit: frontPage(files),
354
356
  };
355
- // A "Blogindlæg" block in a page that picks the post (its props are in the text).
357
+ // A "Blog posts" block in a page that picks the post (its props are in the text).
356
358
  if (collection.id === 'blog') {
357
359
  const page = files.find(
358
360
  (f) =>
@@ -360,28 +362,28 @@ export function usedBy(collection: Collection, file: RepoFile, files: RepoFile[]
360
362
  );
361
363
  if (page)
362
364
  return {
363
- text: siteText('Indlægget er valgt i en blok med blogindlæg på siden "{side}". Vælg et andet først.', {
365
+ text: siteText('The post is picked in a block of blog posts on the page "{side}". Pick another one first.', {
364
366
  side: String(fm(page).title),
365
367
  }),
366
368
  error: true,
367
- edit: { to: page.path, text: siteText('Rediger siden') },
369
+ edit: { to: page.path, text: siteText('Edit the page') },
368
370
  };
369
371
  }
370
- // Any other place under "Bruges på" (usesOf): a "Blogindlæg" or "Udtalelser" block that picks the tag, on a
372
+ // Any other place under "Used on" (usesOf): a "Blog posts" or "Testimonials" block that picks the tag, on a
371
373
  // page or in a post. Files with the tag are handled above (records), so the first use is never a file record.
372
- if (collection.id === 'kategorier') {
374
+ if (collection.id === 'tags') {
373
375
  const use = usesOf(collection, files)(file)[0];
374
376
  if (use) {
375
377
  const post = use.path.startsWith(`${config.folders.blog}/`);
376
378
  return {
377
379
  text: siteText(
378
380
  post
379
- ? 'Tagget er valgt i en blok i blogindlægget "{side}". Vælg et andet først.'
380
- : 'Tagget er valgt i en blok på siden "{side}". Vælg et andet først.',
381
+ ? 'The tag is picked in a block in the blog post "{side}". Pick another one first.'
382
+ : 'The tag is picked in a block on the page "{side}". Pick another one first.',
381
383
  { side: use.name },
382
384
  ),
383
385
  error: true,
384
- edit: { to: use.path, text: siteText(post ? 'Rediger indlægget' : 'Rediger siden') },
386
+ edit: { to: use.path, text: siteText(post ? 'Edit the post' : 'Edit the page') },
385
387
  };
386
388
  }
387
389
  }
@@ -402,12 +404,12 @@ function nameOf(file: RepoFile): string {
402
404
 
403
405
  export type Use = { path: string; name: string };
404
406
 
405
- // The places on the site that use an entry, by the entry's file, each once (the tag's "Bruges på", DataForm.tsx,
407
+ // The places on the site that use an entry, by the entry's file, each once (the tag's "Used on", DataForm.tsx,
406
408
  // and the number at it in its list, Library.tsx); [] for none. For a tag: the blog posts, pages, testimonials,
407
- // promotions and files (by the route "filer/<path>" and the file's name) that have it, and the pages and posts with a "Blogindlæg" or "Udtalelser" block that
408
- // picks it (its tags, or the tag "Alle indlæg" goes to). Made once for the whole list.
409
+ // promotions and files (by the route "files/<path>" and the file's name) that have it, and the pages and posts with a "Blog posts" or "Testimonials" block that
410
+ // picks it (its tags, or the tag "All posts" goes to). Made once for the whole list.
409
411
  export function usesOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => Use[] {
410
- if (collection.kind === 'files' || collection.id !== 'kategorier') return () => [];
412
+ if (collection.kind === 'files' || collection.id !== 'tags') return () => [];
411
413
  const uses = tagUses(files, TAG_FOLDERS);
412
414
  return (file) =>
413
415
  [...(uses.get(slugOf(file.path, collection.folder))?.values() ?? [])]
@@ -418,7 +420,7 @@ export function usesOf(collection: Collection, files: RepoFile[]): (file: RepoFi
418
420
  // The number at the right of an entry in the list of a part, by the entry's file, or null for none
419
421
  // (Library.tsx): at each tag, the number of places that use it (usesOf).
420
422
  export function countsOf(collection: Collection, files: RepoFile[]): (file: RepoFile) => number | null {
421
- if (collection.kind === 'files' || collection.id !== 'kategorier') return () => null;
423
+ if (collection.kind === 'files' || collection.id !== 'tags') return () => null;
422
424
  const uses = tagUses(files, TAG_FOLDERS);
423
425
  return (file) => uses.get(slugOf(file.path, collection.folder))?.size ?? 0;
424
426
  }
@@ -430,19 +432,19 @@ export const testimonialsOf = (files: RepoFile[]) =>
430
432
  .map((f) => ({ ...(json(f) as TestimonialRow & { order?: number }), slug: slugOf(f.path, 'src/data/testimonials') }))
431
433
  .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
432
434
 
433
- /** What is at an address, as said in a message: "forsiden", 'siden "Om mig" (/om-mig/)',
435
+ /** What is at an address, as said in a message: 'the front page', 'the page "About me" (/about-me/)',
434
436
  * or the address (where a redirect sends to, App.tsx). */
435
437
  export function whatIsAt(address: string, relations: Record<Relation, Option[]>): string {
436
438
  const base = address.split('#')[0];
437
- if (base === '/') return siteText('forsiden');
438
- if (base === '/blog/') return siteText('bloggen');
439
+ if (base === '/') return siteText('the front page');
440
+ if (base === '/blog/') return siteText('the blog');
439
441
  const page = [...relations.fixed, ...relations.pages].find((o) => o.value === base);
440
- if (page) return siteText('siden "{navn}" ({adresse})', { navn: page.label, adresse: address });
442
+ if (page) return siteText('the page "{navn}" ({adresse})', { navn: page.label, adresse: address });
441
443
  const post = relations.blog.find((o) => `/${o.value}/` === base);
442
- if (post) return siteText('blogindlægget "{navn}" ({adresse})', { navn: post.label, adresse: address });
444
+ if (post) return siteText('the blog post "{navn}" ({adresse})', { navn: post.label, adresse: address });
443
445
  const tag = relations.categories.find((o) => `/blog/kategori/${o.value}/` === base);
444
- if (tag) return siteText('tagget "{navn}" på bloggen ({adresse})', { navn: tag.label, adresse: address });
446
+ if (tag) return siteText('the tag "{navn}" on the blog ({adresse})', { navn: tag.label, adresse: address });
445
447
  if (base.startsWith(config.folders.media.replace(/^public/, '') + '/'))
446
- return siteText('billedet {navn}', { navn: base.split('/').pop()! });
448
+ return siteText('the picture {navn}', { navn: base.split('/').pop()! });
447
449
  return address;
448
450
  }
@@ -5,7 +5,7 @@ import { config } from '@site/config';
5
5
  import type { SiteSchema } from '../site/schema';
6
6
 
7
7
  export const schema: SiteSchema = {
8
- ids: { business: 'virksomhed', person: 'person' },
8
+ ids: { business: 'business', person: 'person' },
9
9
  businessType: 'LocalBusiness',
10
10
  country: config.locale.split('-')[1] ?? 'DK',
11
11
  terms: {},
@@ -1,7 +1,7 @@
1
1
  // What the previews of the blocks (src/apps/<app>/blocks/, previews.tsx) show from elsewhere on the site, from the files
2
2
  // as the editor has them now: the pages (their titles and texts, for the topics and the services),
3
3
  // the blog posts, the phone, e-mail and address (contact.json), the payment details (payment.json) and
4
- // the texts of the "Book en tid" box (booking.json), the settings of the apps (src/apps/) and the cookie list.
4
+ // the texts of the "Book an appointment" box (booking.json), the settings of the apps (src/apps/) and the cookie list.
5
5
  import type { RepoFile } from '../src/github';
6
6
  import type { StandardSiteData } from '../src/standard/site';
7
7
  import { categoryOf } from '../lib/cookieCategories.mjs';
@@ -30,13 +30,13 @@ export type SiteData = {
30
30
  /** The bank details, and what else the site keeps in payment.json (a site's other ways to pay). */
31
31
  payment: { bank: string; reg: string; account: string } & Record<string, string>;
32
32
  booking: { heading: string; text: string };
33
- /** The labels of the buttons "Book en tid" and "Ring …" (booking.json). */
33
+ /** The labels of the buttons "Book an appointment" and "Call …" (booking.json). */
34
34
  labels: { book: string; call: string };
35
35
  /** The texts every copy of a block has (its shared.ts, saved in blocks.json). */
36
36
  blocks: BlockTexts;
37
- /** The texts of the buttons of "Knapper" that go elsewhere than a page (config.buttonTargets). */
37
+ /** The texts of the buttons of "Buttons" that go elsewhere than a page (config.buttonTargets). */
38
38
  buttons: StandardSiteData['buttons'];
39
- /** The tags not shown on the blog ("Blogkategori" off, packages/editor/site/categories.ts). */
39
+ /** The tags not shown on the blog ("Blog category" off, packages/editor/site/categories.ts). */
40
40
  tagsOffBlog: string[];
41
41
  /** The settings of the apps as saved (config.apps.settings), by the app's id; an app's blocks put
42
42
  * them over those in its settings.ts. */
@@ -27,7 +27,7 @@ import { blockTexts, nav, site } from '../site/site';
27
27
  import { popups } from '../site/popups';
28
28
  import { opensOf } from '@site/opens';
29
29
 
30
- // A post as "Blogindlæg" shows it.
30
+ // A post as "Blog posts" shows it.
31
31
  const listedPost = (p: Post): ListedPost => ({
32
32
  href: postPath(p),
33
33
  title: p.data.title,
@@ -61,14 +61,14 @@ export const standard: StandardSite = {
61
61
  const page = (await byPath()).get(href);
62
62
  return page ? displayTitle(page.data.title) : nav.flatMap((n) => [n, ...(n.children ?? [])]).find((n) => n.href === href)?.label;
63
63
  },
64
- // "Book en tid" opens the booking sheet on the page (`data-open="booking"`, the app Calendly's), and
65
- // goes to the booking page without one there; "Ring" has the number after its text.
64
+ // "Book an appointment" opens the booking sheet on the page (`data-open="booking"`, the app Calendly's), and
65
+ // goes to the booking page without one there; "Call" has the number after its text.
66
66
  buttons: {
67
67
  booking: { href: site.bookingPath, text: site.labels.book, open: 'booking' },
68
68
  phone: { href: site.phoneHref, text: site.labels.call, after: site.phone },
69
69
  },
70
- // "Åbn promovering": a promotion turned on that a button can open (opens.ts). One that stands for a target does
71
- // what it does (the booking sheet: "Book en tid"); a pop-up opens itself (`data-open="promotion:<file name>"`,
70
+ // "Open promotion": a promotion turned on that a button can open (opens.ts). One that stands for a target does
71
+ // what it does (the booking sheet: "Book an appointment"); a pop-up opens itself (`data-open="promotion:<file name>"`,
72
72
  // Popup.astro), with its heading as the text.
73
73
  picked(target, value) {
74
74
  const promotion = target === 'promotion' ? popups.find((p) => p.slug === value) : undefined;
@@ -85,7 +85,10 @@ export const standard: StandardSite = {
85
85
  // Only a tag with posts has a page (/blog/kategori/<slug>/).
86
86
  async tag(slug) {
87
87
  tagPages ??= getCategories().then((all) => new Set(all.map((c) => c.slug)));
88
- return { name: categoryName(slug), href: (await tagPages).has(slug) ? `/blog/kategori/${slug}/` : undefined };
88
+ return {
89
+ name: categoryName(slug),
90
+ href: (await tagPages).has(slug) ? `/blog/kategori/${slug}/` : undefined,
91
+ };
89
92
  },
90
93
  },
91
94
  posts: {