@rigel-cms/rigel 1.0.0-rc.1 → 1.1.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.
Files changed (158) hide show
  1. package/README.md +19 -7
  2. package/docs/extensions.md +194 -0
  3. package/package.json +16 -9
  4. package/src/article-body.ts +49 -37
  5. package/src/article-render.ts +25 -2
  6. package/src/article.ts +44 -0
  7. package/src/config.ts +18 -3
  8. package/src/editor/App.svelte +99 -98
  9. package/src/editor/blocks/extension.ts +29 -0
  10. package/src/editor/blocks/index.ts +17 -15
  11. package/src/editor/blocks/video.ts +2 -1
  12. package/src/editor/i18n/README.md +23 -0
  13. package/src/editor/i18n/de/connection.json +136 -0
  14. package/src/editor/i18n/de/defaults.json +6 -0
  15. package/src/editor/i18n/de/fields.json +130 -0
  16. package/src/editor/i18n/de/local.json +49 -0
  17. package/src/editor/i18n/de/media.json +166 -0
  18. package/src/editor/i18n/de/richtext.json +116 -0
  19. package/src/editor/i18n/de/shell.json +181 -0
  20. package/src/editor/i18n/de/system.json +14 -0
  21. package/src/editor/i18n/de/validation.json +82 -0
  22. package/src/editor/i18n/en/connection.json +136 -0
  23. package/src/editor/i18n/en/defaults.json +6 -0
  24. package/src/editor/i18n/en/fields.json +130 -0
  25. package/src/editor/i18n/en/local.json +49 -0
  26. package/src/editor/i18n/en/media.json +166 -0
  27. package/src/editor/i18n/en/richtext.json +116 -0
  28. package/src/editor/i18n/en/shell.json +181 -0
  29. package/src/editor/i18n/en/system.json +14 -0
  30. package/src/editor/i18n/en/validation.json +82 -0
  31. package/src/editor/i18n/es/connection.json +136 -0
  32. package/src/editor/i18n/es/defaults.json +6 -0
  33. package/src/editor/i18n/es/fields.json +130 -0
  34. package/src/editor/i18n/es/local.json +49 -0
  35. package/src/editor/i18n/es/media.json +166 -0
  36. package/src/editor/i18n/es/richtext.json +116 -0
  37. package/src/editor/i18n/es/shell.json +181 -0
  38. package/src/editor/i18n/es/system.json +14 -0
  39. package/src/editor/i18n/es/validation.json +82 -0
  40. package/src/editor/i18n/format.ts +26 -0
  41. package/src/editor/i18n/fr/connection.json +136 -0
  42. package/src/editor/i18n/fr/defaults.json +6 -0
  43. package/src/editor/i18n/fr/fields.json +130 -0
  44. package/src/editor/i18n/fr/local.json +49 -0
  45. package/src/editor/i18n/fr/media.json +166 -0
  46. package/src/editor/i18n/fr/richtext.json +116 -0
  47. package/src/editor/i18n/fr/shell.json +181 -0
  48. package/src/editor/i18n/fr/system.json +14 -0
  49. package/src/editor/i18n/fr/validation.json +82 -0
  50. package/src/editor/i18n/index.ts +18 -0
  51. package/src/editor/i18n/it/connection.json +136 -0
  52. package/src/editor/i18n/it/defaults.json +6 -0
  53. package/src/editor/i18n/it/fields.json +130 -0
  54. package/src/editor/i18n/it/local.json +49 -0
  55. package/src/editor/i18n/it/media.json +166 -0
  56. package/src/editor/i18n/it/richtext.json +116 -0
  57. package/src/editor/i18n/it/shell.json +181 -0
  58. package/src/editor/i18n/it/system.json +14 -0
  59. package/src/editor/i18n/it/validation.json +82 -0
  60. package/src/editor/i18n/ja/connection.json +136 -0
  61. package/src/editor/i18n/ja/defaults.json +6 -0
  62. package/src/editor/i18n/ja/fields.json +130 -0
  63. package/src/editor/i18n/ja/local.json +49 -0
  64. package/src/editor/i18n/ja/media.json +166 -0
  65. package/src/editor/i18n/ja/richtext.json +116 -0
  66. package/src/editor/i18n/ja/shell.json +181 -0
  67. package/src/editor/i18n/ja/system.json +14 -0
  68. package/src/editor/i18n/ja/validation.json +82 -0
  69. package/src/editor/i18n/locales.ts +13 -0
  70. package/src/editor/i18n/nl/connection.json +136 -0
  71. package/src/editor/i18n/nl/defaults.json +6 -0
  72. package/src/editor/i18n/nl/fields.json +130 -0
  73. package/src/editor/i18n/nl/local.json +49 -0
  74. package/src/editor/i18n/nl/media.json +166 -0
  75. package/src/editor/i18n/nl/richtext.json +116 -0
  76. package/src/editor/i18n/nl/shell.json +181 -0
  77. package/src/editor/i18n/nl/system.json +14 -0
  78. package/src/editor/i18n/nl/validation.json +82 -0
  79. package/src/editor/i18n/ru/connection.json +138 -0
  80. package/src/editor/i18n/ru/defaults.json +6 -0
  81. package/src/editor/i18n/ru/fields.json +132 -0
  82. package/src/editor/i18n/ru/local.json +49 -0
  83. package/src/editor/i18n/ru/media.json +166 -0
  84. package/src/editor/i18n/ru/richtext.json +116 -0
  85. package/src/editor/i18n/ru/shell.json +181 -0
  86. package/src/editor/i18n/ru/system.json +14 -0
  87. package/src/editor/i18n/ru/validation.json +82 -0
  88. package/src/editor/i18n/server.ts +13 -0
  89. package/src/editor/i18n/zh/connection.json +136 -0
  90. package/src/editor/i18n/zh/defaults.json +6 -0
  91. package/src/editor/i18n/zh/fields.json +130 -0
  92. package/src/editor/i18n/zh/local.json +49 -0
  93. package/src/editor/i18n/zh/media.json +166 -0
  94. package/src/editor/i18n/zh/richtext.json +116 -0
  95. package/src/editor/i18n/zh/shell.json +181 -0
  96. package/src/editor/i18n/zh/system.json +14 -0
  97. package/src/editor/i18n/zh/validation.json +82 -0
  98. package/src/editor/lib/ActionDialog.svelte +5 -4
  99. package/src/editor/lib/BlockEditor.svelte +305 -88
  100. package/src/editor/lib/CollectionPanel.svelte +17 -15
  101. package/src/editor/lib/DatePicker.svelte +15 -12
  102. package/src/editor/lib/ExtensionMount.svelte +12 -0
  103. package/src/editor/lib/Field.svelte +3 -2
  104. package/src/editor/lib/ImagePicker.svelte +82 -81
  105. package/src/editor/lib/MarkdownEditor.svelte +25 -24
  106. package/src/editor/lib/MediaPicker.svelte +6 -5
  107. package/src/editor/lib/MenuEditor.svelte +31 -30
  108. package/src/editor/lib/PreviewFrame.svelte +11 -6
  109. package/src/editor/lib/PropertiesTabs.svelte +3 -2
  110. package/src/editor/lib/PublishDialog.svelte +15 -14
  111. package/src/editor/lib/ReferenceSelect.svelte +3 -2
  112. package/src/editor/lib/SchemaField.svelte +15 -13
  113. package/src/editor/lib/SchemaPanel.svelte +20 -19
  114. package/src/editor/lib/Select.svelte +3 -2
  115. package/src/editor/lib/SeoGuidance.svelte +6 -5
  116. package/src/editor/lib/Settings.svelte +54 -54
  117. package/src/editor/lib/Shortcuts.svelte +7 -6
  118. package/src/editor/lib/SitePreview.svelte +10 -9
  119. package/src/editor/lib/TopBar.svelte +18 -17
  120. package/src/editor/lib/UnsavedChanges.svelte +7 -6
  121. package/src/editor/lib/VisualEditor.svelte +14 -13
  122. package/src/editor/lib/calendar.ts +10 -4
  123. package/src/editor/lib/embed-preview.ts +23 -5
  124. package/src/editor/lib/extension-preview.ts +121 -0
  125. package/src/editor/lib/github-auth.ts +23 -22
  126. package/src/editor/lib/github-backend.ts +96 -91
  127. package/src/editor/lib/icons.ts +4 -0
  128. package/src/editor/lib/images.ts +33 -31
  129. package/src/editor/lib/menu-tree.ts +5 -4
  130. package/src/editor/lib/preview-frame.ts +4 -3
  131. package/src/editor/lib/schema-edit.ts +3 -2
  132. package/src/editor/lib/settings.ts +5 -3
  133. package/src/editor/lib/slash-command.ts +1 -0
  134. package/src/editor/lib/tooltip.ts +1 -1
  135. package/src/editor/main.ts +9 -0
  136. package/src/editor/styles.css +6 -3
  137. package/src/env.d.ts +11 -1
  138. package/src/extensions.ts +160 -0
  139. package/src/manifest/collections.ts +6 -5
  140. package/src/manifest/menu.ts +12 -10
  141. package/src/manifest/navigation.ts +9 -8
  142. package/src/manifest/references.ts +8 -7
  143. package/src/manifest/seo.ts +4 -3
  144. package/src/manifest/types.ts +1 -0
  145. package/src/preview-urls.ts +6 -0
  146. package/src/preview.ts +38 -7
  147. package/src/routes/local-api.ts +5 -3
  148. package/src/routes/local-documents.ts +20 -16
  149. package/src/routes/local-media.ts +21 -16
  150. package/src/routes/local-menu.ts +9 -7
  151. package/src/routes/local-post.ts +18 -12
  152. package/src/routes/media-files.ts +10 -8
  153. package/src/routes/rigel-page.astro +2 -1
  154. package/src/schema/store.ts +22 -20
  155. package/src/schema/types.ts +1 -0
  156. package/src/schema/validate.ts +73 -69
  157. package/src/slugs.ts +12 -11
  158. package/src/vite/manifest-plugin.ts +33 -1
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { t, locale } from "./i18n";
2
3
  import { onMount, tick } from "svelte";
