@rigel-cms/rigel 1.0.0 → 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 (156) hide show
  1. package/README.md +4 -0
  2. package/docs/extensions.md +194 -0
  3. package/package.json +15 -3
  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 +12 -2
  8. package/src/editor/App.svelte +98 -97
  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/images.ts +33 -31
  128. package/src/editor/lib/menu-tree.ts +5 -4
  129. package/src/editor/lib/preview-frame.ts +4 -3
  130. package/src/editor/lib/schema-edit.ts +3 -2
  131. package/src/editor/lib/settings.ts +5 -3
  132. package/src/editor/lib/slash-command.ts +1 -0
  133. package/src/editor/lib/tooltip.ts +1 -1
  134. package/src/editor/main.ts +9 -0
  135. package/src/editor/styles.css +6 -3
  136. package/src/env.d.ts +11 -1
  137. package/src/extensions.ts +160 -0
  138. package/src/manifest/collections.ts +6 -5
  139. package/src/manifest/menu.ts +12 -10
  140. package/src/manifest/navigation.ts +9 -8
  141. package/src/manifest/references.ts +8 -7
  142. package/src/manifest/seo.ts +4 -3
  143. package/src/manifest/types.ts +1 -0
  144. package/src/preview.ts +22 -2
  145. package/src/routes/local-api.ts +5 -3
  146. package/src/routes/local-documents.ts +20 -16
  147. package/src/routes/local-media.ts +21 -16
  148. package/src/routes/local-menu.ts +9 -7
  149. package/src/routes/local-post.ts +18 -12
  150. package/src/routes/media-files.ts +10 -8
  151. package/src/routes/rigel-page.astro +2 -1
  152. package/src/schema/store.ts +22 -20
  153. package/src/schema/types.ts +1 -0
  154. package/src/schema/validate.ts +73 -69
  155. package/src/slugs.ts +11 -10
  156. package/src/vite/manifest-plugin.ts +33 -1
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { t } from "../i18n";
2
3
  import { onDestroy, tick } from "svelte";
3
4
  import { EditorState, TextSelection } from "prosemirror-state";
4
5
  import { EditorView } from "prosemirror-view";
@@ -39,14 +40,14 @@
39
40
  view.focus();
40
41
  }
41
42
  const marks = [
42
- { name: "Bold", icon: "textB", type: "strong" },
43
- { name: "Italic", icon: "textItalic", type: "em" },
44
- { name: "Code", icon: "code", type: "code" },
43
+ { name: "richtext.bold", icon: "textB", type: "strong" },
44
+ { name: "richtext.italic", icon: "textItalic", type: "em" },
45
+ { name: "richtext.code", icon: "code", type: "code" },
45
46
  ] as const;
46
47
  const blocks = [
47
- { name: "Bullet list", icon: "listBullets", type: "bullet_list" },
48
- { name: "Numbered list", icon: "listNumbers", type: "ordered_list" },
49
- { name: "Quote", icon: "quotes", type: "blockquote" },
48
+ { name: "richtext.bulletList", icon: "listBullets", type: "bullet_list" },
49
+ { name: "richtext.numberedList", icon: "listNumbers", type: "ordered_list" },
50
+ { name: "richtext.quote", icon: "quotes", type: "blockquote" },
50
51
  ] as const;
51
52
  const format = $derived(state?.selection.$from.parent.type.name === "heading" ? `h${state.selection.$from.parent.attrs.level}` : state?.selection.$from.parent.type.name ?? "paragraph");
52
53
 
@@ -57,7 +58,7 @@
57
58
 
