@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.
- package/CHANGELOG.md +11 -0
- package/defaults/blockTexts.ts +9 -9
- package/defaults/collections.ts +257 -257
- package/defaults/colors.ts +74 -74
- package/defaults/company.ts +9 -9
- package/defaults/editor.ts +65 -65
- package/defaults/i18n/da-DK/blockTexts.ts +12 -0
- package/defaults/i18n/da-DK/collections.ts +220 -0
- package/defaults/i18n/da-DK/colors.ts +102 -0
- package/defaults/i18n/da-DK/company.ts +7 -0
- package/defaults/i18n/da-DK/editor.ts +13 -0
- package/defaults/i18n/da-DK/index.ts +31 -0
- package/defaults/i18n/da-DK/internalLinks.ts +12 -0
- package/defaults/i18n/da-DK/opens.ts +7 -0
- package/defaults/i18n/da-DK/popup.ts +9 -0
- package/defaults/i18n/da-DK/privacy.ts +38 -0
- package/defaults/i18n/da-DK/rules.ts +56 -0
- package/defaults/i18n/da-DK/sitedata.ts +8 -0
- package/defaults/i18n/da-DK/standard.ts +8 -0
- package/defaults/i18n/da-DK/standardPages.ts +17 -0
- package/defaults/internalLinks.ts +8 -8
- package/defaults/opens.ts +2 -2
- package/defaults/popup.tsx +7 -7
- package/defaults/privacy.ts +37 -37
- package/defaults/rules.ts +68 -66
- package/defaults/schema.ts +1 -1
- package/defaults/sitedata.ts +4 -4
- package/defaults/standard.ts +9 -6
- package/defaults/standardPages.ts +14 -14
- package/defaults/texts.ts +7 -510
- package/lib/appRoute.d.mts +2 -2
- package/lib/appRoute.mjs +3 -3
- package/lib/appRoute.test.mjs +8 -8
- package/lib/licenses.mjs +2 -2
- package/lib/licenses.owner.test.mjs +9 -0
- package/lib/menu.mjs +4 -4
- package/lib/menu.test.mjs +101 -101
- package/lib/scheduled.mjs +1 -1
- package/lib/storeEntries.test.mjs +3 -0
- package/lib/translate.d.mts +11 -0
- package/lib/translate.mjs +13 -1
- package/lib/translate.test.mjs +23 -2
- package/package.json +1 -1
- package/scripts/app-screenshots.mjs +7 -7
- package/scripts/check-content.mjs +10 -1
- package/scripts/check-language.mjs +0 -1
- package/scripts/editor-screens.mjs +3 -3
- package/site/defineSite.mjs +2 -2
- package/site/schema.ts +3 -3
- package/src/Advanced.tsx +2 -2
- package/src/App.tsx +78 -77
- package/src/AppAdds.tsx +1 -1
- package/src/AppList.tsx +15 -15
- package/src/BlockDialog.tsx +1 -1
- package/src/BlockParts.tsx +6 -6
- package/src/BlockSettings.tsx +2 -2
- package/src/BrandForm.tsx +2 -2
- package/src/Colors.tsx +6 -6
- package/src/CompanyForm.tsx +1 -1
- package/src/Consents.tsx +3 -3
- package/src/DataForm.tsx +3 -3
- package/src/Deleted.tsx +3 -3
- package/src/EditorFooter.tsx +2 -2
- package/src/EntryForm.tsx +2 -2
- package/src/FooterSettings.tsx +2 -2
- package/src/HeaderSettings.tsx +3 -3
- package/src/Keywords.tsx +5 -5
- package/src/Library.tsx +71 -71
- package/src/Licenses.tsx +8 -8
- package/src/MainDrawerParts.tsx +3 -3
- package/src/MainMenu.tsx +1 -1
- package/src/Media.tsx +7 -7
- package/src/MediaTags.tsx +2 -2
- package/src/Notifications.tsx +2 -2
- package/src/Parts.tsx +17 -17
- package/src/Redirects.tsx +8 -8
- package/src/Screen.tsx +3 -3
- package/src/SeoScores.tsx +6 -6
- package/src/SideColumn.tsx +2 -2
- package/src/StartFab.tsx +2 -2
- package/src/ThemeStore.tsx +4 -4
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +5 -5
- package/src/checkSite.ts +86 -20
- package/src/editor.css +4 -16
- package/src/examples.ts +1 -1
- package/src/fabScreens.test.mjs +4 -4
- package/src/fabScreens.ts +4 -4
- package/src/i18n/da-DK/library.ts +2 -2
- package/src/i18n/da-DK/text.ts +1 -1
- package/src/i18n/en-US/apps.ts +1 -1
- package/src/i18n/en-US/common.ts +8 -8
- package/src/i18n/en-US/library.ts +5 -5
- package/src/i18n/en-US/parts.ts +3 -3
- package/src/i18n/en-US/text.ts +1 -1
- package/src/i18n/index.ts +4 -8
- package/src/images.ts +2 -2
- package/src/licenseData.ts +3 -3
- package/src/menuChildren.ts +5 -5
- package/src/movedRoutes.d.mts +8 -1
- package/src/movedRoutes.mjs +100 -19
- package/src/movedRoutes.test.mjs +123 -6
- package/src/nav.ts +1 -1
- package/src/notificationData.ts +1 -1
- package/src/schema.ts +2 -2
- package/src/site.ts +37 -13
- package/src/standard/accordion/index.tsx +1 -1
- package/src/standard/accordion/shared.ts +1 -1
- package/src/standard/buttons/index.tsx +1 -1
- package/src/standard/cards/index.tsx +1 -1
- package/src/standard/cards/shared.ts +1 -1
- package/src/standard/cookies/index.tsx +1 -1
- package/src/standard/posts/index.tsx +1 -1
- package/src/standard/testimonials/index.tsx +1 -1
- package/src/standard/testimonials/list.ts +1 -1
- package/src/store.ts +4 -4
- package/src/tagUses.d.mts +1 -1
- package/src/tagUses.mjs +2 -2
- package/src/tagUses.test.mjs +1 -1
- package/src/ui.tsx +7 -7
- package/theme/layouts/Base.astro +2 -2
package/src/MainDrawerParts.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The main drawer and its menu button on #
|
|
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.
|
|
57
|
-
<MainDrawerSectionItem icon={icons.
|
|
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="#
|
|
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 ? '
|
|
224
|
-
const [query, setQuery] = useKept(all ? '
|
|
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 ? '
|
|
233
|
-
const [use, setUse] = useKept<'' | 'used' | 'unused'>(all ? '
|
|
234
|
-
const [tag, setTag] = useKept(all ? '
|
|
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
|
|
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 = '
|
|
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="#
|
|
77
|
+
href="#new/tags"
|
|
78
78
|
onClick={(e) => {
|
|
79
79
|
e.preventDefault();
|
|
80
80
|
close();
|
|
81
|
-
leaveTo('#
|
|
81
|
+
leaveTo('#new/tags');
|
|
82
82
|
}}
|
|
83
83
|
>
|
|
84
84
|
{t.media.createTag}
|
package/src/Notifications.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Notifikationer" (#
|
|
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="#
|
|
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" (#
|
|
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('
|
|
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: '#
|
|
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="#
|
|
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="#
|
|
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="#
|
|
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: '
|
|
331
|
-
{ value: '
|
|
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="#
|
|
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="#
|
|
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="#
|
|
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="#
|
|
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: '
|
|
538
|
-
{ value: '
|
|
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: '
|
|
576
|
-
{ value: '
|
|
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: '
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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' ? '
|
|
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('
|
|
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('
|
|
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 (#
|
|
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 === '#
|
|
62
|
+
const current = location.hash === '#notifications';
|
|
63
63
|
return (
|
|
64
64
|
<IconLink
|
|
65
|
-
href="#
|
|
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/
|
|
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/
|
|
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-
|
|
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-
|
|
78
|
-
const [hasKeyword, setHasKeyword] = useKept<HasKeyword | ''>('seo-
|
|
79
|
-
const [kind, setKind] = useKept('seo-
|
|
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 = [
|
package/src/SideColumn.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Udseende" › "Sidekolonne" (#
|
|
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: '#
|
|
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
|
-
// (#
|
|
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: `#
|
|
14
|
+
return c?.create ? [{ icon: icons[id] ?? Plus, label: c.create, href: `#new/${id}` }] : [];
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
|
package/src/ThemeStore.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The themes in the app store (store.ts): their tab in "Appstore" (#
|
|
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 (#
|
|
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) => `#
|
|
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 (#
|
|
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" (#
|
|
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: '#
|
|
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 (#
|
|
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>/
|
|
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
|
-
* "
|
|
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 #
|
|
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 "+", #
|
|
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 {
|
|
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
|
|
221
|
-
// meets is traced back to the one it was translated from (the translations' tables are by it).
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
const tables =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
+
const gaps = Object.fromEntries(
|
|
240
305
|
locales
|
|
241
306
|
.filter((l) => l !== siteLocale || l !== DEFAULTS_LOCALE)
|
|
242
307
|
.map((l) => {
|
|
243
|
-
const site =
|
|
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
|
}
|