@morphemecms/cms 0.12.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 (121) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/defaults/blockTexts.ts +9 -9
  3. package/defaults/collections.ts +257 -257
  4. package/defaults/colors.ts +74 -74
  5. package/defaults/company.ts +9 -9
  6. package/defaults/editor.ts +65 -65
  7. package/defaults/i18n/da-DK/blockTexts.ts +12 -0
  8. package/defaults/i18n/da-DK/collections.ts +220 -0
  9. package/defaults/i18n/da-DK/colors.ts +102 -0
  10. package/defaults/i18n/da-DK/company.ts +7 -0
  11. package/defaults/i18n/da-DK/editor.ts +13 -0
  12. package/defaults/i18n/da-DK/index.ts +31 -0
  13. package/defaults/i18n/da-DK/internalLinks.ts +12 -0
  14. package/defaults/i18n/da-DK/opens.ts +7 -0
  15. package/defaults/i18n/da-DK/popup.ts +9 -0
  16. package/defaults/i18n/da-DK/privacy.ts +38 -0
  17. package/defaults/i18n/da-DK/rules.ts +56 -0
  18. package/defaults/i18n/da-DK/sitedata.ts +8 -0
  19. package/defaults/i18n/da-DK/standard.ts +8 -0
  20. package/defaults/i18n/da-DK/standardPages.ts +17 -0
  21. package/defaults/internalLinks.ts +8 -8
  22. package/defaults/opens.ts +2 -2
  23. package/defaults/popup.tsx +7 -7
  24. package/defaults/privacy.ts +37 -37
  25. package/defaults/rules.ts +68 -66
  26. package/defaults/schema.ts +1 -1
  27. package/defaults/sitedata.ts +4 -4
  28. package/defaults/standard.ts +9 -6
  29. package/defaults/standardPages.ts +14 -14
  30. package/defaults/texts.ts +7 -510
  31. package/lib/appRoute.d.mts +2 -2
  32. package/lib/appRoute.mjs +3 -3
  33. package/lib/appRoute.test.mjs +8 -8
  34. package/lib/licenses.mjs +2 -2
  35. package/lib/licenses.owner.test.mjs +9 -0
  36. package/lib/menu.mjs +4 -4
  37. package/lib/menu.test.mjs +101 -101
  38. package/lib/scheduled.mjs +1 -1
  39. package/lib/storeEntries.test.mjs +3 -0
  40. package/lib/translate.d.mts +11 -0
  41. package/lib/translate.mjs +13 -1
  42. package/lib/translate.test.mjs +23 -2
  43. package/package.json +1 -1
  44. package/scripts/app-screenshots.mjs +7 -7
  45. package/scripts/check-content.mjs +10 -1
  46. package/scripts/check-language.mjs +0 -1
  47. package/scripts/editor-screens.mjs +3 -3
  48. package/site/defineSite.mjs +2 -2
  49. package/site/schema.ts +3 -3
  50. package/src/Advanced.tsx +2 -2
  51. package/src/App.tsx +78 -77
  52. package/src/AppAdds.tsx +1 -1
  53. package/src/AppList.tsx +15 -15
  54. package/src/BlockDialog.tsx +1 -1
  55. package/src/BlockParts.tsx +6 -6
  56. package/src/BlockSettings.tsx +2 -2
  57. package/src/BrandForm.tsx +2 -2
  58. package/src/Colors.tsx +6 -6
  59. package/src/CompanyForm.tsx +1 -1
  60. package/src/Consents.tsx +3 -3
  61. package/src/DataForm.tsx +3 -3
  62. package/src/Deleted.tsx +3 -3
  63. package/src/EditorFooter.tsx +2 -2
  64. package/src/EntryForm.tsx +2 -2
  65. package/src/FooterSettings.tsx +2 -2
  66. package/src/HeaderSettings.tsx +3 -3
  67. package/src/Keywords.tsx +5 -5
  68. package/src/Library.tsx +71 -71
  69. package/src/Licenses.tsx +8 -8
  70. package/src/MainDrawerParts.tsx +3 -3
  71. package/src/MainMenu.tsx +1 -1
  72. package/src/Media.tsx +7 -7
  73. package/src/MediaTags.tsx +2 -2
  74. package/src/Notifications.tsx +2 -2
  75. package/src/Parts.tsx +17 -17
  76. package/src/Redirects.tsx +8 -8
  77. package/src/Screen.tsx +3 -3
  78. package/src/SeoScores.tsx +6 -6
  79. package/src/SideColumn.tsx +2 -2
  80. package/src/StartFab.tsx +2 -2
  81. package/src/ThemeStore.tsx +4 -4
  82. package/src/Tokens.tsx +2 -2
  83. package/src/apps.ts +5 -5
  84. package/src/checkSite.ts +86 -20
  85. package/src/editor.css +4 -16
  86. package/src/examples.ts +1 -1
  87. package/src/fabScreens.test.mjs +4 -4
  88. package/src/fabScreens.ts +4 -4
  89. package/src/i18n/da-DK/library.ts +2 -2
  90. package/src/i18n/da-DK/text.ts +1 -1
  91. package/src/i18n/en-US/apps.ts +1 -1
  92. package/src/i18n/en-US/common.ts +8 -8
  93. package/src/i18n/en-US/library.ts +5 -5
  94. package/src/i18n/en-US/parts.ts +3 -3
  95. package/src/i18n/en-US/text.ts +1 -1
  96. package/src/i18n/index.ts +4 -8
  97. package/src/images.ts +2 -2
  98. package/src/licenseData.ts +3 -3
  99. package/src/menuChildren.ts +5 -5
  100. package/src/movedRoutes.d.mts +8 -1
  101. package/src/movedRoutes.mjs +100 -19
  102. package/src/movedRoutes.test.mjs +123 -6
  103. package/src/nav.ts +1 -1
  104. package/src/notificationData.ts +1 -1
  105. package/src/schema.ts +2 -2
  106. package/src/site.ts +37 -13
  107. package/src/standard/accordion/index.tsx +1 -1
  108. package/src/standard/accordion/shared.ts +1 -1
  109. package/src/standard/buttons/index.tsx +1 -1
  110. package/src/standard/cards/index.tsx +1 -1
  111. package/src/standard/cards/shared.ts +1 -1
  112. package/src/standard/cookies/index.tsx +1 -1
  113. package/src/standard/posts/index.tsx +1 -1
  114. package/src/standard/testimonials/index.tsx +1 -1
  115. package/src/standard/testimonials/list.ts +1 -1
  116. package/src/store.ts +4 -4
  117. package/src/tagUses.d.mts +1 -1
  118. package/src/tagUses.mjs +2 -2
  119. package/src/tagUses.test.mjs +1 -1
  120. package/src/ui.tsx +7 -7
  121. package/theme/layouts/Base.astro +2 -2
