@goodfellow-cms/admin 0.7.0 → 0.9.0

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/README.md CHANGED
@@ -6,6 +6,8 @@ Its editor is built with each site's own config, so it always has the site's blo
6
6
 
7
7
  [Goodfellow](https://github.com/goodfellow-cms/goodfellow-cms) is a website builder for people who aren't developers, with no server or database: pages are files in your own GitHub, GitLab or Bitbucket repository, edited in a visual editor built on [Puck](https://puckeditor.com). To start a new site, run `npm create goodfellow@latest my-site`. See the [documentation](https://goodfellow-cms.github.io/goodfellow-cms/) for everything else.
8
8
 
9
+ Language packs' own checks read the admin panel's English from `@goodfellow-cms/admin/text`, which draws nothing, as [Goodfellow's French pack](https://github.com/goodfellow-cms/goodfellow-language-fr) does.
10
+
9
11
  More in the docs: [Edit pages](https://goodfellow-cms.github.io/goodfellow-cms/docs/edit-pages).
10
12
 
11
13
  ## License
@@ -0,0 +1,273 @@
1
+ import { n as defaultStrings } from "./strings-D94noZrv.js";
2
+ import { adminTextProblem, englishHash } from "@goodfellow-cms/core";
3
+ //#region src/languages/en-puck.ts
4
+ /**
5
+ * Puck's own English for the text Goodfellow translates, as its built-in
6
+ * dictionary has it (Puck doesn't export it), so the admin panel can have
7
+ * Puck's text translated for a site's own language, and check placeholders
8
+ * against it. A test keeps its keys the same as the Spanish table's.
9
+ */
10
+ const PUCK_ENGLISH = {
11
+ "header-publish": "Publish",
12
+ "header-undo": "undo",
13
+ "header-redo": "redo",
14
+ "header-toggle-leftsidebar": "Toggle left sidebar",
15
+ "header-toggle-rightsidebar": "Toggle right sidebar",
16
+ "header-toggle-menubar": "Toggle menu bar",
17
+ "action-selectparent": "Select parent",
18
+ "action-duplicate": "Duplicate",
19
+ "action-delete": "Delete",
20
+ "label-page": "Page",
21
+ "label-component": "Component",
22
+ "outline-empty": "No items",
23
+ "outline-item-collapse": "Collapse",
24
+ "outline-item-expand": "Expand",
25
+ "outline-header-title": "Outline",
26
+ "outline-header-collapseall": "Collapse all",
27
+ "outline-item-duplicate": "Duplicate",
28
+ "outline-item-delete": "Delete",
29
+ "drawer-category-collapse": "Collapse {title}",
30
+ "drawer-category-expand": "Expand {title}",
31
+ "drawer-category-other": "Other",
32
+ "canvas-noconfig": "No configuration for {type}",
33
+ "field-readonly": "Read-only",
34
+ "field-arrayitem-summary": "Item #{index}",
35
+ "field-arrayitem-duplicate": "Duplicate",
36
+ "field-arrayitem-delete": "Delete",
37
+ "field-external-selectdata": "Select data",
38
+ "field-external-search": "Search",
39
+ "field-external-togglefilters": "Toggle filters",
40
+ "field-external-item": "External item",
41
+ "field-external-result-singular": "{count} result",
42
+ "field-external-result-plural": "{count} results",
43
+ "field-richtext-bold": "Bold",
44
+ "field-richtext-italic": "Italic",
45
+ "field-richtext-underline": "Underline",
46
+ "field-richtext-strikethrough": "Strikethrough",
47
+ "field-richtext-blockquote": "Blockquote",
48
+ "field-richtext-code-inline": "Inline code",
49
+ "field-richtext-code-block": "Code block",
50
+ "field-richtext-list-bullet": "Bullet list",
51
+ "field-richtext-list-ordered": "Ordered list",
52
+ "field-richtext-horizontalrule": "Horizontal rule",
53
+ "field-richtext-align-left": "Align left",
54
+ "field-richtext-align-center": "Align center",
55
+ "field-richtext-align-right": "Align right",
56
+ "field-richtext-align-justify": "Justify",
57
+ "field-richtext-select": "Select",
58
+ "field-richtext-headingselect-1": "Heading 1",
59
+ "field-richtext-headingselect-2": "Heading 2",
60
+ "field-richtext-headingselect-3": "Heading 3",
61
+ "field-richtext-headingselect-4": "Heading 4",
62
+ "field-richtext-headingselect-5": "Heading 5",
63
+ "field-richtext-headingselect-6": "Heading 6",
64
+ "field-richtext-alignselect-left": "Left",
65
+ "field-richtext-alignselect-center": "Center",
66
+ "field-richtext-alignselect-right": "Right",
67
+ "field-richtext-alignselect-justify": "Justify",
68
+ "field-richtext-listselect-bullet": "Bullet list",
69
+ "field-richtext-listselect-ordered": "Ordered list",
70
+ "viewport-zoom-in": "Zoom viewport in",
71
+ "viewport-zoom-out": "Zoom viewport out",
72
+ "viewport-zoom-auto": "{zoom}% (Auto)",
73
+ "viewport-toggle-menu": "Toggle viewport menu",
74
+ "viewport-switch": "Switch to {label} viewport",
75
+ "viewport-switch-default": "Switch viewport",
76
+ "plugin-blocks": "Blocks",
77
+ "plugin-outline": "Outline",
78
+ "plugin-fields": "Fields",
79
+ "plugin-components": "Components",
80
+ "layout-maximize": "maximize",
81
+ "layout-minimize": "minimize",
82
+ "loader-loading": "loading"
83
+ };
84
+ //#endregion
85
+ //#region src/admin-text-sources.ts
86
+ const TABLES = [
87
+ "strings",
88
+ "puck",
89
+ "labels"
90
+ ];
91
+ /** A source's key in one list of every table's: the table, a colon, then its own key, since labels' keys are words. */
92
+ function sourceKey(table, key) {
93
+ return `${table}:${key}`;
94
+ }
95
+ function splitKey(key) {
96
+ const colon = key.indexOf(":");
97
+ const table = key.slice(0, colon);
98
+ return TABLES.includes(table) ? {
99
+ table,
100
+ key: key.slice(colon + 1)
101
+ } : void 0;
102
+ }
103
+ /** Every label a block's fields show, as `withLabels()` puts them in another language: settings, choices and placeholders. */
104
+ function fieldLabels(fields, found) {
105
+ for (const field of Object.values(fields)) {
106
+ const described = field ?? {};
107
+ for (const name of ["label", "placeholder"]) {
108
+ const value = described[name];
109
+ if (typeof value === "string" && value.trim()) found.add(value);
110
+ }
111
+ if (Array.isArray(described.options)) {
112
+ for (const option of described.options) if (typeof option?.label === "string" && option.label.trim()) found.add(option.label);
113
+ }
114
+ for (const nested of ["arrayFields", "objectFields"]) {
115
+ const inner = described[nested];
116
+ if (inner && typeof inner === "object") fieldLabels(inner, found);
117
+ }
118
+ }
119
+ }
120
+ /** The labels of the site's blocks and its page settings, from its description, with any more given (such as groups'). */
121
+ function labelSources(description, more = []) {
122
+ const found = new Set(more);
123
+ for (const block of Object.values(description.blocks)) {
124
+ if (block.label?.trim()) found.add(block.label);
125
+ fieldLabels(block.fields, found);
126
+ }
127
+ fieldLabels(description.root.fields, found);
128
+ return [...found].sort((a, b) => a.localeCompare(b));
129
+ }
130
+ /** Everything the admin panel says in English, from every table, keyed with `sourceKey()`. */
131
+ function adminTextSources(labels) {
132
+ return [
133
+ ...Object.entries(defaultStrings).map(([key, english]) => ({
134
+ key: sourceKey("strings", key),
135
+ english,
136
+ table: "strings"
137
+ })),
138
+ ...Object.entries(PUCK_ENGLISH).map(([key, english]) => ({
139
+ key: sourceKey("puck", key),
140
+ english,
141
+ table: "puck"
142
+ })),
143
+ ...labels.map((english) => ({
144
+ key: sourceKey("labels", english),
145
+ english,
146
+ table: "labels"
147
+ }))
148
+ ];
149
+ }
150
+ /** A string's entry in a language's file, by `sourceKey()`. */
151
+ function entryOf(file, key) {
152
+ const split = splitKey(key);
153
+ if (!file || !split) return void 0;
154
+ const table = file[split.table];
155
+ return Object.hasOwn(table, split.key) ? table[split.key] : void 0;
156
+ }
157
+ /**
158
+ * Where a string stands in a language's file: not there, there but not
159
+ * usable (`adminTextProblem()`), made from English that has changed since,
160
+ * or current. Labels are keyed by their English, so they can't be outdated.
161
+ */
162
+ function sourceState(file, source) {
163
+ const entry = entryOf(file, source.key);
164
+ if (!entry) return "missing";
165
+ if (adminTextProblem(entry.text, source.english)) return "unusable";
166
+ if (source.table !== "labels" && entry.source !== void 0 && entry.source !== englishHash(source.english)) return "outdated";
167
+ return "current";
168
+ }
169
+ /** How much of the admin panel a language's file says. */
170
+ function languageStatus(file, sources) {
171
+ const status = {
172
+ total: sources.length,
173
+ current: 0,
174
+ outdated: 0,
175
+ missing: 0,
176
+ unusable: 0,
177
+ reviewed: 0
178
+ };
179
+ for (const source of sources) {
180
+ const state = sourceState(file, source);
181
+ status[state] += 1;
182
+ if (state === "current" && entryOf(file, source.key)?.reviewed) status.reviewed += 1;
183
+ }
184
+ return status;
185
+ }
186
+ /** The strings to translate: those missing or unusable, and with `outdated`, those made from English that has changed. */
187
+ function sourcesToTranslate(file, sources, options = {}) {
188
+ return sources.filter((source) => {
189
+ const state = sourceState(file, source);
190
+ return state === "missing" || state === "unusable" || options.outdated === true && state === "outdated";
191
+ });
192
+ }
193
+ /**
194
+ * A language's file with new translations in it, by `sourceKey()`, each with
195
+ * the hash of the English it was made from, and not marked reviewed, since
196
+ * no one has read it yet. What the file had besides is kept as it was.
197
+ */
198
+ function withTranslations(file, name, translated, sources) {
199
+ const next = {
200
+ version: 1,
201
+ name: file?.name ?? name,
202
+ strings: { ...file?.strings },
203
+ puck: { ...file?.puck },
204
+ labels: { ...file?.labels },
205
+ ...file?.pack && { pack: file.pack }
206
+ };
207
+ const english = new Map(sources.map((source) => [source.key, source.english]));
208
+ for (const [key, text] of translated) {
209
+ const split = splitKey(key);
210
+ const source = english.get(key);
211
+ if (!split || source === void 0) continue;
212
+ const pack = entryOf(file, key)?.pack;
213
+ next[split.table][split.key] = {
214
+ text,
215
+ source: englishHash(source),
216
+ ...pack && { pack }
217
+ };
218
+ }
219
+ return next;
220
+ }
221
+ /**
222
+ * The strings an editor of a language's file shows: those the filter keeps
223
+ * whose English, key or text in the file holds every word searched for,
224
+ * ignoring case.
225
+ */
226
+ function filterSources(file, sources, options = {}) {
227
+ const words = (options.query ?? "").toLocaleLowerCase().split(/\s+/).filter(Boolean);
228
+ const show = options.show ?? "all";
229
+ return sources.filter((source) => {
230
+ const state = sourceState(file, source);
231
+ const entry = entryOf(file, source.key);
232
+ if (show === "unreviewed" ? state !== "current" || entry?.reviewed : show !== "all" && state !== show) return false;
233
+ const haystack = `${source.key} ${source.english} ${entry?.text ?? ""}`.toLocaleLowerCase();
234
+ return words.every((word) => haystack.includes(word));
235
+ });
236
+ }
237
+ /**
238
+ * A language's file with an owner's edits in it, by `sourceKey()`. Text
239
+ * written or read against today's English is current, so it gets today's
240
+ * English's hash; empty text takes the string out of the file, so it shows
241
+ * the default again.
242
+ */
243
+ function withEdits(file, name, edits, sources) {
244
+ const next = {
245
+ version: 1,
246
+ name: file?.name ?? name,
247
+ strings: { ...file?.strings },
248
+ puck: { ...file?.puck },
249
+ labels: { ...file?.labels },
250
+ ...file?.pack && { pack: file.pack }
251
+ };
252
+ const english = new Map(sources.map((source) => [source.key, source.english]));
253
+ for (const [key, edit] of edits) {
254
+ const split = splitKey(key);
255
+ const source = english.get(key);
256
+ if (!split || source === void 0) continue;
257
+ const text = edit.text.trim();
258
+ if (!text) {
259
+ delete next[split.table][split.key];
260
+ continue;
261
+ }
262
+ const pack = entryOf(file, key)?.pack;
263
+ next[split.table][split.key] = {
264
+ text,
265
+ source: englishHash(source),
266
+ ...edit.reviewed && { reviewed: true },
267
+ ...pack && { pack }
268
+ };
269
+ }
270
+ return next;
271
+ }
272
+ //#endregion
273
+ export { languageStatus as a, sourcesToTranslate as c, PUCK_ENGLISH as d, labelSources as i, withEdits as l, entryOf as n, sourceKey as o, filterSources as r, sourceState as s, adminTextSources as t, withTranslations as u };