3
4
  import { tooltipRoot } from "./lib/tooltip";
4
5
  onMount(() => tooltipRoot(document.body).destroy);
@@ -33,11 +34,11 @@
33
34
  import PublishDialog from "./lib/PublishDialog.svelte";
34
35
  import VisualEditor from "./lib/VisualEditor.svelte";
35
36
  import { newDocumentFromTemplate } from "../schema/paths";
36
- import { previewUrl } from "../preview";
37
+ import { previewUrl, previewPath } from "../preview";
37
38
  import type { ContentDocument, Schema } from "../schema/types";
38
39
  import { buildSidebar, navigationKey, type Selection } from "../manifest/navigation";
39
40
  import { startGitHubLogin, checkGitHubAccess, type GitHubAccess } from "./lib/github-auth";
40
- import { loadManifestRemote, saveRemoteSelection, deleteRemotePost, reviewRemotePublish, confirmRemotePublish, type GitContext, type PublishReview } from "./lib/github-backend";
41
+ import { loadManifestRemote, saveRemoteSelection, deleteRemotePost, reviewRemotePublish, confirmRemotePublish, PublishReviewChangedError, type GitContext, type PublishReview } from "./lib/github-backend";
41
42
 
42
43
  type Sel = Selection;
43
44
 
@@ -79,9 +80,9 @@
79
80
  }
80
81
 
81
82
  function publishGuard(context: GitContext, revision: number, auth: number) {
82
- if (!publishOpen || revision !== publishRevision || auth !== authRevision || gitContext !== context) throw new Error("Publish review is no longer active. Reconnect and review again.");
83
+ if (!publishOpen || revision !== publishRevision || auth !== authRevision || gitContext !== context) throw new Error($t("shell.publishReviewInactive"));
83
84
  requireGitContext();
84
- if (dirty || saveTask || saveState === "saving") throw new Error("Save your unsaved edits first, then review publishing. Your edits are still here.");
85
+ if (dirty || saveTask || saveState === "saving") throw new Error($t("shell.saveBeforeReview"));
85
86
  }
86
87
 
87
88
  async function reviewPublish() {
@@ -101,7 +102,7 @@
101
102
  publishReview = review;
102
103
  } catch (error) {
103
104
  handleAuthError(error);
104
- if (revision === publishRevision) publishError = error instanceof Error ? error.message : "Could not review saved changes.";
105
+ if (revision === publishRevision) publishError = error instanceof Error ? error.message : $t("shell.reviewError");
105
106
  } finally { publishLoading = false; }
106
107
  }
107
108
 
@@ -122,13 +123,13 @@
122
123
  guard();
123
124
  manifest = remote;
124
125
  openSelection(sel);
125
- publishMessage = "Drafts synced. Nothing was published. Close this dialog and review again before publishing.";
126
- } else publishMessage = `Published to ${review.publishBranch}. Your configured deployment can now start; the site is not instantly live. The drafts branch is preserved.`;
126
+ publishMessage = $t("shell.draftsSynced");
127
+ } else publishMessage = $t("shell.publishedToBranch", { name: review.publishBranch });
127
128
  } catch (error) {
128
129
  handleAuthError(error);
129
130
  if (revision === publishRevision) {
130
- publishError = error instanceof Error ? error.message : "Could not verify publishing. Check GitHub before retrying.";
131
- if (/changed|no longer matches/.test(publishError)) publishReview = null;
131
+ publishError = error instanceof Error ? error.message : $t("shell.publishVerificationError");
132
+ if (error instanceof PublishReviewChangedError) publishReview = null;
132
133
  }
133
134
  } finally { publishBusy = false; }
134
135
  }
@@ -145,9 +146,9 @@
145
146
  ready = false;
146
147
  try {
147
148
  const response = await fetch("/rigel/manifest.json", { cache: "no-store" });
148
- if (!response.ok) throw new Error("Could not load the editor configuration. Rebuild the site and try again.");
149
+ if (!response.ok) throw new Error($t("shell.configurationLoadError"));
149
150
  const data: Manifest = await response.json();
150
- if (!Array.isArray(data.pages) || (data.backend === "github" && !data.github)) throw new Error("The editor configuration is incomplete. Rebuild the site and try again.");
151
+ if (!Array.isArray(data.pages) || (data.backend === "github" && !data.github)) throw new Error($t("shell.configurationIncomplete"));
151
152
  blueprint = data;
152
153
  manifest = data;
153
154
  ready = true;
@@ -160,16 +161,16 @@
160
161
  }
161
162
  }
162
163
  } catch (error) {
163
- loadError = error instanceof Error ? error.message : "Could not load the editor configuration.";
164
+ loadError = error instanceof Error ? error.message : $t("shell.configurationLoadFailed");
164
165
  }
165
166
  }
166
167
 