58
59
  function load(source: string) {
59
60
  const doc = parseVisualMarkdown(source);
60
- warning = doc ? "" : "This Markdown contains formatting the visual editor cannot safely preserve. Edit the original source below; nothing has been converted.";
61
+ warning = doc ? "" : $t("richtext.unsupportedMarkdown");
61
62
  if (!doc) { mode = "source"; return false; }
62
63
  state = EditorState.create({ doc, plugins: exampleSetup({ schema, menuBar: false }) });
63
64
  view?.updateState(state);
@@ -148,7 +149,7 @@
148
149
 
149
150
  function link(remove = false) {
150
151
  if (!view) return;
151
- if (!remove && (!safeMarkdownUrl(linkUrl) || !linkUrl)) { linkError = "Enter a safe http, https, mailto or relative URL."; return; }
152
+ if (!remove && (!safeMarkdownUrl(linkUrl) || !linkUrl)) { linkError = $t("richtext.safeUrlError"); return; }
152
153
  const { from, to, empty } = view.state.selection;
153
154
  let tr = view.state.tr;
154
155
  if (empty) tr = remove ? tr.removeStoredMark(schema.marks.link) : tr.addStoredMark(schema.marks.link.create({ href: linkUrl }));
@@ -164,36 +165,36 @@
164
165
  <div class="markdown-editor body-editor">
165
166
  <div class="markdown-heading">
166
167
  <div class="field-label" id={`${id}-label`}>{label}</div>
167
- <div class="markdown-modes" role="group" aria-label={`${label} editing mode`}>
168
- <button type="button" aria-pressed={mode === "visual"} onclick={visual}>Visual</button>
169
- <button type="button" aria-pressed={mode === "source"} onclick={() => { mode = "source"; linkOpen = false; }}>Markdown</button>
168
+ <div class="markdown-modes" role="group" aria-label={$t("richtext.editingMode", { label })}>
169
+ <button type="button" aria-pressed={mode === "visual"} onclick={visual}>{$t("richtext.visual")}</button>
170
+ <button type="button" aria-pressed={mode === "source"} onclick={() => { mode = "source"; linkOpen = false; }}>{$t("richtext.markdown")}</button>
170
171
  </div>
171
172
  </div>
172
173
  {#if warning}<p class="markdown-warning" role="status">{warning}</p>{/if}
173
174
  <div class="markdown-visual" hidden={mode !== "visual"}>
174
- <div class="body-toolbar formatting-toolbar" role="group" aria-label={`${label} formatting`}>
175
- <Select ariaLabel="Text style" data-tooltip="Text style" value={format} options={[{ value: "paragraph", label: "Paragraph" }, ...[1, 2, 3, 4, 5, 6].map(level => ({ value: `h${level}`, label: `Heading ${level}` })), { value: "code_block", label: "Code block" }].map(option => ({ ...option, disabled: !!state && format !== option.value && !formatCommand(option.value)(state) }))} onchange={next => command(formatCommand(String(next)))} />
176
- <div class="format-group" role="group" aria-label="Text formatting">
175
+ <div class="body-toolbar formatting-toolbar" role="group" aria-label={$t("richtext.formatting", { label })}>
176
+ <Select ariaLabel={$t("richtext.textStyle")} data-tooltip={$t("richtext.textStyle")} value={format} options={[{ value: "paragraph", label: $t("richtext.paragraph") }, ...[1, 2, 3, 4, 5, 6].map(level => ({ value: `h${level}`, label: $t("richtext.heading", { level }) })), { value: "code_block", label: $t("richtext.codeBlock") }].map(option => ({ ...option, disabled: !!state && format !== option.value && !formatCommand(option.value)(state) }))} onchange={next => command(formatCommand(String(next)))} />
177
+ <div class="format-group" role="group" aria-label={$t("richtext.textFormatting")}>
177
178
  {#each marks as item}
178
- <button type="button" aria-label={item.name} data-tooltip={item.name} aria-pressed={activeMark(item.type)} disabled={!state || !toggleMark(schema.marks[item.type])(state)} onmousedown={event => event.preventDefault()} onclick={() => command(toggleMark(schema.marks[item.type]))}><Icon name={item.icon} size={18} /></button>
179
+ <button type="button" aria-label={$t(item.name)} data-tooltip={$t(item.name)} aria-pressed={activeMark(item.type)} disabled={!state || !toggleMark(schema.marks[item.type])(state)} onmousedown={event => event.preventDefault()} onclick={() => command(toggleMark(schema.marks[item.type]))}><Icon name={item.icon} size={18} /></button>
179
180
  {/each}
180
181
  </div>
181
- <div class="format-group" role="group" aria-label="Lists and quote">
182
+ <div class="format-group" role="group" aria-label={$t("richtext.listsAndQuote")}>
182
183
  {#each blocks as item}
183
- <button type="button" aria-label={item.name} data-tooltip={item.name} aria-pressed={activeBlock(item.type)} disabled={!state || !blockCommand(item.type)(state)} onmousedown={event => event.preventDefault()} onclick={() => command(blockCommand(item.type))}><Icon name={item.icon} size={18} /></button>
184
+ <button type="button" aria-label={$t(item.name)} data-tooltip={$t(item.name)} aria-pressed={activeBlock(item.type)} disabled={!state || !blockCommand(item.type)(state)} onmousedown={event => event.preventDefault()} onclick={() => command(blockCommand(item.type))}><Icon name={item.icon} size={18} /></button>
184
185
  {/each}
185
186
  </div>
186
- <button bind:this={linkButton} type="button" aria-label="Link" data-tooltip="Link" aria-pressed={activeMark("link")} aria-expanded={linkOpen} aria-controls={`${id}-link`} disabled={!state || !toggleMark(schema.marks.link)(state)} onmousedown={event => event.preventDefault()} onclick={openLink}><Icon name="link" size={18} /></button>
187
+ <button bind:this={linkButton} type="button" aria-label={$t("richtext.link")} data-tooltip={$t("richtext.link")} aria-pressed={activeMark("link")} aria-expanded={linkOpen} aria-controls={`${id}-link`} disabled={!state || !toggleMark(schema.marks.link)(state)} onmousedown={event => event.preventDefault()} onclick={openLink}><Icon name="link" size={18} /></button>
187
188
  <ImagePicker toolbar onopen={() => { imageSelection = view?.state.selection.getBookmark(); linkOpen = false; }} onpick={insertImage} onclose={() => { imageSelection = undefined; }} />
188
189
  </div>
189
190
  {#if linkOpen}
190
- <div class="markdown-link" id={`${id}-link`} role="group" aria-label="Edit link" onkeydown={event => { if (event.key === "Escape") { event.stopPropagation(); linkOpen = false; linkButton.focus(); } if (event.key === "Enter") { event.preventDefault(); link(); } }}>
191
- <label for={`${id}-url`}>Link URL</label>
191
+ <div class="markdown-link" id={`${id}-link`} role="group" aria-label={$t("richtext.editLink")} onkeydown={event => { if (event.key === "Escape") { event.stopPropagation(); linkOpen = false; linkButton.focus(); } if (event.key === "Enter") { event.preventDefault(); link(); } }}>
192
+ <label for={`${id}-url`}>{$t("richtext.linkUrl")}</label>
192
193
  <div class="markdown-link-controls">
193
194
  <input id={`${id}-url`} type="text" placeholder="https://…" bind:this={linkInput} bind:value={linkUrl} aria-invalid={!!linkError} aria-describedby={linkError ? `${id}-error` : undefined} />
194
- <button type="button" aria-label="Apply link" data-tooltip="Apply link" onclick={() => link()}><Icon name="check" size={16} /></button>
195
- <button type="button" aria-label="Remove link" data-tooltip="Remove link" disabled={!activeMark("link")} onclick={() => link(true)}><Icon name="linkBreak" size={16} /></button>
196
- <button type="button" aria-label="Close link" data-tooltip="Close link" onclick={() => { linkOpen = false; linkButton.focus(); }}><Icon name="x" size={16} /></button>
195
+ <button type="button" aria-label={$t("richtext.applyLink")} data-tooltip={$t("richtext.applyLink")} onclick={() => link()}><Icon name="check" size={16} /></button>
196
+ <button type="button" aria-label={$t("richtext.removeLink")} data-tooltip={$t("richtext.removeLink")} disabled={!activeMark("link")} onclick={() => link(true)}><Icon name="linkBreak" size={16} /></button>
197
+ <button type="button" aria-label={$t("richtext.closeLink")} data-tooltip={$t("richtext.closeLink")} onclick={() => { linkOpen = false; linkButton.focus(); }}><Icon name="x" size={16} /></button>
197
198
  </div>
198
199
  {#if linkError}<p id={`${id}-error`} role="alert">{linkError}</p>{/if}
199
200
  </div>
@@ -201,6 +202,6 @@
201
202
  <div class="md-preview rich-text" bind:this={host}></div>
202
203
  </div>
203
204
  {#if mode === "source"}
204
- <textarea class="mono" rows="14" aria-label={`${label} Markdown source`} value={value} oninput={(event) => { warning = ""; emit(event.currentTarget.value); }}></textarea>
205
+ <textarea class="mono" rows="14" aria-label={$t("richtext.markdownSource", { label })} value={value} oninput={(event) => { warning = ""; emit(event.currentTarget.value); }}></textarea>
205
206
  {/if}
206
207
  </div>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { t } from "../i18n";
2
3
  import Icon from "./Icon.svelte";
3
4
  import { tick } from "svelte";
4
5
  import ImagePicker from "./ImagePicker.svelte";
@@ -6,7 +7,7 @@
6
7
  import type { EmbedData } from "../blocks/embed";
7
8
  import type { VideoData } from "../blocks/video";
8
9
 
9
- let { triggerLabel = "Insert media", hiddenTrigger = false, onimage, onvideo, onembed, onopen, onclose = () => {} }: {
10
+ let { triggerLabel, hiddenTrigger = false, onimage, onvideo, onembed, onopen, onclose = () => {} }: {
10
11
  triggerLabel?: string;
11
12
  hiddenTrigger?: boolean;
12
13
  onimage?: () => void;
@@ -62,10 +63,10 @@
62
63
 
63
64
  </script>
64
65
 
65
- {#if !hiddenTrigger}<button bind:this={trigger} data-media-trigger type="button" aria-label={triggerLabel} data-tooltip={triggerLabel} aria-haspopup="menu" popovertarget={`${id}-media`} onmousedown={event => event.preventDefault()} onclick={event => { event.preventDefault(); toggleMenu(); }}><Icon name="image" size={18} /></button>{/if}
66
- <div bind:this={menu} id={`${id}-media`} class="block-menu media-menu" popover role="menu" aria-label="Insert media">
67
- <button type="button" role="menuitem" onclick={() => pick("image")}><Icon name="image" size={16} />Image</button>
68
- <button type="button" role="menuitem" onclick={() => pick("video")}><Icon name="filmStrip" size={16} />Video</button>
66
+ {#if !hiddenTrigger}<button bind:this={trigger} data-media-trigger type="button" aria-label={triggerLabel ?? $t("media.insertMedia")} data-tooltip={triggerLabel ?? $t("media.insertMedia")} aria-haspopup="menu" popovertarget={`${id}-media`} onmousedown={event => event.preventDefault()} onclick={event => { event.preventDefault(); toggleMenu(); }}><Icon name="image" size={18} /></button>{/if}
67
+ <div bind:this={menu} id={`${id}-media`} class="block-menu media-menu" popover role="menu" aria-label={$t("media.insertMedia")}>
68
+ <button type="button" role="menuitem" onclick={() => pick("image")}><Icon name="image" size={16} />{$t("media.image")}</button>
69
+ <button type="button" role="menuitem" onclick={() => pick("video")}><Icon name="filmStrip" size={16} />{$t("media.video")}</button>
69
70
  <button type="button" role="menuitem" onclick={() => pick("youtube")}><Icon name="youtubeLogo" size={16} />YouTube</button>
70
71
  <button type="button" role="menuitem" onclick={() => pick("vimeo")}><Icon name="playCircle" size={16} />Vimeo</button>
71
72
  </div>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { t } from "../i18n";
2
3
  import { tick } from "svelte";
3
4
  import type { MenuItem } from "../../manifest/menu";
4
5
  import { editMenuItem, menuRows, moveMenuItem } from "./menu-tree";
@@ -68,8 +69,8 @@
68
69
  function move(id: string, targetId: string | null, position: "before" | "after" | "inside") {
69
70
  try {
70
71
  oninput(moveMenuItem(items, id, targetId, position, maxDepth));
71
- message = "Menu item moved";
72
- } catch (error) { message = error instanceof Error ? error.message : "Cannot move item"; }
72
+ message = $t("fields.menuItemMoved");
73
+ } catch (error) { message = error instanceof Error ? error.message : $t("fields.menuCannotMoveItem"); }
73
74
  dragging = null;
74
75
  target = "";
75
76
  }
@@ -82,10 +83,10 @@
82
83
  }
83
84
  function add(parentId: string | null = null) {
84
85
  const parent = rows.find((row) => row.item.id === parentId);
85
- if (parent && parent.depth >= maxDepth) { message = `Maximum ${maxDepth} levels`; return; }
86
- const item: MenuItem = { id: crypto.randomUUID(), label: "New item", url: "/", children: [] };
86
+ if (parent && parent.depth >= maxDepth) { message = $t("fields.menuMaximumLevels", { maxDepth }); return; }
87
+ const item: MenuItem = { id: crypto.randomUUID(), label: $t("fields.menuNewItem"), url: "/", children: [] };
87
88
  oninput(parentId ? editMenuItem(items, parentId, (p) => ({ ...p, children: [...p.children, item] })) : [...items, item]);
88
- message = "Menu item added";
89
+ message = $t("fields.menuItemAdded");
89
90
  void open(item.id);
90
91
  }
91
92
  function remove(item: MenuItem) {
@@ -96,70 +97,70 @@
96
97
  }
97
98
  </script>
98
99
 
99
- <svelte:window onpointermove={dragPointer} onpointerup={finishDrag} onpointercancel={cancelDrag} onkeydown={(event) => { if (event.key === 'Escape' && pointer) { cancelDrag(); message = 'Move cancelled'; } }} />
100
- <section class="menu-editor" aria-label="Menu items" bind:this={host}>
101
- <nav class="menu-path" aria-label="Menu path">
102
- <button type="button" data-menu-path="" onclick={() => open(null)}>Menu items</button>
100
+ <svelte:window onpointermove={dragPointer} onpointerup={finishDrag} onpointercancel={cancelDrag} onkeydown={(event) => { if (event.key === 'Escape' && pointer) { cancelDrag(); message = $t('fields.menuMoveCancelled'); } }} />
101
+ <section class="menu-editor" aria-label={$t("fields.menuItems")} bind:this={host}>
102
+ <nav class="menu-path" aria-label={$t("fields.menuPath")}>
103
+ <button type="button" data-menu-path="" onclick={() => open(null)}>{$t("fields.menuItems")}</button>
103
104
  {#each chain as row}
104
105
  <Icon name="caretRight" size={12} />
105
- <button type="button" data-menu-path={row.item.id} aria-current={row.item.id === selectedId ? 'step' : undefined} onclick={() => open(row.item.id)}>{row.item.label || "Untitled item"}</button>
106
+ <button type="button" data-menu-path={row.item.id} aria-current={row.item.id === selectedId ? 'step' : undefined} onclick={() => open(row.item.id)}>{row.item.label || $t("fields.menuUntitledItem")}</button>
106
107
  {/each}
107
108
  </nav>
108
109
  <p class="menu-message" role="status" aria-live="polite">{message}</p>
109
110
  <div class="menu-panes">
110
111
  {#each panes as parent, index (parent?.item.id ?? '')}
111
- <section class="menu-pane" class:active={index === panes.length - 1} data-menu-parent={parent?.item.id ?? ''} aria-label={parent ? `${parent.item.label || 'Untitled item'} details` : 'Menu items'}>
112
+ <section class="menu-pane" class:active={index === panes.length - 1} data-menu-parent={parent?.item.id ?? ''} aria-label={parent ? $t("fields.menuItemDetails", { label: parent.item.label || $t("fields.menuUntitledItem") }) : $t("fields.menuItems")}>
112
113
  <div class="menu-pane-head">
113
- <h3>{parent?.item.label || (parent ? "Untitled item" : "Menu items")}</h3>
114
+ <h3>{parent?.item.label || $t(parent ? "fields.menuUntitledItem" : "fields.menuItems")}</h3>
114
115
  {#if parent}
115
116
  <details class="menu-actions">
116
- <summary aria-label={`Actions for ${parent.item.label}`}><Icon name="dotsThreeOutlineVertical" /></summary>
117
+ <summary aria-label={$t("fields.menuItemActions", { label: parent.item.label })}><Icon name="dotsThreeOutlineVertical" /></summary>
117
118
  <div>
118
- <button type="button" disabled={!parent.previous} onclick={() => parent.previous && move(parent.item.id, parent.previous, 'before')}>Up</button>
119
- <button type="button" disabled={!parent.next} onclick={() => parent.next && move(parent.item.id, parent.next, 'after')}>Down</button>
120
- <button type="button" disabled={!parent.previous || parent.depth + Math.max(...menuRows([parent.item]).map(row => row.depth)) > maxDepth} onclick={() => parent.previous && move(parent.item.id, parent.previous, 'inside')}>Indent</button>
121
- <button type="button" disabled={!parent.parentId} onclick={() => parent.parentId && move(parent.item.id, parent.parentId, 'after')}>Outdent</button>
122
- <button type="button" class="menu-remove" onclick={() => remove(parent.item)}>Remove</button>
119
+ <button type="button" disabled={!parent.previous} onclick={() => parent.previous && move(parent.item.id, parent.previous, 'before')}>{$t("fields.menuUp")}</button>
120
+ <button type="button" disabled={!parent.next} onclick={() => parent.next && move(parent.item.id, parent.next, 'after')}>{$t("fields.menuDown")}</button>
121
+ <button type="button" disabled={!parent.previous || parent.depth + Math.max(...menuRows([parent.item]).map(row => row.depth)) > maxDepth} onclick={() => parent.previous && move(parent.item.id, parent.previous, 'inside')}>{$t("fields.menuIndent")}</button>
122
+ <button type="button" disabled={!parent.parentId} onclick={() => parent.parentId && move(parent.item.id, parent.parentId, 'after')}>{$t("fields.menuOutdent")}</button>
123
+ <button type="button" class="menu-remove" onclick={() => remove(parent.item)}>{$t("fields.remove")}</button>
123
124
  </div>
124
125
  </details>
125
126
  {/if}
126
127
  </div>
127
128
  {#if parent}
128
129
  <div class="menu-fields">
129
- <label><span>Label</span><input type="text" aria-label={`Label for ${parent.item.id}`} value={parent.item.label} oninput={event => change(parent.item.id, 'label', event.currentTarget.value)} /></label>
130
- <label><span>URL</span><input type="text" aria-label={`URL for ${parent.item.id}`} value={parent.item.url} placeholder="/about or https://example.com" oninput={event => change(parent.item.id, 'url', event.currentTarget.value)} /></label>
131
- <label class="inline"><input type="checkbox" role="switch" class="switch" checked={parent.item.newTab ?? false} onchange={event => oninput(editMenuItem(items, parent.item.id, item => ({ ...item, newTab: event.currentTarget.checked })))} /><span>Open in new tab</span></label>
130
+ <label><span>{$t("fields.menuLabel")}</span><input type="text" aria-label={$t("fields.menuLabelFor", { id: parent.item.id })} value={parent.item.label} oninput={event => change(parent.item.id, 'label', event.currentTarget.value)} /></label>
131
+ <label><span>{$t("fields.menuUrl")}</span><input type="text" aria-label={$t("fields.menuUrlFor", { id: parent.item.id })} value={parent.item.url} placeholder={$t("fields.menuUrlPlaceholder")} oninput={event => change(parent.item.id, 'url', event.currentTarget.value)} /></label>
132
+ <label class="inline"><input type="checkbox" role="switch" class="switch" checked={parent.item.newTab ?? false} onchange={event => oninput(editMenuItem(items, parent.item.id, item => ({ ...item, newTab: event.currentTarget.checked })))} /><span>{$t("fields.menuOpenInNewTab")}</span></label>
132
133
  </div>
133
- <h4>Items</h4>
134
+ <h4>{$t("fields.menuGroupItems")}</h4>
134
135
  {/if}
135
136
  <div class="menu-list">
136
137
  {#each (parent ? parent.item.children : items) as item (item.id)}
137
138
  <div class="menu-item" class:is-dragging={dragging === item.id} class:drop-before={target === item.id + 'before'} class:drop-after={target === item.id + 'after'} class:selected={chain[index]?.item.id === item.id} data-menu-id={item.id} data-parent-id={parent?.item.id ?? ''} data-depth={index + 1}>
138
- <button type="button" class="menu-drag" aria-label={`Drag ${item.label}`} onpointerdown={event => startDrag(event, item.id)} onlostpointercapture={cancelDrag}><Icon name="listBullets" /></button>
139
- <button type="button" class="menu-open" aria-label={`Open ${item.label || 'Untitled item'}`} aria-expanded={chain[index]?.item.id === item.id} onclick={() => open(item.id)}>
140
- <span><span class="menu-label">{item.label || "Untitled item"}</span><span class="menu-url">{item.url}</span></span>
139
+ <button type="button" class="menu-drag" aria-label={$t("fields.menuDragItem", { label: item.label })} onpointerdown={event => startDrag(event, item.id)} onlostpointercapture={cancelDrag}><Icon name="listBullets" /></button>
140
+ <button type="button" class="menu-open" aria-label={$t("fields.menuOpenItem", { label: item.label || $t("fields.menuUntitledItem") })} aria-expanded={chain[index]?.item.id === item.id} onclick={() => open(item.id)}>
141
+ <span><span class="menu-label">{item.label || $t("fields.menuUntitledItem")}</span><span class="menu-url">{item.url}</span></span>
141
142
  {#if item.children.length}<span class="menu-count">{item.children.length}</span>{/if}
142
143
  <Icon name="caretRight" size={12} />
143
144
  </button>
144
145
  </div>
145
146
  {:else}
146
- <p class="menu-empty">{parent ? "No items in this group." : "No menu items yet."}</p>
147
+ <p class="menu-empty">{$t(parent ? "fields.menuEmptyGroup" : "fields.menuEmpty")}</p>
147
148
  {/each}
148
149
  </div>
149
- <button type="button" class="menu-add" disabled={!!parent && parent.depth >= maxDepth} onclick={() => add(parent?.item.id ?? null)}><Icon name="plus" />{parent ? "Add child" : "Add item"}</button>
150
- {#if parent && parent.depth >= maxDepth}<p class="hint">Maximum {maxDepth} levels.</p>{/if}
150
+ <button type="button" class="menu-add" disabled={!!parent && parent.depth >= maxDepth} onclick={() => add(parent?.item.id ?? null)}><Icon name="plus" />{$t(parent ? "fields.menuAddChild" : "fields.addItem")}</button>
151
+ {#if parent && parent.depth >= maxDepth}<p class="hint">{$t("fields.menuMaximumLevelsHint", { maxDepth })}</p>{/if}
151
152
  </section>
152
153
  {/each}
153
154
  </div>
154
155
  </section>
155
156
  {#if removing}
156
- <ActionDialog title={`Remove “${removing.label}”?`} description="This removes the item and all its children from the draft. Press Save to persist the change." confirmLabel="Remove" danger onconfirm={() => {
157
+ <ActionDialog title={$t("fields.menuRemoveTitle", { label: removing.label })} description={$t("fields.menuRemoveDescription")} confirmLabel={$t("fields.remove")} danger onconfirm={() => {
157
158
  if (removing) {
158
159
  const parentId = rows.find(row => row.item.id === removing?.id)?.parentId ?? null;
159
160
  oninput(editMenuItem(items, removing.id, () => null));
160
161
  void open(parentId);
161
162
  }
162
- message = "Menu item removed";
163
+ message = $t("fields.menuItemRemoved");
163
164
  }} onclose={() => { removing = null; }} />
164
165
  {/if}
165
166
 
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
+ import { t } from "../i18n";
2
3
  import { tick, untrack } from "svelte";
3
4
  import { fetchPreview } from "./preview-frame";
5
+ import { subscribeMediaLabels } from "./embed-preview";
4
6
 
5
7
  let { url, title, frame = $bindable(), reloadToken = 0, onload, onloading, onerror }: {
6
8
  url: string;
@@ -22,18 +24,19 @@
22
24
  const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
23
25
  const controller = new AbortController();
24
26
  const { signal } = controller;
27
+ let stopMediaLabels: (() => void) | undefined;
25
28
  source = null;
26
29
  loading = true;
27
30
  revealed = false;
28
31
  untrack(onloading);
29
- function fail(message = "The prepared page could not be loaded. Check its URL and framing headers.") {
32
+ function fail(message = $t("richtext.preparedPageLoadError")) {
30
33
  if (signal.aborted) return;
31
34
  controller.abort();
32
35
  clearTimeout(timeout);
33
36
  loading = false;
34
37
  onerror(message);
35
38
  }
36
- const timeout = setTimeout(() => fail("The preview took too long to load. Check its URL and try again."), 20000);
39
+ const timeout = setTimeout(() => fail($t("richtext.previewTimeout")), 20000);
37
40
  function wait(ms: number) {
38
41
  return new Promise<void>(resolve => {
39
42
  const finish = () => { clearTimeout(timer); signal.removeEventListener("abort", finish); resolve(); };
@@ -57,11 +60,13 @@
57
60
  onload();
58
61
  await tick();
59
62
  if (signal.aborted || iframe !== frame) return;
63
+ stopMediaLabels?.();
64
+ stopMediaLabels = subscribeMediaLabels(doc.body);
60
65
  clearTimeout(timeout);
61
66
  loading = false;
62
67
  revealed = true;
63
68
  } catch (cause) {
64
- fail(cause instanceof Error ? cause.message : "Preview unavailable");
69
+ fail(cause instanceof Error ? cause.message : $t("richtext.previewUnavailable"));
65
70
  }
66
71
  }
67
72
  const handlers = { ready, fail: () => fail() };
@@ -69,9 +74,9 @@
69
74
  else fetchPreview(target, signal).then(srcdoc => {
70
75
  if (!signal.aborted) source = { srcdoc, ...handlers };
71
76
  }).catch(cause => {
72
- fail(cause instanceof Error ? cause.message : "Preview unavailable");
77
+ fail(cause instanceof Error ? cause.message : $t("richtext.previewUnavailable"));
73
78
  });
74
- return () => { controller.abort(); clearTimeout(timeout); };
79
+ return () => { stopMediaLabels?.(); controller.abort(); clearTimeout(timeout); };
75
80
  });
76
81
  </script>
77
82
 
@@ -82,6 +87,6 @@
82
87
  {/key}
83
88
  {/if}
84
89
  {#if loading}
85
- <div class="preview-loading" role="status"><span class="preview-spinner" aria-hidden="true"></span>Loading preview…</div>
90
+ <div class="preview-loading" role="status"><span class="preview-spinner" aria-hidden="true"></span>{$t("richtext.loadingPreview")}</div>
86
91
  {/if}
87
92
  </div>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { t } from "../i18n";
2
3
  let { tab = $bindable("content") }: { tab?: "content" | "seo" } = $props();
3
4
  function onkey(event: KeyboardEvent) {
4
5
  if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
@@ -8,8 +9,8 @@
8
9
  }
9
10
  </script>
10
11
 
11
- <div class="panel-tabs" role="tablist" aria-label="Properties">
12
+ <div class="panel-tabs" role="tablist" aria-label={$t("fields.properties")}>
12
13
  {#each ["content", "seo"] as value}
13
- <button type="button" role="tab" id={`properties-tab-${value}`} aria-controls={`properties-${value}`} aria-selected={tab === value} tabindex={tab === value ? 0 : -1} data-tab={value} onclick={() => (tab = value as "content" | "seo")} onkeydown={onkey}>{value === "content" ? "Content" : "SEO"}</button>
14
+ <button type="button" role="tab" id={`properties-tab-${value}`} aria-controls={`properties-${value}`} aria-selected={tab === value} tabindex={tab === value ? 0 : -1} data-tab={value} onclick={() => (tab = value as "content" | "seo")} onkeydown={onkey}>{$t(value === "content" ? "fields.content" : "fields.seo")}</button>
14
15
  {/each}
15
16
  </div>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { t } from "../i18n";
2
3
  import Icon from "./Icon.svelte";
3
4
  import type { PublishReview } from "./github-backend";
4
5
  let { review, loading, busy, error, message, onrefresh, onconfirm, onclose }: {
@@ -16,31 +17,31 @@
16
17
 
17
18
  <dialog bind:this={dialog} class="rigel rigel-modal-backdrop publish-dialog" aria-labelledby="publish-title" aria-describedby="publish-description" oncancel={(event) => { event.preventDefault(); if (!busy) onclose(); }}>
18
19
  <form class="rigel-modal" onsubmit={(event) => { event.preventDefault(); onconfirm(); }} aria-busy={loading || busy}>
19
- <div class="publish-head"><h2 id="publish-title">Publish saved changes</h2><button type="button" class="icon-btn" aria-label="Close publish review" disabled={busy} onclick={onclose}><Icon name="x" size={16} /></button></div>
20
- <p id="publish-description">Publish includes <strong>all saved changes on the drafts branch</strong>, including files outside CMS content folders. Unsaved edits are never included or saved automatically.</p>
21
- {#if loading}<p role="status">Loading the complete file review…</p>{/if}
20
+ <div class="publish-head"><h2 id="publish-title">{$t("connection.publishTitle")}</h2><button type="button" class="icon-btn" aria-label={$t("connection.closePublishReview")} disabled={busy} onclick={onclose}><Icon name="x" size={16} /></button></div>
21
+ <p id="publish-description">{#each $t("connection.publishDescription", { scope: "{scope}" }).split("{scope}") as part, index}{#if index}<strong>{$t("connection.publishScope")}</strong>{/if}{part}{/each}</p>
22
+ {#if loading}<p role="status">{$t("connection.loadingReview")}</p>{/if}
22
23
  {#if review}
23
- <p><strong>{review.repo}</strong><br /><span class="publish-branch">{review.branch}</span> to <span class="publish-branch">{review.publishBranch}</span>, as one squash commit.</p>
24
+ <p><strong>{review.repo}</strong><br />{#each $t("connection.publishBranches", { branch: "{branch}", publishBranch: "{publishBranch}" }).split(/(\{branch\}|\{publishBranch\})/) as part}{#if part === "{branch}"}<span class="publish-branch">{review.branch}</span>{:else if part === "{publishBranch}"}<span class="publish-branch">{review.publishBranch}</span>{:else}{part}{/if}{/each}</p>
24
25
  {#if review.needsSync}
25
- <p>The publishing branch has changes not in drafts, including earlier squash publishes. First, explicitly sync it into drafts using GitHub’s normal merge. This does not publish. Conflicts stop the sync. Then review the updated files again before publishing.</p>
26
- <p>These are the current tree differences, not the final publish list:</p>
26
+ <p>{$t("connection.syncDescription")}</p>
27
+ <p>{$t("connection.treeDifferences")}</p>
27
28
  {/if}
28
- <p id="publish-file-count"><strong>{review.files.length} {review.files.length === 1 ? "file" : "files"}</strong></p>
29
+ <p id="publish-file-count"><strong>{$t("connection.fileCount", { count: review.files.length })}</strong></p>
29
30
  {#if review.files.length}
30
- <ul class="publish-files" tabindex="0" aria-label="All changed files" aria-describedby="publish-file-count">
31
+ <ul class="publish-files" tabindex="0" aria-label={$t("connection.allChangedFiles")} aria-describedby="publish-file-count">
31
32
  {#each review.files as file (file.filename)}
32
- <li><span class="publish-file-status">{file.status}</span><span>{file.filename}{#if file.previous_filename}<small>Previously: {file.previous_filename}</small>{/if}</span></li>
33
+ <li><span class="publish-file-status">{$t(`connection.fileStatus.${file.status}`)}</span><span>{file.filename}{#if file.previous_filename}<small>{$t("connection.previousFilename", { filename: file.previous_filename })}</small>{/if}</span></li>
33
34
  {/each}
34
35
  </ul>
35
- {:else}<p>No saved changes to publish. The branch trees are identical.</p>{/if}
36
- <p class="hint">Both branch revisions are checked again before publishing. GitHub pins the draft revision, but cannot atomically lock the target revision; a target update in the final instant can still race. Required checks, reviews and branch protections apply.</p>
36
+ {:else}<p>{$t("connection.noSavedChanges")}</p>{/if}
37
+ <p class="hint">{$t("connection.publishSafety")}</p>
37
38
  {/if}
38
39
  {#if error}<p class="error" role="alert">{error}</p>{/if}
39
40
  {#if message}<p role="status">{message}</p>{/if}
40
41
  <div class="publish-actions">
41
- <button type="button" bind:this={cancel} class="outlined" disabled={busy} onclick={onclose}>{message ? "Close" : "Cancel"}</button>
42
- {#if !message}<button type="button" class="outlined" disabled={loading || busy} onclick={onrefresh}>Review again</button>{/if}
43
- {#if review && !message}<button id="confirm-publish" type="submit" class="primary" disabled={loading || busy || !review.files.length}>{busy ? "Working…" : review.needsSync ? "Sync drafts first" : "Confirm publish"}</button>{/if}
42
+ <button type="button" bind:this={cancel} class="outlined" disabled={busy} onclick={onclose}>{message ? $t("connection.close") : $t("connection.cancel")}</button>
43
+ {#if !message}<button type="button" class="outlined" disabled={loading || busy} onclick={onrefresh}>{$t("connection.reviewAgain")}</button>{/if}
44
+ {#if review && !message}<button id="confirm-publish" type="submit" class="primary" disabled={loading || busy || !review.files.length}>{busy ? $t("connection.working") : review.needsSync ? $t("connection.syncDraftsFirst") : $t("connection.confirmPublish")}</button>{/if}
44
45
  </div>
45
46
  </form>
46
47
  </dialog>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { t } from "../i18n";
2
3
  import type { ReferenceOption } from "../../manifest/references";
3
4
  import Select from "./Select.svelte";
4
5
 
@@ -13,5 +14,5 @@
13
14
  const missing = $derived(selected.filter(id => !options.some(option => option.value === id)));
14
15
  </script>
15
16
 
16
- <Select {label} {multiple} value={multiple ? selected : selected[0] ?? ""} options={[...(!multiple ? [{ value: "", label: `Select ${label.toLowerCase()}…` }] : []), ...options, ...missing.map(id => ({ value: id, label: `Unavailable (${id})` }))]} {onchange} />
17
- {#if !options.length}<p class="hint">No entries have been declared yet.</p>{/if}
17
+ <Select {label} {multiple} value={multiple ? selected : selected[0] ?? ""} options={[...(!multiple ? [{ value: "", label: $t("fields.selectLabel", { label: label.toLowerCase() }) }] : []), ...options, ...missing.map(id => ({ value: id, label: $t("fields.unavailableReference", { id }) }))]} {onchange} />
18
+ {#if !options.length}<p class="hint">{$t("fields.noEntries")}</p>{/if}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { t } from "../i18n";
2
3
  import ImagePicker from "./ImagePicker.svelte";
3
4
  import MarkdownEditor from "./MarkdownEditor.svelte";
4
5
  import SchemaField from "./SchemaField.svelte";
@@ -35,7 +36,8 @@
35
36
  const itemSchema = $derived(field.type === "array" ? itemFields(schema, field) : []);
36
37
  const objectSchema = $derived(field.type === "object" ? objectFields(schema, field) : []);
37
38
  const options = $derived(field.type === "reference" ? referenceOptions(documents, field.to ?? []) : []);
38
- const label = $derived(`${field.label}${field.required ? " *" : ""}${devMode ? ` (${field.key})` : ""}`);
39
+ const fieldLabel = $derived(field.labelKey ? $t(field.labelKey) : field.label);
40
+ const label = $derived(`${fieldLabel}${field.required ? " *" : ""}${devMode ? ` (${field.key})` : ""}`);
39
41
 
40
42
  function normalizeColor(input: string): string {
41
43
  const hex = input.trim().replace(/^#/, "");
@@ -60,8 +62,8 @@
60
62
  <div class="schema-field">
61
63
  <label for={id}><span>{label}</span></label>
62
64
  <div class="schema-color">
63
- <input type="color" aria-label={`${field.label} picker`} value={/^#[0-9a-f]{6}$/.test(color) ? color : "#000000"} oninput={(event) => onvalue(event.currentTarget.value)} />
64
- <input id={id} type="text" aria-label={`${field.label} hex value`} value={typeof value === "string" ? value : ""} placeholder="#000000" spellcheck={false} oninput={(event) => onvalue(event.currentTarget.value)} onblur={(event) => onvalue(normalizeColor(event.currentTarget.value))} />
65
+ <input type="color" aria-label={$t("fields.colorPicker", { label: fieldLabel })} value={/^#[0-9a-f]{6}$/.test(color) ? color : "#000000"} oninput={(event) => onvalue(event.currentTarget.value)} />
66
+ <input id={id} type="text" aria-label={$t("fields.colorHexValue", { label: fieldLabel })} value={typeof value === "string" ? value : ""} placeholder="#000000" spellcheck={false} oninput={(event) => onvalue(event.currentTarget.value)} onblur={(event) => onvalue(normalizeColor(event.currentTarget.value))} />
65
67
  </div>
66
68
  </div>
67
69
  {:else if field.type === "code"}
@@ -74,7 +76,7 @@
74
76
  <label for={id}><span>{label}</span></label>
75
77
  <div class="secret-input">
76
78
  <input id={id} type={revealed ? "text" : "password"} value={typeof value === "string" ? value : ""} placeholder={field.placeholder ?? ""} autocomplete="off" spellcheck={false} oninput={(event) => onvalue(event.currentTarget.value)} />
77
- <button type="button" aria-label={`${revealed ? "Hide" : "Show"} ${field.label}`} aria-controls={id} aria-pressed={revealed} onclick={() => revealed = !revealed}><Icon name={revealed ? "eyeSlash" : "eye"} size={18} /></button>
79
+ <button type="button" aria-label={$t(revealed ? "fields.hideValue" : "fields.showValue", { label: fieldLabel })} aria-controls={id} aria-pressed={revealed} onclick={() => revealed = !revealed}><Icon name={revealed ? "eyeSlash" : "eye"} size={18} /></button>
78
80
  </div>
79
81
  </div>
80
82
  {:else if field.type === "text"}
@@ -83,7 +85,7 @@
83
85
  <textarea rows="3" value={typeof value === "string" ? value : ""} oninput={(event) => onvalue(event.currentTarget.value)}></textarea>
84
86
  </label>
85
87
  {:else if field.type === "markdown"}
86
- <MarkdownEditor label={field.label} value={typeof value === "string" ? value : ""} oninput={(next) => onvalue(next)} />
88
+ <MarkdownEditor label={fieldLabel} value={typeof value === "string" ? value : ""} oninput={(next) => onvalue(next)} />
87
89
  {:else if field.type === "image"}
88
90
  <div class="schema-field">
89
91
  <span class="schema-label">{label}</span>
@@ -102,7 +104,7 @@
102
104
  {:else if field.type === "date"}
103
105
  <DatePicker {label} value={typeof value === "string" ? value : ""} required={field.required} onchange={next => onvalue(next || undefined)} />
104
106
  {:else if field.type === "reference"}
105
- <Select {label} value={value && typeof value === "object" ? String((value as Record<string, unknown>)._ref ?? "") : ""} options={[{ value: "", label: "Choose…" }, ...options.map(option => ({ value: option.id, label: String(option.data.title ?? option.data.name ?? option.slug) }))]} onchange={next => onvalue(next ? { _ref: String(next) } : undefined)} />
107
+ <Select {label} value={value && typeof value === "object" ? String((value as Record<string, unknown>)._ref ?? "") : ""} options={[{ value: "", label: $t("fields.choose") }, ...options.map(option => ({ value: option.id, label: String(option.data.title ?? option.data.name ?? option.slug) }))]} onchange={next => onvalue(next ? { _ref: String(next) } : undefined)} />
106
108
  {:else if field.type === "object"}
107
109
  <fieldset class="schema-object">
108
110
  <legend>{label}</legend>
@@ -124,13 +126,13 @@
124
126
  {#each itemList as item, index (`${base}.${item && typeof item === "object" ? (item as Record<string, unknown>)._key ?? index : index}`)}
125
127
  <div class="schema-item" data-schema-path={`${base}.${item && typeof item === "object" ? (item as Record<string, unknown>)._key ?? index : index}`}>
126
128
  <div class="schema-item-head">
127
- <span>Item {index + 1}</span>
129
+ <span>{$t("fields.itemNumber", { number: index + 1 })}</span>
128
130
  <span class="schema-key">{(item && typeof item === "object" ? (item as Record<string, unknown>)._key : "") ?? ""}</span>
129
131
  <div class="schema-item-actions">
130
- <button type="button" aria-label={`Move ${field.label} item ${index + 1} up`} data-tooltip="Move up" disabled={index === 0} onclick={() => onvalue(moveItem(itemList, index, -1))}>↑</button>
131
- <button type="button" aria-label={`Move ${field.label} item ${index + 1} down`} data-tooltip="Move down" disabled={index === itemList.length - 1} onclick={() => onvalue(moveItem(itemList, index, 1))}>↓</button>
132
- <button type="button" aria-label={`Duplicate ${field.label} item ${index + 1}`} data-tooltip="Duplicate" onclick={() => onvalue(duplicateItem(itemList, index))}>⧉</button>
133
- <button type="button" aria-label={`Remove ${field.label} item ${index + 1}`} data-tooltip="Remove" onclick={() => onvalue(itemList.filter((_, position) => position !== index))}>×</button>
132
+ <button type="button" aria-label={$t("fields.moveItemUp", { label: fieldLabel, number: index + 1 })} data-tooltip={$t("fields.moveUp")} disabled={index === 0} onclick={() => onvalue(moveItem(itemList, index, -1))}>↑</button>
133
+ <button type="button" aria-label={$t("fields.moveItemDown", { label: fieldLabel, number: index + 1 })} data-tooltip={$t("fields.moveDown")} disabled={index === itemList.length - 1} onclick={() => onvalue(moveItem(itemList, index, 1))}>↓</button>
134
+ <button type="button" aria-label={$t("fields.duplicateItem", { label: fieldLabel, number: index + 1 })} data-tooltip={$t("fields.duplicate")} onclick={() => onvalue(duplicateItem(itemList, index))}>⧉</button>
135
+ <button type="button" aria-label={$t("fields.removeItem", { label: fieldLabel, number: index + 1 })} data-tooltip={$t("fields.remove")} onclick={() => onvalue(itemList.filter((_, position) => position !== index))}>×</button>
134
136
  </div>
135
137
  </div>
136
138
  {#if itemSchema.length}
@@ -146,11 +148,11 @@
146
148
  />
147
149
  {/each}
148
150
  {:else}
149
- <input type="text" aria-label={`${field.label} item ${index + 1}`} value={typeof item === "string" ? item : ""} oninput={(event) => setItem(index, event.currentTarget.value)} />
151
+ <input type="text" aria-label={$t("fields.itemLabel", { label: fieldLabel, number: index + 1 })} value={typeof item === "string" ? item : ""} oninput={(event) => setItem(index, event.currentTarget.value)} />
150
152
  {/if}
151
153
  </div>
152
154
  {/each}
153
- <button type="button" class="outlined" onclick={() => onvalue([...itemList, itemSchema.length ? blankItem(itemSchema) : field.items?.some((item) => item.type === "reference") ? { _key: newKey(), _ref: "" } : ""])}>Add item</button>
155
+ <button type="button" class="outlined" onclick={() => onvalue([...itemList, itemSchema.length ? blankItem(itemSchema) : field.items?.some((item) => item.type === "reference") ? { _key: newKey(), _ref: "" } : ""])}>{$t("fields.addItem")}</button>
154
156
  </div>
155
157
  {/if}
156
158
  </div>