@@ -1,4 +1,4 @@
1
- // The main drawer and its menu button on #udseende/dele/editor (Parts.tsx): the modal drawer in its two levels (sections, and
1
+ // The main drawer and its menu button on #appearance/parts/editor (Parts.tsx): the modal drawer in its two levels (sections, and
2
2
  // a section's list with the back row and a fold-out), opened from a button, so it can be tried at any width, and the menu
3
3
  // button open and closed. The drawer's header is a sample user (AccountRow). Exposes MainDrawerSample. Uses MainDrawer and
4
4
  // its parts from ui.tsx.
@@ -53,8 +53,8 @@ export function MainDrawerSample() {
53
53
  {section === null ? (
54
54
  <MainDrawerLevel>
55
55
  <MainDrawerItem href="" icon={icons.home} label={parts.company} />
56
- <MainDrawerSectionItem icon={icons.globalt} label={parts.header} selected onClick={() => setSection('globalt')} />
57
- <MainDrawerSectionItem icon={icons.udseende} label={parts.appearance} onClick={() => setSection('udseende')} />
56
+ <MainDrawerSectionItem icon={icons.global} label={parts.header} selected onClick={() => setSection('global')} />
57
+ <MainDrawerSectionItem icon={icons.appearance} label={parts.appearance} onClick={() => setSection('appearance')} />
58
58
  </MainDrawerLevel>
59
59
  ) : (
60
60
  <MainDrawerLevel focus="list">
package/src/MainMenu.tsx CHANGED
@@ -86,7 +86,7 @@ export function MainMenu({ route, files, user }: { route: string; files: RepoFil
86
86
  onEscape={onEscape}
87
87
  header={
88
88
  shown === null ? (
89
- user && <AccountRow user={user} supporting={config.domain} href="#konto" />
89
+ user && <AccountRow user={user} supporting={config.domain} href="#account" />
90
90
  ) : (
91
91
  <MainDrawerBack label={t.library.back} name={t.common.backTo(t.library.mainMenu)} onClick={() => show(null)} />
92
92
  )
package/src/Media.tsx CHANGED
@@ -220,8 +220,8 @@ function Gallery({
220
220
  // All the files, not only pictures (Filer), with their type.
221
221
  const all = !!used;
222
222
  // In Filer, kept for coming back to it (from a file's page); in a picker, from the start each time.
223
- const [kind, setKind] = useKept<'' | Kind>(all ? 'filer-type' : null, '');
224
- const [query, setQuery] = useKept(all ? 'filer-soeg' : null, '');
223
+ const [kind, setKind] = useKept<'' | Kind>(all ? 'files-type' : null, '');
224
+ const [query, setQuery] = useKept(all ? 'files-search' : null, '');
225
225
  const [sort, setSort] = useState<Sort>(() => {
226
226
  try {
227
227
  const saved = localStorage.getItem(SORT_KEY);
@@ -229,9 +229,9 @@ function Gallery({
229
229
  } catch {}
230
230
  return 'new';
231
231
  });
232
- const [date, setDate] = useKept(all ? 'filer-dato' : null, '');
233
- const [use, setUse] = useKept<'' | 'used' | 'unused'>(all ? 'filer-brug' : null, '');
234
- const [tag, setTag] = useKept(all ? 'filer-tag' : null, '');
232
+ const [date, setDate] = useKept(all ? 'files-date' : null, '');
233
+ const [use, setUse] = useKept<'' | 'used' | 'unused'>(all ? 'files-use' : null, '');
234
+ const [tag, setTag] = useKept(all ? 'files-tag' : null, '');
235
235
 
236
236
  function pickSort(next: Sort) {
237
237
  setSort(next);
@@ -410,7 +410,7 @@ const codeImages = () =>
410
410
  .then((r) => (r.ok ? (r.json() as Promise<string[]>) : null))
411
411
  .catch(() => null));
412
412
 
413
- // open: the address of a file to show picked at once (the route filer/<address>); ignored when no such file exists.
413
+ // open: the address of a file to show picked at once (the route files/<address>); ignored when no such file exists.
414
414
  export default function Media({ files, env, open }: { files: RepoFile[]; env: Env; open?: string }) {
415
415
  // The pictures and the other files.
416
416
  const pictures = [...mediaOf(files), ...filesOf(files)];
@@ -678,7 +678,7 @@ export default function Media({ files, env, open }: { files: RepoFile[]; env: En
678
678
  icon={ClockCounterClockwise}
679
679
  label={t.history.deletedOf(t.history.files)}
680
680
 
681
- onClick={() => (location.hash = 'slettet/filer')}
681
+ onClick={() => (location.hash = 'deleted/files')}
682
682
  />
683
683
  </>
684
684
  }
package/src/MediaTags.tsx CHANGED
@@ -74,11 +74,11 @@ export default function MediaTags({ src, files, env }: { src: string; files: Rep
74
74
  keep="categories"
75
75
  footer={(close) => (
76
76
  <SheetLink
77
- href="#ny/kategorier"
77
+ href="#new/tags"
78
78
  onClick={(e) => {
79
79
  e.preventDefault();
80
80
  close();
81
- leaveTo('#ny/kategorier');
81
+ leaveTo('#new/tags');
82
82
  }}
83
83
  >
84
84
  {t.media.createTag}
@@ -1,4 +1,4 @@
1
- // "Notifikationer" (#notifikationer), which the bell in the top bar opens (Screen.tsx NotificationsLink): the
1
+ // "Notifikationer" (#notifications), which the bell in the top bar opens (Screen.tsx NotificationsLink): the
2
2
  // notifications of the editor and the apps (notificationData.ts), newest first, each with when it came and, when it had
3
3
  // one, a link to what it is about; an app's with the app's icon. Opening it marks them seen; at the top the bin clears
4
4
  // them and the sliders open "Konto", where each type is turned on or off.
@@ -41,7 +41,7 @@ export default function Notifications() {
41
41
  disabled={!items.length}
42
42
  onClick={clearNotifications}
43
43
  />
44
- <IconLink href="#konto" icon={SlidersHorizontal} label={t.library.notificationChoices} />
44
+ <IconLink href="#account" icon={SlidersHorizontal} label={t.library.notificationChoices} />
45
45
  </>
46
46
  }
47
47
  >
package/src/Parts.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // "Udseende" › "Dele" › "Editor" (#udseende/dele/editor): the parts only the editor has (ui.tsx: the SEO rating, the
1
+ // "Udseende" › "Dele" › "Editor" (#appearance/parts/editor): the parts only the editor has (ui.tsx: the SEO rating, the
2
2
  // colour field, the choices of pages and icons, the form's layout …) in every state, on one screen, to see and agree on
3
3
  // how the editor looks, and below them the form fields (fields.tsx), one of each type with the settings it can have.
4
4
  // The parts the theme has too (buttons, fields, switches, lists, sheets …) are the theme's, shown under "UI-komponenter"
@@ -118,7 +118,7 @@ const GROUPS = (['buttons', 'fields', 'choices', 'lists', 'sheets', 'messages',
118
118
  }));
119
119
 
120
120
  export default function Parts({ env }: { env: Env }) {
121
- const [sort, setSort] = useState('nyeste');
121
+ const [sort, setSort] = useState('newest');
122
122
  const [filter, setFilter] = useState('');
123
123
  const [content, setContent] = useState('');
124
124
  const [picked, setPicked] = useState('a');
@@ -155,7 +155,7 @@ export default function Parts({ env }: { env: Env }) {
155
155
  const page = useMemo(() => exampleData().relations.pages[1].value, []);
156
156
 
157
157
  return (
158
- <Screen title={t.parts.screens.editor} back={{ href: '#udseende/dele', label: t.common.parts.editorParts }}>
158
+ <Screen title={t.parts.screens.editor} back={{ href: '#appearance/parts', label: t.common.parts.editorParts }}>
159
159
  <div className="space-y-10">
160
160
  <p className="text-muted">{t.parts.intro('packages/editor/src/ui.tsx')}</p>
161
161
  <PartsShown groups={GROUPS} keep="parts">
@@ -169,7 +169,7 @@ export default function Parts({ env }: { env: Env }) {
169
169
  </Part>
170
170
 
171
171
  <Part name="ArrowLink" use={use.arrowLink}>
172
- <ArrowLink href="#udseende/farver">{sample.editColors}</ArrowLink>
172
+ <ArrowLink href="#appearance/colors">{sample.editColors}</ArrowLink>
173
173
  </Part>
174
174
  </PartGroup>
175
175
 
@@ -199,7 +199,7 @@ export default function Parts({ env }: { env: Env }) {
199
199
  { value: 'top', label: sample.top },
200
200
  { value: 'list', label: sample.inList },
201
201
  ]}
202
- href="#udseende/dele/editor"
202
+ href="#appearance/parts/editor"
203
203
  onChange={setPlacement}
204
204
  />
205
205
  </div>
@@ -273,7 +273,7 @@ export default function Parts({ env }: { env: Env }) {
273
273
  max={3}
274
274
  footer={(close) => (
275
275
  <SheetLink
276
- href="#udseende/dele/editor"
276
+ href="#appearance/parts/editor"
277
277
  onClick={(e) => {
278
278
  e.preventDefault();
279
279
  close();
@@ -327,8 +327,8 @@ export default function Parts({ env }: { env: Env }) {
327
327
  value={sort}
328
328
  disabled={asMenu}
329
329
  options={[
330
- { value: 'nyeste', label: sample.newest },
331
- { value: 'aeldste', label: sample.oldest },
330
+ { value: 'newest', label: sample.newest },
331
+ { value: 'oldest', label: sample.oldest },
332
332
  ]}
333
333
  onChange={setSort}
334
334
  />
@@ -393,10 +393,10 @@ export default function Parts({ env }: { env: Env }) {
393
393
  { icon: FolderSimple, label: sample.media, count: 42 },
394
394
  { icon: PencilSimple, label: sample.pages, count: 12 },
395
395
  ].map((p) => (
396
- <ListItem key={p.label} href="#udseende/dele/editor" icon={p.icon} headline={p.label} count={p.count} arrow />
396
+ <ListItem key={p.label} href="#appearance/parts/editor" icon={p.icon} headline={p.label} count={p.count} arrow />
397
397
  ))}
398
398
  {/* With a text under its name: the icon beside the first two lines. */}
399
- <ListItem href="#udseende/dele/editor" icon={Gear} headline={sample.cookieBanner} supporting={sample.cookieHint} arrow />
399
+ <ListItem href="#appearance/parts/editor" icon={Gear} headline={sample.cookieBanner} supporting={sample.cookieHint} arrow />
400
400
  {/* With a picture before its name (the picture a page's colours came from). */}
401
401
  <ListItem
402
402
  leading={<LeadingImage src={examplePicture} />}
@@ -459,7 +459,7 @@ export default function Parts({ env }: { env: Env }) {
459
459
  <Part name="UnfoldButton, subListClass" use={use.unfold}>
460
460
  <ul className={listClass}>
461
461
  <ListItem
462
- href="#udseende/dele/editor"
462
+ href="#appearance/parts/editor"
463
463
  headline={sample.pages}
464
464
  actions={
465
465
  <span className="shrink-0 pr-1.5">
@@ -477,7 +477,7 @@ export default function Parts({ env }: { env: Env }) {
477
477
  <li className="pb-2">
478
478
  <ul className={subListClass}>
479
479
  {[1, 2, 3].map((n) => (
480
- <ListItem key={n} tone="sub" href="#udseende/dele/editor" headline={sample.subpage(n)} />
480
+ <ListItem key={n} tone="sub" href="#appearance/parts/editor" headline={sample.subpage(n)} />
481
481
  ))}
482
482
  </ul>
483
483
  </li>
@@ -534,8 +534,8 @@ export default function Parts({ env }: { env: Env }) {
534
534
  icon={ArrowsDownUp}
535
535
  value={sort}
536
536
  options={[
537
- { value: 'nyeste', label: sample.newest },
538
- { value: 'aeldste', label: sample.oldest },
537
+ { value: 'newest', label: sample.newest },
538
+ { value: 'oldest', label: sample.oldest },
539
539
  ]}
540
540
  onChange={setSort}
541
541
  />
@@ -572,8 +572,8 @@ export default function Parts({ env }: { env: Env }) {
572
572
  search={t.common.search}
573
573
  value={sort}
574
574
  options={[
575
- { value: 'nyeste', label: sample.newest },
576
- { value: 'aeldste', label: sample.oldest },
575
+ { value: 'newest', label: sample.newest },
576
+ { value: 'oldest', label: sample.oldest },
577
577
  ]}
578
578
  onChange={setSort}
579
579
  />
@@ -811,7 +811,7 @@ function fieldTypes(): FieldType[] {
811
811
  { name: 'phone', label: sample.phone, type: 'string', phone: '+45', digitsInPairs: 8 },
812
812
  { name: 'keyword', label: f.keyword, type: 'string', chips: 'one' },
813
813
  { name: 'keywords', label: f.keywords, type: 'string', chips: 'many' },
814
- { name: 'linkText', label: f.linkText, type: 'string', edit: { to: 'udseende/farver', text: f.editColors } },
814
+ { name: 'linkText', label: f.linkText, type: 'string', edit: { to: 'appearance/colors', text: f.editColors } },
815
815
  ],
816
816
  },
817
817
  {
package/src/Redirects.tsx CHANGED
@@ -1,12 +1,12 @@
1
- // "Videresendelser" under Indhold (#seo/videresendelser): two rows with how many
1
+ // "Videresendelser" under Indhold (#seo/redirects): two rows with how many
2
2
  // each has, each opening a screen of its own.
3
- // "Alle videresendelser" (#seo/videresendelser/alle): every address of a page the site sends on elsewhere
3
+ // "Alle videresendelser" (#seo/redirects/all): every address of a page the site sends on elsewhere
4
4
  // (redirectData.ts), by address, with where it sends to, and search. Tapping one opens a sheet where
5
5
  // where it sends to can be changed, or the redirect deleted; the FAB adds a new one (in the editor's own,
6
6
  // src/data/redirects.json). Each save is one commit, and none sends on through another afterwards
7
7
  // (flatten): a redirect to an address that is sent on goes straight to where that ends, and those
8
8
  // that went on through a deleted one go on to where it sent them.
9
- // "Ikke fundet" (#seo/videresendelser/ikke-fundet): the addresses visitors came to that do not exist (notFound.ts), most visits
9
+ // "Ikke fundet" (#seo/redirects/not-found): the addresses visitors came to that do not exist (notFound.ts), most visits
10
10
  // first. Tapping one opens the same sheet, to send it on to a page (then it is off that list) or take
11
11
  // it off the list. One that has become a page or sends on since is not shown. Over each list, as over
12
12
  // the lists under "Indhold", the search, the order (kept in this browser; by address under the first
@@ -66,15 +66,15 @@ type Editing = { from: string; to: string; isNew: boolean; missing?: Missing };
66
66
  export type Tab = 'redirects' | 'missing';
67
67
 
68
68
  // Where each list opens, under "Videresendelser".
69
- export const TAB_ROUTES: Record<Tab, string> = { redirects: 'seo/videresendelser/alle', missing: 'seo/videresendelser/ikke-fundet' };
69
+ export const TAB_ROUTES: Record<Tab, string> = { redirects: 'seo/redirects/all', missing: 'seo/redirects/not-found' };
70
70
  /** Where "Videresendelser" opens, under "Søgemaskineoptimering". */
71
- export const REDIRECTS_ROUTE = 'seo/videresendelser';
71
+ export const REDIRECTS_ROUTE = 'seo/redirects';
72
72
 
73
73
  // The orders of each list: the redirects by their address or by the page they send to, the addresses
74
74
  // that do not exist by the visits, the last visit or the address. Kept in this browser, each its own.
75
75
  type Sort = 'az' | 'za' | 'page' | 'visits' | 'last';
76
76
  const SORTS: Record<Tab, Sort[]> = { redirects: ['az', 'za', 'page'], missing: ['visits', 'last', 'az', 'za'] };
77
- const sortKey = (tab: Tab) => `editor-sort-${tab === 'redirects' ? 'videresendelser' : 'ikke-fundet'}`;
77
+ const sortKey = (tab: Tab) => `editor-sort-${tab === 'redirects' ? 'redirects' : 'not-found'}`;
78
78
 
79
79
  function savedSort(tab: Tab): Sort {
80
80
  try {
@@ -99,7 +99,7 @@ export default function Redirects({ env, tab }: { env: Env; tab?: Tab }) {
99
99
  }, []);
100
100
  const all = useMemo(() => (files ? readAll(files) : null), [files]);
101
101
  const entries = useMemo(() => (all ? entriesOf(all) : []), [all]);
102
- const [query, setQuery] = useKept('videresendelser-soeg', '');
102
+ const [query, setQuery] = useKept('redirects-search', '');
103
103
  const q = query.trim().toLowerCase();
104
104
  const name = (to: string) => whatIsAt(to, env.relations);
105
105
  // The pages and posts the redirects send to (not a picture or another site), by their names as in the
@@ -255,7 +255,7 @@ export default function Redirects({ env, tab }: { env: Env; tab?: Tab }) {
255
255
  { tab: 'missing' as const, label: t.redirects.missing.title, icon: LinkBreak, count: missing && files ? stillMissing.length : null },
256
256
  ];
257
257
  return (
258
- <Screen title={t.common.parts.redirects} wide back={backFrom('videresendelser')}>
258
+ <Screen title={t.common.parts.redirects} wide back={backFrom('redirects')}>
259
259
  {problem && <ProblemNote>{problem}</ProblemNote>}
260
260
  <Group>
261
261
  {rows.map((r) => (
package/src/Screen.tsx CHANGED
@@ -55,14 +55,14 @@ export function WebsiteLink() {
55
55
  return <IconLink href="/" target="_blank" rel="noopener" icon={ArrowSquareOut} label={t.library.openWebsite} />;
56
56
  }
57
57
 
58
- /** The bell in the top bar, before the link to the website: the notifications (#notifikationer, Notifications.tsx), with a dot
58
+ /** The bell in the top bar, before the link to the website: the notifications (#notifications, Notifications.tsx), with a dot
59
59
  * while some are not seen yet. */
60
60
  export function NotificationsLink({ className = '' }: { className?: string }) {
61
61
  const count = unseen(useNotifications());
62
- const current = location.hash === '#notifikationer';
62
+ const current = location.hash === '#notifications';
63
63
  return (
64
64
  <IconLink
65
- href="#notifikationer"
65
+ href="#notifications"
66
66
  icon={Bell}
67
67
  label={count ? t.library.notificationsNew(count) : t.library.notifications}
68
68
  aria-current={current ? 'page' : undefined}
package/src/SeoScores.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // "SEO-vurdering" under "Søgemaskineoptimering" (#seo/vurdering): every page and blog post with the score of its SEO
1
+ // "SEO-vurdering" under "Søgemaskineoptimering" (#seo/rating): every page and blog post with the score of its SEO
2
2
  // rating (seo.ts, the same as at the top of "Søgemaskiner" on it) in a ring at the start of its row, and its
3
3
  // primary keyword under its name, under "Sider" and "Blogindlæg", the lowest first. Over them, as over the lists, a
4
4
  // search by name and keyword, the order (lowest or highest first, A–Å or Å–A, kept in this browser) and the funnel with
@@ -18,7 +18,7 @@ import { byText, type Collection, type MarkdownCollection } from './schema';
18
18
  import { t } from './i18n';
19
19
 
20
20
  /** Where it opens, under "Søgemaskineoptimering". */
21
- export const SEO_SCORES_ROUTE = 'seo/vurdering';
21
+ export const SEO_SCORES_ROUTE = 'seo/rating';
22
22
 
23
23
  type Sort = 'low' | 'high' | 'az' | 'za';
24
24
  const SORTS: Sort[] = ['low', 'high', 'az', 'za'];
@@ -56,7 +56,7 @@ function rate(collection: MarkdownCollection, file: RepoFile, relations: Env['re
56
56
  export const isRated = (c: Collection): c is MarkdownCollection => c.kind === 'markdown' && c.fields.some((f) => f.name === 'seoTitle');
57
57
 
58
58
  export default function SeoScores({ files, env }: { files: RepoFile[]; env: Env }) {
59
- const [query, setQuery] = useKept('seo-soeg', '');
59
+ const [query, setQuery] = useKept('seo-search', '');
60
60
  const [sort, setSort] = useState(savedSort);
61
61
  function pickSort(next: Sort) {
62
62
  setSort(next);
@@ -74,9 +74,9 @@ export default function SeoScores({ files, env }: { files: RepoFile[]; env: Env
74
74
  );
75
75
  const all = groups.flatMap((g) => g.items);
76
76
  const average = all.length ? Math.round(all.reduce((sum, r) => sum + r.score, 0) / all.length) : null;
77
- const [rating, setRating] = useKept<Rating | ''>('seo-vurdering', '');
78
- const [hasKeyword, setHasKeyword] = useKept<HasKeyword | ''>('seo-soegeord', '');
79
- const [kind, setKind] = useKept('seo-slags', '');
77
+ const [rating, setRating] = useKept<Rating | ''>('seo-rating', '');
78
+ const [hasKeyword, setHasKeyword] = useKept<HasKeyword | ''>('seo-keywords', '');
79
+ const [kind, setKind] = useKept('seo-kind', '');
80
80
  const shownKind = groups.some((g) => g.collection.id === kind) ? kind : '';
81
81
  const kindName = groups.find((g) => g.collection.id === shownKind)?.collection.label ?? '';
82
82
  const filters = [
@@ -1,4 +1,4 @@
1
- // "Udseende" › "Sidekolonne" (#udseende/sidekolonne): the block drawn in a column beside the text of the pages, chosen
1
+ // "Udseende" › "Sidekolonne" (#appearance/side-column): the block drawn in a column beside the text of the pages, chosen
2
2
  // among "Ingen" and the installed blocks that can be (`beside` in their load.ts), saved in the site's file (config.side)
3
3
  // in its own commit. Exposes SideColumn, sideSettingOf and sideChoicesOf (also used by the page form and by Library.tsx).
4
4
  // Uses ui.tsx (ChoiceRow, Group), registry.tsx (siteBlocks) and lib/side-block.mjs.
@@ -24,7 +24,7 @@ const sources: Record<string, string> = Object.fromEntries(
24
24
  );
25
25
 
26
26
  const capital = (s: string) => s.charAt(0).toLocaleUpperCase(locale) + s.slice(1);
27
- const back = { href: '#udseende', label: t.common.parts.appearance };
27
+ const back = { href: '#appearance', label: t.common.parts.appearance };
28
28
 
29
29
  /** The block type the site's file (config.side) chooses, or null. */
30
30
  export function sideSettingOf(files: { path: string; text: string }[]): string | null {
package/src/StartFab.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  // The start screen's FAB menu (Start in Library.tsx): a new page and a new blog post, each opening its empty form
2
- // (#ny/<list>), as the FAB on its list does. Exposes newOnStart (the actions) and StartFab. Uses FabMenu (ui.tsx),
2
+ // (#new/<list>), as the FAB on its list does. Exposes newOnStart (the actions) and StartFab. Uses FabMenu (ui.tsx),
3
3
  // START_FAB (fabScreens.ts) and the collections and icons of site.ts.
4
4
  import { Plus } from '@phosphor-icons/react';
5
5
  import { t } from './i18n';
@@ -11,7 +11,7 @@ import { FabMenu, type FabAction } from './ui';
11
11
  export function newOnStart(): FabAction[] {
12
12
  return START_FAB.flatMap((id) => {
13
13
  const c = collections.find((x) => x.id === id);
14
- return c?.create ? [{ icon: icons[id] ?? Plus, label: c.create, href: `#ny/${id}` }] : [];
14
+ return c?.create ? [{ icon: icons[id] ?? Plus, label: c.create, href: `#new/${id}` }] : [];
15
15
  });
16
16
  }
17
17
 
@@ -1,6 +1,6 @@
1
- // The themes in the app store (store.ts): their tab in "Appstore" (#indstillinger/appstore/temaer, StoreThemes), all themes, those
1
+ // The themes in the app store (store.ts): their tab in "Appstore" (#settings/appstore/themes, StoreThemes), all themes, those
2
2
  // in the store and those installed, with the same search, order and status filter as the apps; the page of one that
3
- // is not installed (#udseende/temaer/<id>, StoreThemePage), drawn from the catalog as a theme's page (its picture,
3
+ // is not installed (#appearance/themes/<id>, StoreThemePage), drawn from the catalog as a theme's page (its picture,
4
4
  // name, version, who made it, "Installer", its pictures of a site in it, what it is and gives the site, and who made
5
5
  // it); and on the page of one installed from the store (ThemePage in Library.tsx) "Opdater til …" and "Afinstaller"
6
6
  // (ThemeStoreButtons). Installing copies its folder into the site's themes (config.themes/<id>/, with its store.json),
@@ -38,7 +38,7 @@ import { siteT, t } from './i18n';
38
38
  import { kindAndName } from './deploy';
39
39
 
40
40
  /** A theme's page (installed, or in the store). */
41
- export const themeHref = (id: string) => `#udseende/temaer/${id}`;
41
+ export const themeHref = (id: string) => `#appearance/themes/${id}`;
42
42
 
43
43
  /** A theme's picture (its theme.json's icon), where a row has its icon. */
44
44
  export function ThemePicture({ src }: { src: string }) {
@@ -270,7 +270,7 @@ export function ThemeRow({ of }: { of: ReturnType<typeof allThemes>[number] }) {
270
270
  );
271
271
  }
272
272
 
273
- /** The page of a theme in the store that is not installed, or installed but not yet built (#udseende/temaer/<id>),
273
+ /** The page of a theme in the store that is not installed, or installed but not yet built (#appearance/themes/<id>),
274
274
  * from the catalog: as a theme's page, with "Installer" (or "Afinstaller"). */
275
275
  export function StoreThemePage({ id, env }: { id: string; env: Env }) {
276
276
  const catalog = useThemeCatalog(env);
package/src/Tokens.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // "Dele" › "Værdier" (#udseende/dele/vaerdier): the theme's design tokens, as the theme in use sets them, to see them in one
1
+ // "Dele" › "Værdier" (#appearance/parts/values): the theme's design tokens, as the theme in use sets them, to see them in one
2
2
  // place: the corners, one row for each size the theme uses (the theme's base, base.css and components.css, which every
3
3
  // theme has, and its UI parts', ui.css: each part's own variable, else the one ui.css gives it instead), named by
4
4
  // Material 3's corner radius scale where the size is one of its steps, with what has it under; the shadows, one row for
@@ -15,7 +15,7 @@ import { Group, LeadingToken, ListItem, type TokenKind } from './ui';
15
15
  import { locale, t } from './i18n';
16
16
 
17
17
  const say = t.parts.tokens;
18
- const back = { href: '#udseende/dele', label: t.common.parts.editorParts };
18
+ const back = { href: '#appearance/parts', label: t.common.parts.editorParts };
19
19
 
20
20
  /** A token: its CSS variable, its name here, and for a part's own the token or the value ui.css gives it when the theme
21
21
  * does not set it (its `var(--chip-radius, var(--block-control-radius))`). */
package/src/apps.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  // asking its own addresses in the Worker (`api`);
26
26
  // screens in index.ts screens of its own under its page (#apps/<app>/<screen>, AppScreenPage), drawn as its page,
27
27
  // which links to them (Cookiescanner's "Blokeret adresse");
28
- // tabs in index.ts tabs it adds to the site's lists (#liste/<list>/<app>, after the list's own tab in the top app
28
+ // tabs in index.ts tabs it adds to the site's lists (#list/<list>/<app>, after the list's own tab in the top app
29
29
  // bar, Library.tsx List), drawn as its page (Cookiescanner's "Problemer" on "Cookies");
30
30
  // notifications in index.ts the kinds of notifications it sends, each turned off under "Konto", and how it finds
31
31
  // them (`check`, with `api`; AppNotifications);
@@ -161,7 +161,7 @@ export type AppDefinition = {
161
161
  * what it does ("nyhedsbrev"). The kind also says where its settings are: under "Indstillinger" on the site's screen
162
162
  * with it (PartScreen.apps in partScreens: "analytics" on "Analytics"), or on a part of "Virksomheden" with it
163
163
  * (Part.apps in companyParts: "payment" on "Betaling"), so they are where they are used and not on its page under
164
- * "Apps", which only installs it; without such a screen, on a screen of their own (#apps/<id>/indstillinger). */
164
+ * "Apps", which only installs it; without such a screen, on a screen of their own (#apps/<id>/settings). */
165
165
  category?: AppCategory;
166
166
  keywords?: string[];
167
167
  /** The categories of cookies (lib/cookieCategories.mjs) of what it loads that sets cookies (a tag), with these
@@ -173,10 +173,10 @@ export type AppDefinition = {
173
173
  * editor's parts (ui.tsx), its texts with siteText. */
174
174
  page?: ComponentType<AppPageProps>;
175
175
  /** Screens of its own under its page while it is installed, by the last part of their address (#apps/<id>/<key>; not
176
- * "indstillinger", the Settings tab's field pages, and not "om", the Information tab: the editor matches those
176
+ * "settings", the Settings tab's field pages, and not "about", the Information tab: the editor matches those
177
177
  * first, so a screen with either key is never shown), each linked to from its page. */
178
178
  screens?: Record<string, AppScreen>;
179
- /** Tabs it adds to the site's lists while it is installed, by the list's id, each at #liste/<list>/<app>. */
179
+ /** Tabs it adds to the site's lists while it is installed, by the list's id, each at #list/<list>/<app>. */
180
180
  tabs?: Record<string, AppTab>;
181
181
  notifications?: AppNotifications;
182
182
  };
@@ -230,7 +230,7 @@ export function withKinds<C extends { id: string; kind: string; fields?: Field[]
230
230
  }
231
231
 
232
232
  /** An app's kind of a list as it is turned off under "Blokindstillinger", among the blocks turned off ("popups/mailchimp",
233
- * as its address under "+", #ny/popups/mailchimp). */
233
+ * as its address under "+", #new/popups/mailchimp). */
234
234
  export const kindKey = (list: string, app: string) => `${list}/${app}`;
235
235
 
236
236
  /** The kinds a new entry of a list can be, under its "+": its own, and those of the apps installed and not turned off
package/src/checkSite.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // The redirects (redirectData.ts): none sends on through another, and none in a loop. And the site's own rules
9
9
  // (siteProblems in its rules.ts: a button that opens a promotion not on its page).
10
10
  // The file records (src/data/files: the tags of files in Filer): each is valid, no tag is both chosen and removed, one per address.
11
- // And the site's texts the editor has no translation of (untranslated).
11
+ // And the site's texts the editor has no translation of, and a converted standard text with no Danish (untranslated).
12
12
  import { check } from './check';
13
13
  import {
14
14
  appearance,
@@ -29,7 +29,7 @@ import { colorProblems, lowContrast, MODES } from '../lib/colors';
29
29
  import { completeColors, dynamicProblems } from '../lib/dynamic-colors';
30
30
  import { APP_HOOKS, hooksOf } from '../lib/hooks';
31
31
  import { texts } from '@site/texts';
32
- import { texts as standardTexts } from '../defaults/texts';
32
+ import { defaultsDaDK } from '../defaults/i18n/da-DK';
33
33
  import type { Part } from './schema';
34
34
  import { apps, asStandardTemplate, blockProblems, siteBlocks, standardPageOf, withPageBlocks } from './registry';
35
35
  import { installedApps, settingsFile } from './apps';
@@ -37,6 +37,14 @@ import { appFolder, SCREENSHOTS, shotFile, shotOf } from './store';
37
37
  import { appFolders } from './appFiles';
38
38
  import { colorPairs as shownPairs, sharedParts, translator, writtenApps, writtenBlockParts, writtenCollections } from './site';
39
39
  import { splitBlocks } from '../lib/blocks.mjs';
40
+ import { splitMissing } from '../lib/translate.mjs';
41
+ import { collections as standardCollections } from '../defaults/collections';
42
+ import { companyParts as standardCompany } from '../defaults/company';
43
+ import { blockParts as standardBlocks } from '../defaults/blockTexts';
44
+ import { colorFields as standardColorFields, colorPairs as standardColorPairs } from '../defaults/colors';
45
+ import { privacy as standardPrivacy } from '../defaults/privacy';
46
+ import { internalLinks as standardInternalLinks } from '../defaults/internalLinks';
47
+ import { appearance as standardAppearance, groups as standardGroups } from '../defaults/editor';
40
48
  import { isRecordPath, recordProblems } from './fileRecords.mjs';
41
49
  import { appTexts, DEFAULTS_LOCALE, locales, siteLocale, t, type Texts } from './i18n';
42
50
  import { flatten, readAll, REDIRECT_FILES, type AllRedirects } from './redirectData';
@@ -212,19 +220,73 @@ function chainsOf(all: AllRedirects, flat: AllRedirects): string[] {
212
220
  return lines;
213
221
  }
214
222
 
223
+ type Definitions = {
224
+ collections: typeof collections;
225
+ parts: Part[];
226
+ screens: typeof partScreens;
227
+ groups: typeof groups;
228
+ colorFields: typeof colorFields;
229
+ colorPairs: typeof colorPairs;
230
+ };
231
+
232
+ /** The definitions the walk goes over: the site's (the standard parts, merged with the site's own) ... */
233
+ const siteDefinitions: Definitions = {
234
+ collections,
235
+ parts: [...companyParts, ...partScreens.flatMap((s) => s.parts), ...appearance, ...seoParts, ...blockParts],
236
+ screens: partScreens,
237
+ groups,
238
+ colorFields,
239
+ colorPairs,
240
+ };
241
+ /** ... and the package's own standard parts, as defaults/ writes them, whatever the site overrides. */
242
+ const standardDefinitions: Definitions = {
243
+ collections: standardCollections,
244
+ parts: [...standardCompany, ...standardPrivacy.parts, ...standardAppearance, standardInternalLinks, ...standardBlocks],
245
+ screens: [standardPrivacy],
246
+ groups: standardGroups,
247
+ colorFields: standardColorFields,
248
+ colorPairs: standardColorPairs,
249
+ };
250
+
251
+ /** Shows each text of the definitions to the translator `w`. */
252
+ function walkDefinitions(w: ReturnType<typeof translator>, d: Definitions) {
253
+ d.collections.forEach(w.collection);
254
+ d.parts.forEach(w.part);
255
+ d.screens.forEach((s) => w.group({ id: s.id, title: s.label, ids: [] }));
256
+ d.groups.forEach(w.group);
257
+ d.colorFields.forEach(w.color);
258
+ d.colorPairs.forEach(w.pair);
259
+ }
260
+
215
261
  /** The site's own texts with no translation, by the editor's other languages, by the file they belong
216
262
  * in: the site's in @site/texts, a block's in its texts.ts (or the site's, for what it takes from
217
263
  * the site). The editor shows them as they are written. Not the ones said in code (the messages in
218
- * rules.ts, the lines on the blocks' cards). */
264
+ * rules.ts, the lines on the blocks' cards). A text of the package's standard parts with no translation in a language
265
+ * (defaults/i18n/da-DK/ for da-DK, a blank value too) is under 'defaults/i18n/da-DK/index.ts': it stops `check:content`. */
219
266
  export function untranslated(): Record<string, Record<string, string[]>> {
220
- // The editor shows a site's definitions translated already on a site in another language than Danish, so a text the walk
221
- // meets is traced back to the one it was translated from (the translations' tables are by it). A text is written in the
222
- // package's language when the package's standard parts or an app have it in Danish (defaults/texts.ts, an app's
223
- // texts.ts), else in the site's; it needs a translation in each other language.
224
- const tables = [standardTexts, ...Object.values(appTexts)].map((table) => Object.entries(table['en-US'] ?? {}));
267
+ // The editor shows a site's definitions translated already on a site in another language than the standard parts', so a text
268
+ // the walk meets is traced back to the one it was translated from (the translations' tables are by it). The standard parts are
269
+ // written in English (the keys of defaults/i18n/da-DK/); the texts of an app are still written in Danish (its texts.ts, until
270
+ // the apps are converted); every other text is written in the site's language. A text needs a translation in each other language.
271
+ const tables = Object.values(appTexts).map((table) => Object.entries(table['en-US'] ?? {}));
225
272
  const danish = new Set(tables.flatMap((entries) => entries.map(([text]) => text)));
226
273
  const original = new Map(tables.flatMap((entries) => entries.map(([text, translated]) => [translated, text] as const)));
227
- const writtenIn = (text: string) => (danish.has(text) ? DEFAULTS_LOCALE : siteLocale);
274
+ // An entry with no Danish (a blank value) is no translation. The walk meets the Danish as it is shown.
275
+ const converted = Object.entries(defaultsDaDK);
276
+ const english = new Set(converted.map(([text]) => text));
277
+ const inDanish = Object.fromEntries(converted.filter(([, danish]) => danish.trim()));
278
+ for (const [text, danish] of Object.entries(inDanish)) original.set(danish, text);
279
+ const writtenIn = (text: string) => (english.has(text) ? DEFAULTS_LOCALE : danish.has(text) ? 'da-DK' : siteLocale);
280
+ // The texts of the package's standard parts: the walk over defaults/'s own definitions, so one with no Danish entry is known too.
281
+ const standardSet = new Set<string>();
282
+ walkDefinitions(
283
+ translator((shown) => {
284
+ standardSet.add(original.get(shown) ?? shown);
285
+ return shown;
286
+ }),
287
+ standardDefinitions,
288
+ );
289
+ const lacking = new Map<string, Set<string>>();
228
290
  const missing = (l: string, own: Record<string, string>, walk: (w: ReturnType<typeof translator>) => void) => {
229
291
  const found = new Set<string>();
230
292
  walk(
@@ -234,22 +296,21 @@ export function untranslated(): Record<string, Record<string, string[]>> {
234
296
  return shown;
235
297
  }),
236
298
  );
237
- return [...found];
299
+ // A text of the package's standard parts stops the check, the others (the site's own, an app's) only warn.
300
+ const split = splitMissing(found, standardSet);
301
+ split.standard.forEach((text) => lacking.set(l, (lacking.get(l) ?? new Set()).add(text)));
302
+ return split.own;
238
303
  };
239
- return Object.fromEntries(
304
+ const gaps = Object.fromEntries(
240
305
  locales
241
306
  .filter((l) => l !== siteLocale || l !== DEFAULTS_LOCALE)
242
307
  .map((l) => {
243
- const site = (texts as Texts)[l] ?? {};
308
+ const site = {
309
+ ...(l === 'da-DK' ? inDanish : {}),
310
+ ...Object.fromEntries(Object.entries((texts as Texts)[l] ?? {}).filter(([, v]) => v.trim())),
311
+ };
244
312
  const files: Record<string, string[]> = {
245
- 'src/site/texts.ts': missing(l, site, (w) => {
246
- collections.forEach(w.collection);
247
- [...companyParts, ...partScreens.flatMap((s) => s.parts), ...appearance, ...seoParts, ...blockParts].forEach(w.part);
248
- partScreens.forEach((s) => w.group({ id: s.id, title: s.label, ids: [] }));
249
- groups.forEach(w.group);
250
- colorFields.forEach(w.color);
251
- colorPairs.forEach(w.pair);
252
- }),
313
+ 'src/site/texts.ts': missing(l, site, (w) => walkDefinitions(w, siteDefinitions)),
253
314
  };
254
315
  // An app's block's own texts, and the fields of its shared texts (shared.ts), in its texts.ts.
255
316
  for (const block of siteBlocks.filter((b) => b.app))
@@ -266,4 +327,9 @@ export function untranslated(): Record<string, Record<string, string[]>> {
266
327
  return [l, Object.fromEntries(Object.entries(files).filter(([, list]) => list.length))];
267
328
  }),
268
329
  );
330
+ // A standard text with no Danish: no entry in defaults/i18n/da-DK/, or a blank one.
331
+ const blank = converted.filter(([, danish]) => !danish.trim()).map(([text]) => text);
332
+ for (const text of blank) lacking.set('da-DK', (lacking.get('da-DK') ?? new Set()).add(text));
333
+ for (const [l, list] of lacking) gaps[l] = { ...gaps[l], 'defaults/i18n/da-DK/index.ts': [...list] };
334
+ return gaps;
269
335
  }