167
168
  function requireGitContext() {
168
- if (connectionBusy) throw new Error("Wait for GitHub access verification before saving.");
169
- if (!gitContext || !connected || githubAccess?.status !== "ready") throw new Error("Connect GitHub and verify repository access before saving.");
169
+ if (connectionBusy) throw new Error($t("shell.waitForGithubVerification"));
170
+ if (!gitContext || !connected || githubAccess?.status !== "ready") throw new Error($t("shell.connectBeforeSaving"));
170
171
  if (Date.now() >= sessionExpiresAt - 30_000) {
171
172
  invalidateSession();
172
- throw new Error("Your GitHub session expired. Reconnect GitHub, then retry. Your unsaved edits are still here.");
173
+ throw new Error($t("shell.sessionExpiredRetry"));
173
174
  }
174
175
  return gitContext;
175
176
  }
@@ -181,7 +182,7 @@
181
182
  try {
182
183
  if (Date.now() >= sessionExpiresAt - 30_000) {
183
184
  invalidateSession();
184
- throw new Error("Your GitHub session expired. Sign in again to check repository access.");
185
+ throw new Error($t("shell.sessionExpiredCheckAccess"));
185
186
  }
186
187
  const access = await checkGitHubAccess(context.token, config.repo, config.appSlug);
187
188
  if (gitContext !== context) return;
@@ -228,7 +229,7 @@
228
229
  writeSession(manifest.github, session);
229
230
  await verifyGitHub();
230
231
  } catch (error) {
231
- connectionError = error instanceof Error ? error.message : "Could not connect GitHub. Try signing in again.";
232
+ connectionError = error instanceof Error ? error.message : $t("shell.githubConnectionError");
232
233
  } finally {
233
234
  connectionBusy = false;
234
235
  }
@@ -239,7 +240,7 @@
239
240
  connectionBusy = true;
240
241
  connectionError = "";
241
242
  try { await verifyGitHub(); }
242
- catch (error) { connectionError = error instanceof Error ? error.message : "Could not check GitHub access. Try again."; }
243
+ catch (error) { connectionError = error instanceof Error ? error.message : $t("shell.githubAccessError"); }
243
244
  finally { connectionBusy = false; }
244
245
  }
245
246
 
@@ -292,19 +293,19 @@
292
293
  const creatingDoc = docSel !== null && newDocument?.id === docSel.id;
293
294
  try {
294
295
  if ((selection?.kind === "post" || (docSel && !activeDocType?.shared)) && Object.hasOwn(values, "slug")) {
295
- values.slug = normalizeUrlSlug(values.slug);
296
+ values.slug = normalizeUrlSlug(values.slug, $t);
296
297
  draft.slug = values.slug;
297
298
  }
298
299
  if (githubMode) {
299
- if (!selection) throw new Error("Select content before saving.");
300
+ if (!selection) throw new Error($t("shell.selectBeforeSaving"));
300
301
  await saveRemoteSelection(requireGitContext(), manifest, selection, values, body, !!creating || creatingDoc);
301
302
  } else {
302
303
  const url = docSel
303
304
  ? `/rigel/api/local/documents/${encodeURIComponent(docSel.id)}`
304
305
  : m ? `/rigel/api/local/menus/${m.slug}` : po && c ? `/rigel/api/local/${c.name}/${encodeURIComponent(po.slug)}` : "/rigel/api/local/save";
305
306
  const payload = docSel ? { data: values } : m ? { slug: m.slug, title: values.title, items: values.items } : po ? { frontmatter: values, body } : { slug: p?.slug, values };
306
- const res = await fetch(url, { method: docSel ? (creatingDoc ? "POST" : "PUT") : m ? "PUT" : "POST", headers: { "Content-Type": "application/json", ...(po && creating ? { "If-None-Match": "*" } : {}) }, body: JSON.stringify(payload) });
307
- if (!res.ok) throw new Error((await res.text()) || `Save failed (${res.status})`);
307
+ const res = await fetch(url, { method: docSel ? (creatingDoc ? "POST" : "PUT") : m ? "PUT" : "POST", headers: { "Content-Type": "application/json", "Accept-Language": $locale, ...(po && creating ? { "If-None-Match": "*" } : {}) }, body: JSON.stringify(payload) });
308
+ if (!res.ok) throw new Error((await res.text()) || $t("shell.saveFailedStatus", { status: res.status }));
308
309
  }
309
310
  if (docSel) {
310
311
  const list = manifest.documents ?? (manifest.documents = []);
@@ -324,7 +325,7 @@
324
325
  } catch (error) {
325
326
  handleAuthError(error);
326
327
  saveState = "error";
327
- saveError = error instanceof Error ? error.message : "Could not save changes";
328
+ saveError = error instanceof Error ? error.message : $t("shell.saveChangesError");
328
329
  return false;
329
330
  }
330
331
  }
@@ -338,10 +339,10 @@
338
339
 
339
340
  let settings = $state(getSettings());
340
341
  let settingsError = $state("");
341
- const userName = $derived(githubMode ? githubAccess?.user.name ?? githubAccess?.user.login ?? "GitHub user" : "Local user");
342
+ const userName = $derived(githubMode ? githubAccess?.user.name ?? githubAccess?.user.login ?? $t("shell.githubUser") : $t("shell.localUser"));
342
343
  const userAvatar = $derived(githubMode && /^https:\/\/avatars\.githubusercontent\.com\//.test(githubAccess?.user.avatar_url ?? "") ? githubAccess!.user.avatar_url : "");
343
344
  let failedAvatar = $state("");
344
- const userLogin = $derived(githubMode && githubAccess ? `@${githubAccess.user.login}` : "Local mode");
345
+ const userLogin = $derived(githubMode && githubAccess ? `@${githubAccess.user.login}` : $t("shell.localMode"));
345
346
  let content: HTMLDivElement;
346
347
  let contentWidth = $state(0);
347
348
  let resizing = $state(false);
@@ -390,10 +391,10 @@
390
391
 
391
392
  function eraseBrowserData() {
392
393
  actionDialog = {
393
- title: "Erase browser data?", confirmLabel: "Erase data", danger: true,
394
- description: "This removes Rigel settings, API keys, GitHub login and unsaved drafts from this browser. Saved repository files will not change. The editor will reload.",
394
+ title: $t("shell.eraseBrowserDataTitle"), confirmLabel: $t("shell.eraseData"), danger: true,
395
+ description: $t("shell.eraseBrowserDataDescription"),
395
396
  onconfirm: () => {
396
- if (saveState === "saving") throw new Error("Wait for the current save to finish.");
397
+ if (saveState === "saving") throw new Error($t("shell.waitForSave"));
397
398
  eraseData();
398
399
  dirty = false;
399
400
  location.reload();
@@ -439,12 +440,12 @@
439
440
  });
440
441
 
441
442
  const columns = $derived.by((): { key: string; label: string; image?: boolean }[] => {
442
- if (sel?.kind === "menus") return [{ key: "title", label: "Name" }, { key: "slug", label: "Slug" }];
443
- if (sel?.kind === "pages") return [{ key: "label", label: "Title" }, { key: "slug", label: "Slug" }, { key: "category", label: "Category" }];
443
+ if (sel?.kind === "menus") return [{ key: "title", label: $t("shell.name") }, { key: "slug", label: $t("shell.slug") }];
444
+ if (sel?.kind === "pages") return [{ key: "label", label: $t("shell.title") }, { key: "slug", label: $t("shell.slug") }, { key: "category", label: $t("shell.category") }];
444
445
  const source = sel?.kind === "documents" ? activeDocType : coll;
445
446
  if (!source) return [];
446
447
  const keys = ["title", "slug", ...(source.columns ?? (sel?.kind === "documents" ? [] : source.fields.filter(field => !isSeoField(field.key)).map(field => field.key)))];
447
- const titleLabel = sel?.kind === "documents" && !source.fields.some(field => field.key === "title") ? source.fields.find(field => field.key === "name")?.label ?? "Title" : "Title";
448
+ const titleLabel = sel?.kind === "documents" && !source.fields.some(field => field.key === "title") ? source.fields.find(field => field.key === "name")?.label ?? $t("shell.title") : $t("shell.title");
448
449
  const seen = new Set<string>();
449
450
  const out: { key: string; label: string; image?: boolean }[] = [];
450
451
 
@@ -452,7 +453,7 @@
452
453
  if (seen.has(k)) continue;
453
454
  seen.add(k);
454
455
  const f = source.fields.find((x) => x.key === k);
455
- out.push({ key: k, label: k === "slug" ? "Slug" : k === "title" ? titleLabel : f?.label ?? k, image: f?.type === "image" });
456
+ out.push({ key: k, label: k === "slug" ? $t("shell.slug") : k === "title" ? titleLabel : f?.labelKey ? $t(f.labelKey) : f?.label ?? k, image: f?.type === "image" });
456
457
  }
457
458
  return sel?.kind === "documents" ? [...out.filter(column => column.image), ...out.filter(column => !column.image && column.key !== "slug"), ...out.filter(column => column.key === "slug")] : out;
458
459
  });
@@ -468,7 +469,7 @@
468
469
  });
469
470
  });
470
471
 
471
- const sidebar = $derived(buildSidebar(manifest));
472
+ const sidebar = $derived(buildSidebar(manifest, $t));
472
473
  const pageCategory = $derived(sel?.kind === "pages" ? sel.category : undefined);
473
474
  const schema = $derived<Schema | undefined>(manifest.schema);
474
475
  const docTypes = $derived(schema?.documents ?? []);
@@ -515,7 +516,7 @@
515
516
 
516
517
  const menu = $derived.by(() => { const slug = sel?.kind === "menu" ? sel.slug : null; return manifest.menus?.entries.find((m) => m.slug === slug); });
517
518
  const sortedMenus = $derived([...(manifest.menus?.entries ?? [])].sort((a, b) => (sortBy === "slug" ? a.slug.localeCompare(b.slug) : a.title.localeCompare(b.title)) * sortDir));
518
- const listTitle = $derived(sel?.kind === "menus" ? "All Menus" : sel?.kind === "documents" ? activeDocType?.title ?? "Documents" : sel?.kind === "pages" ? `All ${pageCategory ?? "pages"}` : coll?.listTitle?.trim() || `All ${coll?.label ?? "entries"}`);
519
+ const listTitle = $derived(sel?.kind === "menus" ? $t("shell.allMenus") : sel?.kind === "documents" ? activeDocType?.title ?? $t("shell.documents") : sel?.kind === "pages" ? pageCategory !== undefined ? $t("shell.allNamedEntries", { name: pageCategory }) : $t("shell.allPages") : coll?.listTitle?.trim() || (coll?.label !== undefined ? $t("shell.allNamedEntries", { name: coll.label }) : $t("shell.allEntries")));
519
520
  const listRows = $derived(sel?.kind === "menus"
520
521
  ? sortedMenus.map((m) => ({ slug: m.slug, cells: { slug: m.slug, title: m.title } as Record<string, string>, draft: false, target: { kind: "menu", slug: m.slug } as Sel }))
521
522
  : sel?.kind === "documents"
@@ -538,7 +539,7 @@
538
539
  setSettings(settings);
539
540
  settingsError = "";
540
541
  } catch {
541
- settingsError = "Settings could not be saved in this browser. Allow browser storage and try again.";
542
+ settingsError = $t("shell.settingsStorageError");
542
543
  }
543
544
  });
544
545
 
@@ -682,7 +683,7 @@
682
683
  function cellValue(p: { slug: string; frontmatter: Record<string, unknown> }, key: string): string {
683
684
  const v = key === "slug" ? effectiveSlug(p.frontmatter, p.slug) : p.frontmatter[key];
684
685
  const field = coll?.fields.find(field => field.key === key);
685
- if (field?.type === "reference") return referenceLabel(v, referenceOptions(field, manifest.documents)) || "—";
686
+ if (field?.type === "reference") return referenceLabel(v, referenceOptions(field, manifest.documents), $t) || "—";
686
687
  if (v === true) return "✓";
687
688
  if (v === false || v == null) return "—";
688
689
  if (Array.isArray(v)) return v.join(", ");
@@ -692,12 +693,12 @@
692
693
  function newPage(type = pageDocType) {
693
694
  if (!type || !schema || type.creatable === false || type.singleton) return;
694
695
  actionDialog = {
695
- title: `Add ${type.title}`, description: "Enter a name to start a draft from the template. Nothing is written until you press Save.", fieldLabel: type.shared ? "Name" : "Page name", confirmLabel: "Create draft",
696
+ title: $t("shell.addDocument", { name: type.title }), description: $t("shell.newDocumentDescription"), fieldLabel: type.shared ? $t("shell.name") : $t("shell.pageName"), confirmLabel: $t("shell.createDraft"),
696
697
  onconfirm: (title) => {
697
698
  const slug = slugify(title);
698
- if (!slug) throw new Error("Enter a title containing letters or numbers.");
699
+ if (!slug) throw new Error($t("shell.titleRequired"));
699
700
  const taken = [...(manifest.documents ?? []).filter(doc => !type.shared || doc.type === type.name).flatMap((doc) => [doc.slug, ...(type.shared || docTypes.find(type => type.name === doc.type)?.shared ? [] : [effectiveSlug(doc.data, doc.slug)])]), ...(!type.shared ? manifest.pages.map((page) => page.slug) : [])];
700
- if (taken.includes(slug)) throw new Error(`The slug "${slug}" is already used by another document or page.`);
701
+ if (taken.includes(slug)) throw new Error($t("shell.slugAlreadyUsed", { slug }));
701
702
  const id = `${type.name}:${slug}`;
702
703
  const data = newDocumentFromTemplate(schema, type.name, title, slug);
703
704
  newDocument = { type: type.name, id, slug };
@@ -710,10 +711,10 @@
710
711
 
711
712
  function newPost(c: ManifestCollection) {
712
713
  actionDialog = {
713
- title: `Add to ${c.label}`, description: "Enter a title to start a draft. Nothing is written until you press Save.", fieldLabel: "Title", confirmLabel: "Create draft",
714
+ title: $t("shell.addToCollection", { name: c.label }), description: $t("shell.newPostDescription"), fieldLabel: $t("shell.title"), confirmLabel: $t("shell.createDraft"),
714
715
  onconfirm: (title) => {
715
716
  const slug = slugify(title, c.posts.flatMap((post) => [post.slug, effectiveSlug(post.frontmatter, post.slug)]));
716
- if (!slug) throw new Error("Enter a title containing letters or numbers.");
717
+ if (!slug) throw new Error($t("shell.titleRequired"));
717
718
  newEntry = { collection: c.name, post: { slug, frontmatter: { ...initialCollectionValues(c.fields, title), slug }, body: emptyBody() } };
718
719
  openSelection({ kind: "post", collection: c.name, slug });
719
720
  },
@@ -725,8 +726,8 @@
725
726
  const current = post, collection = coll, unsaved = newEntry !== null;
726
727
  if (!current || !collection) return;
727
728
  actionDialog = {
728
- title: `Delete “${String(current.frontmatter.title ?? current.slug)}”?`, confirmLabel: "Delete", danger: true,
729
- description: unsaved ? "This discards the new post and its unsaved changes." : `This deletes ${current.slug}.json${githubMode ? ` from ${manifest.github?.branch}` : ""} and discards any unsaved changes. This cannot be undone in the editor.`,
729
+ title: $t("shell.deletePostTitle", { name: String(current.frontmatter.title ?? current.slug) }), confirmLabel: $t("shell.delete"), danger: true,
730
+ description: unsaved ? $t("shell.discardNewPostDescription") : githubMode ? $t("shell.deleteRemotePostDescription", { slug: current.slug, branch: String(manifest.github?.branch) }) : $t("shell.deleteLocalPostDescription", { slug: current.slug }),
730
731
  onconfirm: async () => {
731
732
  if (!unsaved) {
732
733
  if (githubMode) {
@@ -734,8 +735,8 @@
734
735
  catch (error) { handleAuthError(error); throw error; }
735
736
  }
736
737
  else {
737
- const res = await fetch(`/rigel/api/local/${collection.name}/${encodeURIComponent(current.slug)}`, { method: "DELETE" });
738
- if (!res.ok) throw new Error((await res.text()) || `Delete failed (${res.status})`);
738
+ const res = await fetch(`/rigel/api/local/${collection.name}/${encodeURIComponent(current.slug)}`, { method: "DELETE", headers: { "Accept-Language": $locale } });
739
+ if (!res.ok) throw new Error((await res.text()) || $t("shell.deleteFailedStatus", { status: res.status }));
739
740
  }
740
741
  collection.posts = collection.posts.filter((p) => p.slug !== current.slug);
741
742
  }
@@ -762,7 +763,7 @@
762
763
  onstorage={(event) => {
763
764
  if ((event.key === "rigelcms.session" || event.key === null) && blueprint?.github && !readSession(blueprint.github)) {
764
765
  invalidateSession();
765
- connectionError = "Signed out in another tab. Reconnect GitHub to save. Your unsaved edits are still here.";
766
+ connectionError = $t("shell.signedOutAnotherTab");
766
767
  }
767
768
  }}
768
769
  onresize={endPreviewResize}
@@ -784,31 +785,31 @@
784
785
  <div class="rigel connection-shell">
785
786
  <main id="github-connection" class="connection-card" aria-busy={connectionBusy}>
786
787
  <div class="connection-brand"><img src={logo} alt="" width="64" height="64" /><span>Rigel <span class="brand-suffix">CMS</span></span></div>
787
- <h1>{!ready ? "Loading editor" : "Connect GitHub"}</h1>
788
+ <h1>{!ready ? $t("shell.loadingEditor") : $t("shell.connectGithub")}</h1>
788
789
  {#if !ready}
789
- {#if loadError}<p class="connection-error" role="alert">{loadError}</p><button class="primary" onclick={loadBlueprint}>Retry loading</button>
790
- {:else}<p role="status">Loading the editor configuration…</p>{/if}
790
+ {#if loadError}<p class="connection-error" role="alert">{loadError}</p><button class="primary" onclick={loadBlueprint}>{$t("shell.retryLoading")}</button>
791
+ {:else}<p role="status">{$t("shell.loadingConfiguration")}</p>{/if}
791
792
  {:else if manifest.github}
792
793
  {#if githubAccess}
793
- <p>Signed in as <strong>{githubAccess.user.login}</strong>.</p>
794
+ <p>{#each $t("shell.signedInAs", { name: "{name}" }).split(/(\{name\})/) as part}{#if part === "{name}"}<strong>{githubAccess.user.login}</strong>{:else}{part}{/if}{/each}</p>
794
795
  {#if githubAccess.status === "read-only" || githubAccess.status === "installation-required"}
795
- <p>You don’t have editing access. Contact the website administrator.</p>
796
+ <p>{$t("shell.noEditingAccess")}</p>
796
797
  {:else}
797
- <p role="status">{connectionBusy ? "Opening the editor…" : "Access confirmed. Try opening the editor again."}</p>
798
+ <p role="status">{connectionBusy ? $t("shell.openingEditor") : $t("shell.accessConfirmed")}</p>
798
799
  {/if}
799
800
  {#if githubAccess.status !== "ready"}
800
- <a id="github-install" class="primary" href={githubAccess.installUrl} target="_blank" rel="noopener noreferrer">Manage GitHub access</a>
801
+ <a id="github-install" class="primary" href={githubAccess.installUrl} target="_blank" rel="noopener noreferrer">{$t("shell.manageGithubAccess")}</a>
801
802
  {/if}
802
803
  {:else}
803
- <p>Sign in to edit this website.</p>
804
+ <p>{$t("shell.signInHint")}</p>
804
805
  {/if}
805
806
  <div class="connection-actions">
806
807
  {#if sessionExpiresAt && !tokenExpired}
807
- <button id="github-check-access" class="primary" disabled={connectionBusy} onclick={recheckGitHub}>{connectionBusy ? "Checking access…" : githubAccess?.status === "ready" ? "Open editor" : "Check access"}</button>
808
+ <button id="github-check-access" class="primary" disabled={connectionBusy} onclick={recheckGitHub}>{connectionBusy ? $t("shell.checkingAccess") : githubAccess?.status === "ready" ? $t("shell.openEditor") : $t("shell.checkAccess")}</button>
808
809
  {/if}
809
- <button id="github-sign-in" class:primary={!sessionExpiresAt || tokenExpired} class:outlined={sessionExpiresAt > 0 && !tokenExpired} disabled={connectionBusy} onclick={connectGitHub}>{connectionBusy && !sessionExpiresAt ? "Signing in…" : sessionExpiresAt ? "Sign in again" : "Sign in with GitHub"}</button>
810
+ <button id="github-sign-in" class:primary={!sessionExpiresAt || tokenExpired} class:outlined={sessionExpiresAt > 0 && !tokenExpired} disabled={connectionBusy} onclick={connectGitHub}>{connectionBusy && !sessionExpiresAt ? $t("shell.signingIn") : sessionExpiresAt ? $t("shell.signInAgain") : $t("shell.signInWithGithub")}</button>
810
811
  </div>
811
- <p class="connection-note">By continuing, you agree to the <a href="https://rigelcms.dev/terms" target="_blank" rel="noopener noreferrer">Terms of Use</a> and acknowledge the <a href="https://rigelcms.dev/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
812
+ <p class="connection-note">{#each $t("shell.legalNotice", { terms: "{terms}", privacy: "{privacy}" }).split(/(\{terms\}|\{privacy\})/) as part}{#if part === "{terms}"}<a href="https://rigelcms.dev/terms" target="_blank" rel="noopener noreferrer">{$t("shell.termsOfUse")}</a>{:else if part === "{privacy}"}<a href="https://rigelcms.dev/privacy" target="_blank" rel="noopener noreferrer">{$t("shell.privacyPolicy")}</a>{:else}{part}{/if}{/each}</p>
812
813
  {/if}
813
814
  {#if connectionError}<p class="connection-error" role="alert">{connectionError}</p>{/if}
814
815
  </main>
@@ -816,13 +817,13 @@
816
817
  {:else}
817
818
  <div class="rigel" class:sidebar-collapsed={settings.userPref.sidebarCollapsed}>
818
819
  <aside id="rigel-sidebar" data-tooltip-position="right">
819
- <a class="brand" href="/rigel/" role="button" aria-label="Welcome" onclick={(event) => { event.preventDefault(); goWelcome(); }}>
820
+ <a class="brand" href="/rigel/" role="button" aria-label={$t("shell.welcome")} onclick={(event) => { event.preventDefault(); goWelcome(); }}>
820
821
  <img src={logo} alt="" width="32" height="32" />
821
822
  <span>Rigel <span class="brand-suffix">CMS</span></span>
822
823
  </a>
823
824
  <TopBar {manifest} {sel} onselect={select} />
824
825
 
825
- <nav id="rigel-sidebar-navigation" class="rail-scroll" aria-label="Content">
826
+ <nav id="rigel-sidebar-navigation" class="rail-scroll" aria-label={$t("shell.content")}>
826
827
  {#each sidebar as node}
827
828
  {#if node.kind === "heading"}
828
829
  <h3 class="sidebar-heading">{node.label}</h3>
@@ -850,16 +851,16 @@
850
851
  </nav>
851
852
 
852
853
  {#if githubMode && manifest.github?.publishBranch}
853
- <button id="publish-button" class="sidebar-publish" aria-label="Publish" data-tooltip={dirty ? "Save your edits before publishing" : "Publish saved branch changes"} disabled={publishBusy || publishLoading || saveState === "saving" || connectionBusy || tokenExpired || githubAccess?.status !== "ready"} onclick={reviewPublish}>
854
- <Icon name="external" size={18} /><span class="sidebar-label">Publish</span>
854
+ <button id="publish-button" class="sidebar-publish" aria-label={$t("shell.publish")} data-tooltip={dirty ? $t("shell.saveBeforePublishing") : $t("shell.publishSavedChanges")} disabled={publishBusy || publishLoading || saveState === "saving" || connectionBusy || tokenExpired || githubAccess?.status !== "ready"} onclick={reviewPublish}>
855
+ <Icon name="rocketLaunch" size={18} /><span class="sidebar-label">{$t("shell.publish")}</span>
855
856
  </button>
856
857
  {/if}
857
- <button class="sidebar-toggle" aria-expanded={!settings.userPref.sidebarCollapsed} aria-controls="rigel-sidebar-navigation" aria-label={settings.userPref.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} data-tooltip={settings.userPref.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} onclick={() => { settings.userPref.sidebarCollapsed = !settings.userPref.sidebarCollapsed; }}>
858
- <SidebarIcon name="sidebar-simple" /><span class="sidebar-label">Collapse sidebar</span>
858
+ <button class="sidebar-toggle" aria-expanded={!settings.userPref.sidebarCollapsed} aria-controls="rigel-sidebar-navigation" aria-label={settings.userPref.sidebarCollapsed ? $t("shell.expandSidebar") : $t("shell.collapseSidebar")} data-tooltip={settings.userPref.sidebarCollapsed ? $t("shell.expandSidebar") : $t("shell.collapseSidebar")} onclick={() => { settings.userPref.sidebarCollapsed = !settings.userPref.sidebarCollapsed; }}>
859
+ <SidebarIcon name="sidebar-simple" /><span class="sidebar-label">{$t("shell.collapseSidebar")}</span>
859
860
  </button>
860
861
  <div class="aside-foot">
861
862
  <div class="user-menu-wrap">
862
- <button class="user-btn" aria-label={githubMode ? "GitHub user menu" : "Local user menu"} data-tooltip={githubMode ? "GitHub user menu" : "Local user menu"} aria-haspopup="menu" aria-expanded={userOpen} onclick={() => (userOpen = !userOpen)}>
863
+ <button class="user-btn" aria-label={githubMode ? $t("shell.githubUserMenu") : $t("shell.localUserMenu")} data-tooltip={githubMode ? $t("shell.githubUserMenu") : $t("shell.localUserMenu")} aria-haspopup="menu" aria-expanded={userOpen} onclick={() => (userOpen = !userOpen)}>
863
864
  {@render avatar()}
864
865
  <span class="user-details"><span class="uname">{userName}</span><small>{userLogin}</small></span>
865
866
  <span class="user-menu-icon"><Icon name="dotsThreeOutlineVertical" size={18} /></span>
@@ -873,26 +874,26 @@
873
874
  <div class="divider"></div>
874
875
  <a role="menuitem" href="/" target="_blank" rel="noopener">
875
876
  <Icon name="external" size={13} />
876
- Live Site
877
+ {$t("shell.liveSite")}
877
878
  </a>
878
879
  <div class="divider"></div>
879
880
  <button role="menuitem" onclick={() => { userOpen = false; settingsOpen = true; }}>
880
881
  <Icon name="gear" size={13} />
881
- Settings
882
+ {$t("shell.settings")}
882
883
  </button>
883
884
  <button role="menuitem" onclick={() => { userOpen = false; shortcutsOpen = true; }}>
884
885
  <Icon name="keyboard" size={13} />
885
- Keyboard Shortcuts
886
+ {$t("shell.keyboardShortcutsMenu")}
886
887
  </button>
887
888
  <div class="divider"></div>
888
889
  {#if githubMode}
889
- <a role="menuitem" href={githubAccess?.installUrl} target="_blank" rel="noopener noreferrer">Configure GitHub access</a>
890
- <button role="menuitem" class="github-reconnect" disabled={connectionBusy || saveState === "saving"} onclick={() => { userOpen = false; void connectGitHub(); }}>Reconnect GitHub</button>
890
+ <a role="menuitem" href={githubAccess?.installUrl} target="_blank" rel="noopener noreferrer">{$t("shell.configureGithubAccess")}</a>
891
+ <button role="menuitem" class="github-reconnect" disabled={connectionBusy || saveState === "saving"} onclick={() => { userOpen = false; void connectGitHub(); }}>{$t("shell.reconnectGithub")}</button>
891
892
  {/if}
892
- <button id={githubMode ? "github-sign-out" : undefined} role="menuitem" class:muted={!githubMode} disabled={!githubMode || saveState === "saving" || connectionBusy} data-tooltip={githubMode ? "Sign out of this editor" : "Local mode"} onclick={signOut}>
893
+ <button id={githubMode ? "github-sign-out" : undefined} role="menuitem" class:muted={!githubMode} disabled={!githubMode || saveState === "saving" || connectionBusy} data-tooltip={githubMode ? $t("shell.signOutTooltip") : $t("shell.localMode")} onclick={signOut}>
893
894
  <Icon name="signOut" size={13} />
894
- Sign Out
895
- {#if !githubMode}<span class="soon">backend: github</span>{/if}
895
+ {$t("shell.signOut")}
896
+ {#if !githubMode}<span class="soon">{$t("shell.githubBackendHint")}</span>{/if}
896
897
  </button>
897
898
  </div>
898
899
  {/if}
@@ -904,8 +905,8 @@
904
905
  <main id="rigel-editor-pane" class:wide={sel?.kind === "collection" || sel?.kind === "pages" || sel?.kind === "documents" || sel?.kind === "document" || sel?.kind === "post" || sel?.kind === "menus" || sel?.kind === "menu" || sel?.kind === "media"} class:visual-pane={visualSelection}>
905
906
  {#if githubMode && (tokenExpired || githubAccess?.status !== "ready" || connectionError)}
906
907
  <div class="github-session" role="status">
907
- <p>{connectionError || (tokenExpired ? "Your GitHub session expired. Your unsaved edits are still here." : "Repository access changed. Reconnect GitHub before saving.")}</p>
908
- <button class="primary" disabled={connectionBusy || saveState === "saving"} onclick={connectGitHub}>{connectionBusy ? "Connecting…" : "Reconnect GitHub"}</button>
908
+ <p>{connectionError || (tokenExpired ? $t("shell.sessionExpired") : $t("shell.repositoryAccessChanged"))}</p>
909
+ <button class="primary" disabled={connectionBusy || saveState === "saving"} onclick={connectGitHub}>{connectionBusy ? $t("shell.connecting") : $t("shell.reconnectGithub")}</button>
909
910
  </div>
910
911
  {/if}
911
912
  {#if settingsError}<p role="alert">{settingsError}</p>{/if}
@@ -913,30 +914,30 @@
913
914
  {#if sel.kind === "page" || sel.kind === "post" || sel.kind === "menu" || sel.kind === "document"}
914
915
  <div class="editor-actions">
915
916
  {#if sel.kind === "page" && page}
916
- <p class="back"><button onclick={() => select(page.shared ? null : { kind: "pages", category: page.category })}>← Back to {page.shared ? "Welcome" : page.category ?? "Pages"}</button></p>
917
+ <p class="back"><button onclick={() => select(page.shared ? null : { kind: "pages", category: page.category })}>{$t("shell.backTo", { name: page.shared ? $t("shell.welcome") : page.category ?? $t("shell.pages") })}</button></p>
917
918
  {:else if sel.kind === "document"}
918
919
  {@const backSel: Sel | null = activeDocType?.singleton ? null : sel.type === pageDocType?.name ? { kind: "pages" } : { kind: "documents", type: sel.type }}
919
- <p class="back"><button onclick={() => select(backSel)}>← Back to {activeDocType?.singleton ? "Welcome" : sel.type === pageDocType?.name ? "Pages" : activeDocType?.title ?? "Documents"}</button></p>
920
+ <p class="back"><button onclick={() => select(backSel)}>{$t("shell.backTo", { name: activeDocType?.singleton ? $t("shell.welcome") : sel.type === pageDocType?.name ? $t("shell.pages") : activeDocType?.title ?? $t("shell.documents") })}</button></p>
920
921
  {:else if sel.kind === "post" && coll}
921
- <p class="back"><button onclick={() => select({ kind: "collection", name: coll.name })}>← Back to {coll.label}</button></p>
922
+ <p class="back"><button onclick={() => select({ kind: "collection", name: coll.name })}>{$t("shell.backTo", { name: coll.label })}</button></p>
922
923
  {:else if sel.kind === "menu"}
923
- <p class="back"><button onclick={() => select({ kind: "menus" })}>← Back to Menus</button></p>
924
+ <p class="back"><button onclick={() => select({ kind: "menus" })}>{$t("shell.backTo", { name: $t("shell.menus") })}</button></p>
924
925
  {/if}
925
926
  <div class="save-actions">
926
927
  <span class="save-status" role="status" class:dirty={dirty && saveState !== "saving"} class:saving={saveState === "saving"} class:saved={!dirty && saveState === "saved"} class:error={saveState === "error"}>
927
- {saveState === "saving" ? "Saving…" : saveState === "error" ? "Save failed" : dirty ? "Unsaved" : saveState === "saved" ? "Saved" : ""}
928
+ {saveState === "saving" ? $t("shell.saving") : saveState === "error" ? $t("shell.saveFailed") : dirty ? $t("shell.unsaved") : saveState === "saved" ? $t("shell.saved") : ""}
928
929
  </span>
929
- <button class="primary save-button" disabled={!dirty || saveState === "saving"} onclick={() => save()}>{saveState === "saving" ? "Saving…" : "Save"}</button>
930
+ <button class="primary save-button" disabled={!dirty || saveState === "saving"} onclick={() => save()}>{saveState === "saving" ? $t("shell.saving") : $t("shell.save")}</button>
930
931
  </div>
931
932
  </div>
932
933
  {/if}
933
934
  <div class="titlebar">
934
935
  <h2>{sel.kind === "page" ? page?.label : sel.kind === "menu" ? String(draft.title || menu?.slug || "") : sel.kind === "post" ? String(draft.title || post?.slug || "") : sel.kind === "document" ? activeDocType?.singleton ? activeDocType.title : String(draft.title || draft.name || activeDocument?.slug || sel.id) : listTitle}</h2>
935
936
  {#if (sel?.kind === "collection" && coll) || (sel?.kind === "pages" && pageDocType?.creatable !== false && pageDocType) || (sel?.kind === "documents" && activeDocType && activeDocType.creatable !== false && !activeDocType.singleton)}
936
- <button class="primary" onclick={() => (sel?.kind === "collection" ? newPost(coll!) : newPage(sel?.kind === "documents" ? activeDocType : pageDocType))}>Add new</button>
937
+ <button class="primary" onclick={() => (sel?.kind === "collection" ? newPost(coll!) : newPage(sel?.kind === "documents" ? activeDocType : pageDocType))}>{$t("shell.addNew")}</button>
937
938
  {/if}
938
939
  {#if sel?.kind === "post" && post}
939
- <button class="icon-btn" aria-label="Delete post" data-tooltip="Delete post" disabled={saveState === "saving"} onclick={deletePost}>
940
+ <button class="icon-btn" aria-label={$t("shell.deletePost")} data-tooltip={$t("shell.deletePost")} disabled={saveState === "saving"} onclick={deletePost}>
940
941
  <Icon name="trash" size={15} />
941
942
  </button>
942
943
  {/if}
@@ -945,16 +946,16 @@
945
946
  {#if saveError}
946
947
  <div class="save-toast" role="alert">
947
948
  <div class="save-toast-content">
948
- <strong>Could not save changes</strong>
949
+ <strong>{$t("shell.saveChangesError")}</strong>
949
950
  <p>{saveError}</p>
950
- <button class="outlined" disabled={saveState === "saving"} onclick={() => save()}>Retry save</button>
951
- {#if githubMode}<button class="outlined github-reconnect" disabled={connectionBusy || saveState === "saving"} onclick={connectGitHub}>Reconnect GitHub</button>{/if}
951
+ <button class="outlined" disabled={saveState === "saving"} onclick={() => save()}>{$t("shell.retrySave")}</button>
952
+ {#if githubMode}<button class="outlined github-reconnect" disabled={connectionBusy || saveState === "saving"} onclick={connectGitHub}>{$t("shell.reconnectGithub")}</button>{/if}
952
953
  </div>
953
- <button type="button" class="icon-btn" aria-label="Dismiss save error" onclick={() => { saveError = ""; }}><Icon name="x" size={18} /></button>
954
+ <button type="button" class="icon-btn" aria-label={$t("shell.dismissSaveError")} onclick={() => { saveError = ""; }}><Icon name="x" size={18} /></button>
954
955
  </div>
955
956
  {/if}
956
957
  {#if !ready}
957
- <p>Loading…</p>
958
+ <p>{$t("shell.loading")}</p>
958
959
 
959
960
  {:else if sel?.kind === "media"}
960
961
  <ImagePicker inline />
@@ -981,7 +982,7 @@
981
982
  <td>
982
983
  {#if col.key === titleColumn}
983
984
  <button class="table-link" onclick={() => select(row.target)}>{row.cells[col.key]}</button>
984
- {#if row.draft}<span class="badge">draft</span>{/if}
985
+ {#if row.draft}<span class="badge">{$t("shell.draftBadge")}</span>{/if}
985
986
  {:else if col.image}
986
987
  {#if row.cells[col.key]}
987
988
  <img class="table-thumbnail" src={row.cells[col.key]} alt="" width="32" height="32" loading="lazy" decoding="async" />
@@ -995,7 +996,7 @@
995
996
  {/each}
996
997
  </tr>
997
998
  {:else}
998
- <tr><td colspan={columns.length}>No entries</td></tr>
999
+ <tr><td colspan={columns.length}>{$t("shell.noEntries")}</td></tr>
999
1000
  {/each}
1000
1001
  </tbody>
1001
1002
  </table>
@@ -1028,14 +1029,14 @@
1028
1029
 
1029
1030
  {:else if sel?.kind === "menu" && menu && manifest.menus}
1030
1031
  <section class="menu-details">
1031
- <label><span>Slug</span><input type="text" value={menu.slug} readonly /></label>
1032
- <label><span>Name</span><input type="text" value={String(draft.title ?? "")} oninput={(e) => { draft.title = e.currentTarget.value; touch(); }} /></label>
1032
+ <label><span>{$t("shell.slug")}</span><input type="text" value={menu.slug} readonly /></label>
1033
+ <label><span>{$t("shell.name")}</span><input type="text" value={String(draft.title ?? "")} oninput={(e) => { draft.title = e.currentTarget.value; touch(); }} /></label>
1033
1034
  </section>
1034
1035
  <MenuEditor items={draft.items as MenuItem[]} maxDepth={manifest.menus.maxDepth} oninput={(items) => { draft.items = items; touch(); }} />
1035
1036
 
1036
1037
  {:else if sel?.kind === "page" && page}
1037
1038
  {#if !page.shared}
1038
- <section><label><span>Website route</span><input type="text" value={manifest.preview?.pages?.[page.slug] ?? "Not configured"} readonly /></label><p class="hint">This copy page uses a fixed Astro route. Its URL can only be changed in the website code; slug metadata cannot move it.</p></section>
1039
+ <section><label><span>{$t("shell.websiteRoute")}</span><input type="text" value={previewPath(manifest.preview ?? {}, { kind: "page", slug: page.slug }) ?? $t("shell.notConfigured")} readonly /></label><p class="hint">{$t("shell.fixedRouteHint")}</p></section>
1039
1040
  {/if}
1040
1041
  {#each page.groups as group (group.id)}
1041
1042
  <section>
@@ -1060,7 +1061,7 @@
1060
1061
  <VisualEditor
1061
1062
  docId={`${coll.name}:${post.slug}`}
1062
1063
  values={draft}
1063
- previewValues={collectionPreviewValues(coll.fields, draft, manifest.documents ?? [])}
1064
+ previewValues={collectionPreviewValues(coll.fields, draft, manifest.documents ?? [], $t)}
1064
1065
  body={bodyDraft}
1065
1066
  bodyInline={settings.userPref.editorExperience === "inline"}
1066
1067
  onbody={(value) => { bodyDraft = value; touch(); }}
@@ -1079,20 +1080,20 @@
1079
1080
  {:else}
1080
1081
  <div class="welcome">
1081
1082
  <img src={logo} alt="" width="64" height="64" />
1082
- <h2>Welcome to Rigel CMS</h2>
1083
- <p>The best CMS in the whole space.</p>
1084
- <p class="welcome-hint">Select a page or collection from the sidebar to start editing.</p>
1083
+ <h2>{$t("shell.welcomeTitle")}</h2>
1084
+ <p>{$t("shell.welcomeTagline")}</p>
1085
+ <p class="welcome-hint">{$t("shell.welcomeHint")}</p>
1085
1086
  </div>
1086
1087
  {/if}
1087
1088
  {#if settings.devMode && (sel?.kind === "page" || sel?.kind === "post" || sel?.kind === "menu" || sel?.kind === "document")}
1088
1089
  <section class="md-preview" class:article-state={sel.kind === "post"}>
1089
- <h3>Current draft</h3>
1090
+ <h3>{$t("shell.currentDraft")}</h3>
1090
1091
  <pre>{JSON.stringify(sel.kind === "post" ? { frontmatter: draft, body: bodyDraft } : draft, null, 2)}</pre>
1091
1092
  </section>
1092
1093
  {/if}
1093
1094
  </main>
1094
1095
  {#if manifest.preview && previewTarget}
1095
- <div class="preview-divider" role="separator" tabindex="0" aria-label="Editor and preview width" aria-orientation="vertical" aria-controls="rigel-editor-pane" aria-valuemin={Math.round(splitMin * 100)} aria-valuemax={Math.round(splitMax * 100)} aria-valuenow={Math.round(previewSplit * 100)} aria-valuetext={`${Math.round(previewSplit * 100)}% editor width`} onpointerdown={startPreviewResize} onpointermove={movePreviewResize} onpointerup={endPreviewResize} onpointercancel={endPreviewResize} onlostpointercapture={endPreviewResize} onkeydown={keyPreviewResize}></div>
1096
+ <div class="preview-divider" role="separator" tabindex="0" aria-label={$t("shell.editorPreviewWidth")} aria-orientation="vertical" aria-controls="rigel-editor-pane" aria-valuemin={Math.round(splitMin * 100)} aria-valuemax={Math.round(splitMax * 100)} aria-valuenow={Math.round(previewSplit * 100)} aria-valuetext={$t("shell.editorWidthPercent", { percent: Math.round(previewSplit * 100) })} onpointerdown={startPreviewResize} onpointermove={movePreviewResize} onpointerup={endPreviewResize} onpointercancel={endPreviewResize} onlostpointercapture={endPreviewResize} onkeydown={keyPreviewResize}></div>
1096
1097
  <SitePreview target={previewTarget} config={manifest.preview} isNew={!!newEntry} values={draft} body={bodyDraft} devMode={settings.devMode} />
1097
1098
  {/if}
1098
1099
  </div>
@@ -0,0 +1,29 @@
1
+ import { DecoratorNode, type LexicalEditor, type NodeKey } from "lexical";
2
+ import type { BlockData } from "../../extensions";
3
+ import { validateBlockData } from "../../extensions";
4
+
5
+ export function cloneBlockData(data: unknown): BlockData {
6
+ validateBlockData(data);
7
+ return JSON.parse(JSON.stringify(data));
8
+ }
9
+
10
+ export class ExtensionNode extends DecoratorNode<null> {
11
+ __name: string;
12
+ __data: BlockData;
13
+ static getType() { return "extension"; }
14
+ static clone(node: ExtensionNode) { return new ExtensionNode(node.__name, node.__data, node.__key); }
15
+ constructor(name: string, data: BlockData, key?: NodeKey) { super(key); this.__name = name; this.__data = cloneBlockData(data); }
16
+ static importJSON(raw: unknown) { const node = raw as { name: string; data: BlockData }; return new ExtensionNode(node.name, node.data); }
17
+ exportJSON() { return { type: "extension", version: 1, name: this.__name, data: cloneBlockData(this.__data) }; }
18
+ setData(data: BlockData) { this.getWritable().__data = cloneBlockData(data); }
19
+ createDOM(_config: unknown, editor: LexicalEditor) {
20
+ const element = (editor.getRootElement()?.ownerDocument ?? document).createElement("div");
21
+ element.contentEditable = "false";
22
+ element.dataset.rigelExtensionEditor = this.__name;
23
+ return element;
24
+ }
25
+ updateDOM() { return false; }
26
+ decorate() { return null; }
27
+ isInline() { return false; }
28
+ getTextContent() { return this.__name; }
29
+ }