@widgetic/editor 4.0.11 → 4.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 (58) hide show
  1. package/README.md +124 -0
  2. package/dist/components/WidgetPreview.svelte +58 -59
  3. package/dist/components/color-select/ColorSelect.svelte +4 -4
  4. package/dist/components/editors/CompositionEditorApp.svelte +69 -15
  5. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +20 -0
  6. package/dist/components/editors/EmbedEditor.svelte +33 -22
  7. package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
  8. package/dist/components/editors/PropsEditor.svelte +74 -58
  9. package/dist/components/editors/PropsEditor.svelte.d.ts +3 -3
  10. package/dist/components/ic/AngleInputController.svelte +1 -1
  11. package/dist/components/ic/BrowserInputController.svelte +37 -29
  12. package/dist/components/ic/ColorPickerInputController.svelte +31 -15
  13. package/dist/components/ic/DateFieldInputController.svelte +27 -11
  14. package/dist/components/ic/DatePickerInputController.svelte +27 -17
  15. package/dist/components/ic/DropdownInputController.svelte +10 -5
  16. package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
  17. package/dist/components/ic/FixedPositionInputController.svelte +12 -6
  18. package/dist/components/ic/FontInputController.svelte +32 -27
  19. package/dist/components/ic/InputControllerHeader.svelte +5 -1
  20. package/dist/components/ic/NumberControls.svelte +4 -4
  21. package/dist/components/ic/NumberInputController.svelte +2 -2
  22. package/dist/components/ic/PositionInputController.svelte +9 -4
  23. package/dist/components/ic/RangeCalendarInputController.svelte +26 -16
  24. package/dist/components/ic/RoundedCornersInputController.svelte +22 -17
  25. package/dist/components/ic/SliderInputController.svelte +7 -2
  26. package/dist/components/ic/SwitchInputController.svelte +12 -3
  27. package/dist/components/ic/TagsInputController.svelte +19 -4
  28. package/dist/components/ic/TextAreaInputController.svelte +59 -44
  29. package/dist/components/ic/TextAreaPreviewInputController.svelte +8 -3
  30. package/dist/components/ic/TextInputController.svelte +17 -1
  31. package/dist/components/ic/TimeInputController.svelte +2 -1
  32. package/dist/components/ic/VectorInputController.svelte +21 -16
  33. package/dist/components/ic/shared/ItemGrid.svelte +7 -3
  34. package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
  35. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
  36. package/dist/composition/compositionEditorController.d.ts +11 -2
  37. package/dist/composition/compositionEditorController.js +33 -21
  38. package/dist/composition/compositionHelpers.d.ts +3 -1
  39. package/dist/composition/compositionHelpers.js +4 -2
  40. package/dist/i18n/index.d.ts +4 -0
  41. package/dist/i18n/index.js +4 -0
  42. package/dist/i18n/labels.d.ts +15 -0
  43. package/dist/i18n/labels.js +57 -0
  44. package/dist/i18n/language.d.ts +228 -0
  45. package/dist/i18n/language.js +34 -0
  46. package/dist/i18n/languageKit.d.ts +54 -0
  47. package/dist/i18n/languageKit.js +102 -0
  48. package/dist/i18n/messages.d.ts +241 -0
  49. package/dist/i18n/messages.js +253 -0
  50. package/dist/i18n/translate.d.ts +6 -0
  51. package/dist/i18n/translate.js +8 -0
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.js +4 -0
  54. package/dist/utils/dockPlacement.d.ts +12 -0
  55. package/dist/utils/dockPlacement.js +13 -0
  56. package/dist/utils/fieldDirection.d.ts +13 -0
  57. package/dist/utils/fieldDirection.js +25 -0
  58. package/package.json +2 -2
package/README.md CHANGED
@@ -152,6 +152,130 @@ has no preview address (`WIDGET_NOT_PUBLISHED`): its settings stay editable and
152
152
  It is a Svelte component for a page that can import it, and it takes the page's styles and fonts as they are. A
153
153
  plain-JavaScript module that works on any page, inside a shadow root, is a separate step.
154
154
 
155
+ ## Languages
156
+
157
+ The editor shows English until it is told otherwise. `CompositionEditorApp` takes two props, and both can change while
158
+ the editor is open:
159
+
160
+ ```svelte
161
+ <CompositionEditorApp
162
+ {compositionId}
163
+ {apiBasePath}
164
+ {getAccessToken}
165
+ locale="de"
166
+ messages={{
167
+ 'editor.props.save': 'Speichern',
168
+ 'editor.app.tryAgain': 'Erneut versuchen',
169
+ 'action.cancel': 'Abbrechen',
170
+ 'shadEditor.toolbar.bold': 'Fett (⌘B)'
171
+ }}
172
+ />
173
+ ```
174
+
175
+ - **`locale`** is a BCP 47 language tag (`de`, `pt-BR`, `ar-EG`). It decides the plural rules and how numbers, dates and the
176
+ names of months and weekdays are written (all through `Intl`). It translates nothing. Blank or left out is English.
177
+ - **`messages`** replaces words, and it is ONE flat object for everything inside the editor: the editor's own words
178
+ (keys that start with `editor.`), the File Browser that opens from its file pickers (`action.`, `upload.`, `dialog.` ...,
179
+ see the File Browser's README, "Languages") and the rich text editor of its text fields (`shadEditor.`). Give the keys
180
+ you want to change; every key you leave out stays English, so a language can be added one screen at a time. A key that
181
+ does not exist is ignored, and the console says `[Editor] ...` once, with the key you probably meant.
182
+
183
+ `ALL_EDITOR_MESSAGES` is the list to hand to a translator: every key of the three catalogs with its English text.
184
+ `EDITOR_MESSAGES` is the editor's own part of it.
185
+
186
+ ```ts
187
+ import { ALL_EDITOR_MESSAGES, EDITOR_MESSAGES } from '@widgetic/editor';
188
+
189
+ console.log(Object.keys(ALL_EDITOR_MESSAGES).length); // every word a person can read in the editor
190
+ console.log(EDITOR_MESSAGES['editor.preview.compilationErrorsDetail']); // 'The generated code has syntax or compilation errors (HTTP {status}).'
191
+ ```
192
+
193
+ A message is text. `{name}` puts a value in, and `{count, plural, one {# file} other {# files}}` picks a form for a
194
+ number in the page's language (the format is the design system's: `@widgetic/design-system/i18n`). Each English message
195
+ names the values it is given; a translation may leave one out or use them in any order, but it cannot ask for a value the
196
+ message is never given.
197
+
198
+ **Finding a mistake before a person does.** `checkEditorMessages(messages)` checks a `messages` object without showing
199
+ anything: a key that does not exist (with the nearest real one), a value that is not text, a template that does not parse,
200
+ a message that asks for a value it is never given. An empty list means every message will be used. Put it in a test of your
201
+ page; `describeMessageProblem` (from `@widgetic/design-system/i18n`) turns each problem into a sentence:
202
+
203
+ ```ts
204
+ import { checkEditorMessages } from '@widgetic/editor';
205
+ import { describeMessageProblem } from '@widgetic/design-system/i18n';
206
+ import german from './editor.de.json';
207
+
208
+ expect(checkEditorMessages(german).map(describeMessageProblem)).toEqual([]);
209
+ ```
210
+
211
+ While the editor runs, a message that cannot be used is never thrown: the English text of that key is shown.
212
+
213
+ **Parts used on their own.** `PropsEditor`, `EmbedEditor` and `WidgetPreview` read their words from the language of the
214
+ `CompositionEditorApp` they sit in. A page that mounts one of them without it says English, unless a component above it
215
+ calls `provideEditorLanguage({ locale, messages })` while it is being made (it gives back a handle whose `update(...)`
216
+ changes the language later). `useEditorLanguage()` is what the parts read it with.
217
+
218
+ **What stays as it is**
219
+
220
+ - **The widget's own words.** The names, descriptions and options of a widget's settings are written by the person who made
221
+ the widget, and are shown as written.
222
+ - **What is saved with the composition.** The title of a new item ("Item 03"), "Untitled Composition" and the names a font
223
+ weight or a position is saved under ("ExtraBold", "top-left") are kept in English whatever the language. The editor shows
224
+ the weights, the tabs of the font and vector lists and the positions in the language; it saves the English name.
225
+ - **What the editor tells the page's code.** `error` events, `failure.message` and the other event details stay English: they
226
+ are for your logs. The person is told by the editor's own screen, in the language.
227
+ - **What comes from outside.** A sentence the Widgetic API sends back, or a media library you give the file pickers, is
228
+ shown as it arrived.
229
+ - **A sentence the editor said once** (why there is no preview) keeps the language it was said in until the editor says
230
+ something new. Every label and button changes at once, and so do the banners of a composition that could not be loaded and
231
+ of a save that failed: they are written when they are drawn.
232
+ - **Dates.** Months and weekdays follow `locale`, and so does the way the range control shows the chosen dates: 7 October 2026
233
+ is `10/07/26` with `en` (and with no `locale`), `07/10/26` with `en-GB` and `07.10.26` with `de`. In a language written right
234
+ to left a calendar writes each weekday in one letter (Arabic `ح ن ث ر خ ج س`) and gives a screen reader the whole name;
235
+ where the browser has no one-letter names (Urdu) the short names stay.
236
+ - **Right to left.** The layout turns around when the editor is given `dir="rtl"` (see "Right to left" below). For a language written right to left a name that is put inside a sentence sits between two invisible marks (Unicode isolates U+2068 and U+2069, made by the shared translator), so the words around it cannot turn it around; a test that compares text has to leave them out.
237
+ - **The design system.** The editor needs `@widgetic/design-system` **0.5.16 or newer** and a `@widgetic/file-browser` that
238
+ takes `locale` and `messages`: its words come from the design system's `@widgetic/design-system/i18n`.
239
+
240
+ The `@widgetic/ui-sdk` module of the composition editor takes the same `locale` and `messages` and adds `setLanguage()`.
241
+
242
+ ### Right to left
243
+
244
+ Arabic, Hebrew, Persian and Urdu start a line on the right. `CompositionEditorApp` takes one more prop for that, and it can
245
+ change while the editor is open:
246
+
247
+ ```svelte
248
+ <CompositionEditorApp {compositionId} {apiBasePath} {getAccessToken} locale="ar" messages={arabic} dir="rtl" />
249
+ ```
250
+
251
+ - **`dir`** is `'rtl'` or `'ltr'`. It is not taken from `locale`: it says which way the screen is read, and a page decides
252
+ that (`resolveTextDirection(locale)` from `@widgetic/design-system/i18n` answers it for a language tag; the `@widgetic/ui-sdk`
253
+ module does exactly this). Left out, the editor follows the direction of the screen it sits in (a component above it that
254
+ calls `provideTextDirection(store)` from `@widgetic/design-system/components/textDirection`), and with neither it is left
255
+ to right, as it always was. The editor writes `dir` on its own root, so it is read the right way wherever it is put.
256
+ - **What turns around.** The settings editor and the preview float over the editor's area and are put in place in pixels
257
+ when the editor appears and again when `dir` changes: on a right to left screen the settings editor is docked at the
258
+ right of the middle and the preview left of it, with the same gaps (`leftOnThisScreen`, `src/lib/utils/dockPlacement.ts`).
259
+ The Design and Content tabs, the label and the control of a row, the calendars (their arrows are mirrored), the sliders,
260
+ the switches and the file picker (the File Browser, which opens inside the editor's own root and is read the way the root
261
+ is) all start at the start of the line; the arrow keys of the date fields follow it, the help tip opens at the end of
262
+ the line, and each tab panel slides in from the side its tab is on.
263
+ - **What a person types** is read by its own first letter on a right to left screen (`directionOfFieldValue`,
264
+ `src/lib/utils/fieldDirection.ts`): a sentence in Latin letters is set from the left and one in Arabic from the right,
265
+ in the same form. A field with nothing in it yet has no first letter (`dir="auto"` on an empty field reads left to right,
266
+ which would put the hint and the caret at the wrong edge), so it takes the direction of the screen. The kinds `email` and
267
+ `url` are addresses and stay left to right on every screen. Note that the kind a schema gives a plain text field is
268
+ `textInput`, not an HTML `type`: only those two names are addresses.
269
+ - **Left to right on every screen.** The colour code, the drawing of the colour picker (saturation and hue), the grid that
270
+ places a box and the picker of the corners of a rounded box are drawings, not lines of text, and say `dir="ltr"` themselves.
271
+ - **Keeping it so.** `tests/editor-placement.test.mjs` reads the source of the editor with `auditPhysicalPlacement` from
272
+ `@widgetic/design-system/rtl/audit` and fails on a class or a style that puts something on a physical side (`left`,
273
+ `right`, `ml-`, `pr-`, `text-left`, four-value shorthands such as `padding: 4px 6px 4px 0` ...): write `start`/`end`
274
+ (`ms-`, `pe-`, `text-start`, `border-s`) or say why with the note `rtl-ok` on the same line or the line above.
275
+ `tests/editor-direction.test.mjs` pins the rest. A panel that went off the screen in Arabic (a `position: relative` box
276
+ is moved from where it sits in the flow, which on a right to left screen is the right edge) and a field that never read
277
+ its text by its own letter were not found by the audit but by looking at a screen: look at one.
278
+
155
279
  ## Input Controllers
156
280
 
157
281
  The component uses the input controllers to edit the widget properties.
@@ -2,6 +2,10 @@
2
2
  import { onMount, onDestroy, createEventDispatcher, tick } from 'svelte';
3
3
  import { GenerateLoader } from '@widgetic/design-system/components';
4
4
  import { lockPointerStyle } from '../utils/shadowDom';
5
+ import { useEditorLanguage } from '../i18n/language';
6
+
7
+ // The words of the editor in the language of the page: the editor above this one chose it (see `provideEditorLanguage`).
8
+ const { t } = useEditorLanguage();
5
9
 
6
10
  const dispatch = createEventDispatcher<{
7
11
  load: { src: string | null };
@@ -88,6 +92,7 @@
88
92
  */
89
93
  export let resizeMode: 'concentric' | 'corner' = 'concentric';
90
94
  /** Initial / reset placement inside the stage. */
95
+ // rtl-ok: 'right-half' names the half of the stage by its place on the screen; the composition editor docks the preview itself
91
96
  export let defaultStageAlign: 'center' | 'right-half' = 'center';
92
97
  /** Observe stage size and fill it (desktop modal) */
93
98
  export let autoFitStage: boolean = false;
@@ -105,7 +110,6 @@
105
110
  let internalIframeSrc: string | null = null;
106
111
  let iframeLoaded = false;
107
112
  let iframeError = false;
108
- let errorMessage = '';
109
113
  let isWidgetPublished = false;
110
114
  let triedFallback = false;
111
115
  let pendingWidget: any = null;
@@ -174,6 +178,7 @@
174
178
  ): { left: number; top: number } {
175
179
  const maxLeft = Math.max(RESIZE_PAD_X, stageWidth - shellWidth - RESIZE_PAD_X);
176
180
  const maxTop = Math.max(RESIZE_PAD_TOP, stageHeight - shellHeight - RESIZE_PAD_BOTTOM);
181
+ // rtl-ok: the half of the stage is named by its place on the screen, see the prop
177
182
  if (defaultStageAlign === 'right-half') {
178
183
  const rightHalfCenterX = stageWidth * 0.75;
179
184
  const left = clampDimension(rightHalfCenterX - shellWidth / 2, RESIZE_PAD_X, maxLeft);
@@ -378,8 +383,8 @@
378
383
  : '';
379
384
  $: iframeTitle =
380
385
  mode === 'controlled'
381
- ? 'Widget preview'
382
- : `Widget preview – ${widget?.name || 'Widget'}`;
386
+ ? $t('editor.preview.frameTitle')
387
+ : $t('editor.preview.frameTitleNamed', { name: widget?.name || $t('editor.preview.widgetName') });
383
388
 
384
389
  function buildWidgetUrl(w: any): string | null {
385
390
  if (!w) return null;
@@ -420,7 +425,6 @@
420
425
  iframeLoaded = false;
421
426
  iframeError = false;
422
427
  widgetReady = false;
423
- errorMessage = '';
424
428
  pendingMessages = [];
425
429
  isResolvingUrl = false;
426
430
 
@@ -476,7 +480,6 @@
476
480
  isWidgetPublished = false;
477
481
  iframeLoaded = false;
478
482
  iframeError = false;
479
- errorMessage = '';
480
483
  widgetReady = false;
481
484
  isResolvingUrl = false;
482
485
  }
@@ -594,7 +597,6 @@
594
597
  iframeError = true;
595
598
  iframeLoaded = false;
596
599
  widgetReady = false;
597
- errorMessage = 'Failed to load widget preview.';
598
600
  console.error('[WidgetPreview] Widget iframe onerror:', effectiveSrc);
599
601
  dispatch('error', { src: effectiveSrc });
600
602
  }
@@ -810,7 +812,7 @@
810
812
  <span
811
813
  class="preview-url-text flex-1 rounded border border-gray-300 px-2 py-1 text-xs text-gray-500 min-w-0 font-mono overflow-x-auto whitespace-nowrap"
812
814
  >
813
- {effectiveDisplayUrl || effectiveSrc || 'no URL'}
815
+ {effectiveDisplayUrl || effectiveSrc || $t('editor.preview.noUrl')}
814
816
  </span>
815
817
  <button
816
818
  type="button"
@@ -818,7 +820,7 @@
818
820
  const url = effectiveDisplayUrl || effectiveSrc;
819
821
  if (url) navigator.clipboard.writeText(url);
820
822
  }}
821
- title="Copy URL"
823
+ title={$t('editor.preview.copyUrl')}
822
824
  class="shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 text-sm cursor-pointer"
823
825
  >
824
826
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
@@ -827,14 +829,14 @@
827
829
  <button
828
830
  type="button"
829
831
  onclick={handleRefreshClick}
830
- title="Refresh preview"
832
+ title={$t('editor.preview.refresh')}
831
833
  class="shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 text-sm cursor-pointer"
832
834
  >🔄</button>
833
835
  {#if showViewportButtons && enableResize}
834
836
  <button
835
837
  type="button"
836
838
  onclick={handleFitToAreaClick}
837
- title="Fit preview to panel"
839
+ title={$t('editor.preview.fitToPanel')}
838
840
  class="preview-fit-to-area-btn shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 cursor-pointer"
839
841
  >
840
842
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>
@@ -842,7 +844,7 @@
842
844
  <button
843
845
  type="button"
844
846
  onclick={applyMobileViewport}
845
- title="Mobile viewport"
847
+ title={$t('editor.preview.mobileViewport')}
846
848
  class="preview-mobile-viewport-btn shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 cursor-pointer"
847
849
  >
848
850
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
@@ -850,7 +852,7 @@
850
852
  <button
851
853
  type="button"
852
854
  onclick={handleDesktopViewport}
853
- title="Desktop viewport (full window)"
855
+ title={$t('editor.preview.desktopViewport')}
854
856
  class="preview-desktop-viewport-btn shrink-0 w-7 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 cursor-pointer"
855
857
  >
856
858
  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
@@ -859,9 +861,9 @@
859
861
  <button
860
862
  type="button"
861
863
  onclick={handleOpenInTabClick}
862
- title="Open in new tab"
864
+ title={$t('editor.preview.openInNewTab')}
863
865
  class="shrink-0 px-2 h-7 flex items-center justify-center rounded border border-gray-300 bg-white hover:bg-gray-100 text-xs cursor-pointer whitespace-nowrap"
864
- >↗ New tab</button>
866
+ >{$t('editor.preview.newTab')}</button>
865
867
  {/if}
866
868
  </div>
867
869
  {/if}
@@ -880,7 +882,7 @@
880
882
 
881
883
  {:else if mode === 'standalone' && !widget}
882
884
  <div class="widget-preview-select-ct flex items-center justify-center h-full text-sm text-gray-400">
883
- Select a widget to preview
885
+ {$t('editor.preview.selectWidget')}
884
886
  </div>
885
887
 
886
888
  {:else if mode === 'standalone' && widget && !effectiveSrc && !isResolvingUrl && !iframeError}
@@ -892,11 +894,11 @@
892
894
  </svg>
893
895
  </div>
894
896
  <div>
895
- <p class="text-sm font-medium text-gray-600">{widget.name || 'Widget'}</p>
896
- <p class="text-xs text-gray-400 mt-1">Not published yet</p>
897
+ <p class="text-sm font-medium text-gray-600">{widget.name || $t('editor.preview.widgetName')}</p>
898
+ <p class="text-xs text-gray-400 mt-1">{$t('editor.preview.notPublished')}</p>
897
899
  </div>
898
900
  <p class="text-[11px] text-gray-300 max-w-[220px]">
899
- Publish the widget in Widget Creator to preview it here.
901
+ {$t('editor.preview.publishToPreview')}
900
902
  </p>
901
903
  </div>
902
904
  </div>
@@ -909,7 +911,7 @@
909
911
  <div
910
912
  class="preview-iframe-shell relative flex flex-col {enableResize ? 'preview-iframe-shell-positioned' : ''}"
911
913
  class:preview-iframe-shell-symmetrical={showSymmetricalStroke}
912
- style="{enableResize
914
+ style="{enableResize /* rtl-ok: the preview is placed in pixels from the left edge of the stage; the dock of the editor mirrors it */
913
915
  ? `position: absolute; left: ${previewShellLeft}px; top: ${previewShellTop}px; width: ${resizeWidth ? resizeWidth + 'px' : '100%'};`
914
916
  : `width: ${resizeWidth ? resizeWidth + 'px' : '100%'}; height: ${resizeHeight ? resizeHeight + 'px' : '100%'};`} transition: {isResizing || isDraggingPreview ? 'none' : 'width 0.2s, height 0.2s, left 0.15s, top 0.15s'};"
915
917
  >
@@ -921,18 +923,18 @@
921
923
  onpointerdown={startPreviewDrag}
922
924
  >
923
925
  {#if enableDrag}
924
- <span class="preview-chrome-drag-grip shrink-0 text-gray-400 text-xs leading-none" title="Drag preview" aria-hidden="true">⠿</span>
926
+ <span class="preview-chrome-drag-grip shrink-0 text-gray-400 text-xs leading-none" title={$t('editor.preview.dragHandle')} aria-hidden="true">⠿</span>
925
927
  {/if}
926
- <span class="preview-chrome-title text-[11px] font-semibold uppercase tracking-wide text-gray-500 shrink-0">Preview</span>
928
+ <span class="preview-chrome-title text-[11px] font-semibold uppercase tracking-wide text-gray-500 shrink-0">{$t('editor.preview.title')}</span>
927
929
  {#if showChromeDeviceToggle}
928
930
  <div
929
- class="preview-chrome-device-toggle ml-auto flex shrink-0 items-center overflow-hidden rounded border border-gray-300"
931
+ class="preview-chrome-device-toggle ms-auto flex shrink-0 items-center overflow-hidden rounded border border-gray-300"
930
932
  onpointerdown={(event) => event.stopPropagation()}
931
933
  >
932
934
  <button
933
935
  type="button"
934
936
  class="preview-chrome-device-desktop flex h-6 w-6 items-center justify-center {!deviceMobile ? 'bg-gray-100 text-gray-800' : 'bg-white text-gray-400'} hover:text-gray-800 focus:outline-none focus-visible:outline-none"
935
- aria-label="Desktop preview"
937
+ aria-label={$t('editor.preview.desktop')}
936
938
  aria-pressed={!deviceMobile}
937
939
  onclick={() => dispatch('deviceView', { mobile: false })}
938
940
  onpointerdown={(event) => event.stopPropagation()}
@@ -941,8 +943,8 @@
941
943
  </button>
942
944
  <button
943
945
  type="button"
944
- class="preview-chrome-device-mobile flex h-6 w-6 items-center justify-center border-l border-gray-300 {deviceMobile ? 'bg-gray-100 text-gray-800' : 'bg-white text-gray-400'} hover:text-gray-800 focus:outline-none focus-visible:outline-none"
945
- aria-label="Mobile preview"
946
+ class="preview-chrome-device-mobile flex h-6 w-6 items-center justify-center border-s border-gray-300 {deviceMobile ? 'bg-gray-100 text-gray-800' : 'bg-white text-gray-400'} hover:text-gray-800 focus:outline-none focus-visible:outline-none"
947
+ aria-label={$t('editor.preview.mobile')}
946
948
  aria-pressed={deviceMobile}
947
949
  onclick={() => dispatch('deviceView', { mobile: true })}
948
950
  onpointerdown={(event) => event.stopPropagation()}
@@ -952,9 +954,9 @@
952
954
  </div>
953
955
  {/if}
954
956
  {#if showDimensionInputs}
955
- <div class="preview-chrome-dimensions flex items-center gap-1.5 {showChromeDeviceToggle ? '' : 'ml-auto'}">
957
+ <div class="preview-chrome-dimensions flex items-center gap-1.5 {showChromeDeviceToggle ? '' : 'ms-auto'}">
956
958
  <label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
957
- <span>W</span>
959
+ <span>{$t('editor.preview.widthShort')}</span>
958
960
  <input
959
961
  type="number"
960
962
  min={MIN_WIDTH}
@@ -969,7 +971,7 @@
969
971
  </label>
970
972
  <span class="text-[10px] text-gray-400">×</span>
971
973
  <label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
972
- <span>H</span>
974
+ <span>{$t('editor.preview.heightShort')}</span>
973
975
  <input
974
976
  type="number"
975
977
  min={MIN_HEIGHT}
@@ -1021,20 +1023,20 @@
1021
1023
  <svg class="w-8 h-8 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
1022
1024
  <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
1023
1025
  </svg>
1024
- <span class="text-sm text-gray-500">{@html errorMessage || 'Failed to load widget.'}</span>
1026
+ <span class="text-sm text-gray-500">{$t('editor.preview.loadFailedPreview')}</span>
1025
1027
  </div>
1026
1028
  </div>
1027
1029
  {:else if mode === 'controlled' && previewError && !isGeneratingCode}
1028
1030
  <div class="preview-error-overlay absolute inset-0 z-10 flex h-full flex-col items-center justify-center bg-red-50/95 text-gray-700 p-6">
1029
1031
  <div class="text-4xl mb-3">⚠️</div>
1030
- <div class="font-semibold text-lg text-red-600 mb-2">Preview Unavailable</div>
1032
+ <div class="font-semibold text-lg text-red-600 mb-2">{$t('editor.preview.unavailable')}</div>
1031
1033
  <div class="text-sm text-center max-w-md whitespace-pre-line text-gray-600 mb-4">{previewError}</div>
1032
1034
  <div class="flex gap-3">
1033
1035
  <button type="button" onclick={() => dispatch('retry')} class="px-3 py-1.5 text-xs rounded border border-blue-500 text-blue-600 bg-white hover:bg-blue-50 cursor-pointer">
1034
- 🔄 Retry {previewRetryCount > 0 ? `(${previewRetryCount})` : ''}
1036
+ 🔄 {$t('editor.preview.retry')} {previewRetryCount > 0 ? `(${previewRetryCount})` : ''}
1035
1037
  </button>
1036
1038
  <button type="button" onclick={() => dispatch('openInTab')} class="px-3 py-1.5 text-xs rounded border border-gray-300 text-gray-600 bg-white hover:bg-gray-100 cursor-pointer">
1037
- Open in new tab
1039
+ {$t('editor.preview.openInNewTab')}
1038
1040
  </button>
1039
1041
  </div>
1040
1042
  </div>
@@ -1042,21 +1044,17 @@
1042
1044
 
1043
1045
  {#if showStandaloneLoading}
1044
1046
  <div class="widget-preview-loading-ct absolute inset-0 z-10 flex items-center justify-center bg-gray-50/80">
1045
- <GenerateLoader title="Loading widget…" subtitle="" size="md" />
1047
+ <GenerateLoader title={$t('editor.preview.loadingWidget')} subtitle="" size="md" />
1046
1048
  </div>
1047
1049
  {:else if showControlledLoading && !isGeneratingCode}
1048
1050
  <div class="loading-overlay absolute inset-0 z-20 flex items-center justify-center bg-gray-50">
1049
1051
  <GenerateLoader
1050
- title={!src && isLoadingUrl
1051
- ? 'Loading Preview…'
1052
- : previewHydrating
1053
- ? 'Loading Preview…'
1054
- : 'Loading Preview…'}
1052
+ title={$t('editor.preview.loadingPreview')}
1055
1053
  subtitle={!src && isLoadingUrl
1056
- ? 'Checking widget on Worker…'
1054
+ ? $t('editor.preview.checkingWidget')
1057
1055
  : previewHydrating
1058
- ? 'Applying composition settings…'
1059
- : 'Connecting to the widget preview…'}
1056
+ ? $t('editor.preview.applyingSettings')
1057
+ : $t('editor.preview.connecting')}
1060
1058
  size="lg"
1061
1059
  />
1062
1060
  </div>
@@ -1067,17 +1065,17 @@
1067
1065
  <div class="text-5xl mb-4">⚠️</div>
1068
1066
  <div class="font-semibold text-xl mb-2">
1069
1067
  {previewError && /TypeError|Uncaught|is not a function|is not defined/i.test(previewError)
1070
- ? 'Widget runtime error'
1071
- : 'Code Has Compilation Errors'}
1068
+ ? $t('editor.preview.runtimeError')
1069
+ : $t('editor.preview.compilationErrors')}
1072
1070
  </div>
1073
1071
  <div class="text-sm text-center text-red-600 mb-4 max-w-md whitespace-pre-line">
1074
1072
  {previewError
1075
- || `The generated code has syntax or compilation errors (HTTP ${previewRuntimeErrorStatus}).`}
1073
+ || $t('editor.preview.compilationErrorsDetail', { status: previewRuntimeErrorStatus ?? '?' })}
1076
1074
  </div>
1077
1075
  <div class="flex gap-3 flex-wrap justify-center">
1078
- <button type="button" onclick={() => dispatch('fixErrors')} class="px-4 py-2 text-xs rounded bg-red-600 hover:bg-red-700 text-white cursor-pointer border-0">🔧 Fix Errors</button>
1079
- <button type="button" onclick={() => dispatch('viewErrors')} class="px-3 py-1.5 text-xs rounded border border-orange-400 text-orange-600 bg-white hover:bg-orange-50 cursor-pointer">📋 View Errors</button>
1080
- <button type="button" onclick={() => dispatch('retryPreview')} class="px-3 py-1.5 text-xs rounded border border-red-300 text-red-600 bg-white hover:bg-red-50 cursor-pointer">🔄 Retry Preview</button>
1076
+ <button type="button" onclick={() => dispatch('fixErrors')} class="px-4 py-2 text-xs rounded bg-red-600 hover:bg-red-700 text-white cursor-pointer border-0">🔧 {$t('editor.preview.fixErrors')}</button>
1077
+ <button type="button" onclick={() => dispatch('viewErrors')} class="px-3 py-1.5 text-xs rounded border border-orange-400 text-orange-600 bg-white hover:bg-orange-50 cursor-pointer">📋 {$t('editor.preview.viewErrors')}</button>
1078
+ <button type="button" onclick={() => dispatch('retryPreview')} class="px-3 py-1.5 text-xs rounded border border-red-300 text-red-600 bg-white hover:bg-red-50 cursor-pointer">🔄 {$t('editor.preview.retryPreview')}</button>
1081
1079
  </div>
1082
1080
  </div>
1083
1081
  {/if}
@@ -1109,7 +1107,7 @@
1109
1107
  {/if}
1110
1108
 
1111
1109
  {#if mode === 'controlled' && isGeneratingCode}
1112
- <GenerateLoader overlay title="Generating code…" subtitle="" size="lg" />
1110
+ <GenerateLoader overlay title={$t('editor.preview.generatingCode')} subtitle="" size="lg" />
1113
1111
  {/if}
1114
1112
  </div>
1115
1113
  </div>
@@ -1119,22 +1117,22 @@
1119
1117
  {:else if mode === 'controlled'}
1120
1118
  {#if isLoadingUrl}
1121
1119
  <div class="flex h-full flex-col items-center justify-center p-6">
1122
- <GenerateLoader title="Loading Preview…" subtitle="" size="lg" />
1120
+ <GenerateLoader title={$t('editor.preview.loadingPreview')} subtitle="" size="lg" />
1123
1121
  </div>
1124
1122
  {:else}
1125
1123
  <div class="flex h-full flex-col items-center justify-center p-6 text-center text-sm text-gray-600">
1126
1124
  {#if isGeneratingCode}
1127
- <GenerateLoader title="Generating code…" subtitle="" size="lg" />
1125
+ <GenerateLoader title={$t('editor.preview.generatingCode')} subtitle="" size="lg" />
1128
1126
  {:else if previewError === 'NO_CODE_YET'}
1129
- <div class="font-semibold text-lg text-gray-700 mb-2">No Code Generated Yet</div>
1127
+ <div class="font-semibold text-lg text-gray-700 mb-2">{$t('editor.preview.noCodeYet')}</div>
1130
1128
  {:else if previewError}
1131
- <div class="font-semibold text-lg text-red-600 mb-2">Preview Unavailable</div>
1129
+ <div class="font-semibold text-lg text-red-600 mb-2">{$t('editor.preview.unavailable')}</div>
1132
1130
  <div class="text-sm text-gray-600 mb-4">{previewError}</div>
1133
- <button type="button" onclick={() => dispatch('retry')} class="px-3 py-1.5 text-xs rounded border border-blue-500 text-blue-600 bg-white hover:bg-blue-50 cursor-pointer">🔄 Retry Build</button>
1131
+ <button type="button" onclick={() => dispatch('retry')} class="px-3 py-1.5 text-xs rounded border border-blue-500 text-blue-600 bg-white hover:bg-blue-50 cursor-pointer">🔄 {$t('editor.preview.retryBuild')}</button>
1134
1132
  {:else if previewLoading}
1135
- <div class="text-lg font-medium text-gray-700">Building Preview...</div>
1133
+ <div class="text-lg font-medium text-gray-700">{$t('editor.preview.building')}</div>
1136
1134
  {:else}
1137
- <div class="text-lg text-gray-600 mb-2">🖼️ No Preview Available</div>
1135
+ <div class="text-lg text-gray-600 mb-2">🖼️ {$t('editor.preview.noPreview')}</div>
1138
1136
  {/if}
1139
1137
  </div>
1140
1138
  {/if}
@@ -1178,10 +1176,11 @@
1178
1176
  box-shadow: none;
1179
1177
  }
1180
1178
 
1181
- .preview-resize-handle-tl { top: -5px; left: -5px; cursor: nwse-resize; }
1182
- .preview-resize-handle-tr { top: -5px; right: -5px; cursor: nesw-resize; }
1183
- .preview-resize-handle-bl { bottom: -5px; left: -5px; cursor: nesw-resize; }
1184
- .preview-resize-handle-br { bottom: -5px; right: -5px; cursor: nwse-resize; }
1179
+ /* The four corner handles of the preview are on the four corners of the box, a place on the screen: the set is the same in both directions. */
1180
+ .preview-resize-handle-tl { top: -5px; left: -5px; cursor: nwse-resize; } /* rtl-ok: top left corner */
1181
+ .preview-resize-handle-tr { top: -5px; right: -5px; cursor: nesw-resize; } /* rtl-ok: top right corner */
1182
+ .preview-resize-handle-bl { bottom: -5px; left: -5px; cursor: nesw-resize; } /* rtl-ok: bottom left corner */
1183
+ .preview-resize-handle-br { bottom: -5px; right: -5px; cursor: nwse-resize; } /* rtl-ok: bottom right corner */
1185
1184
 
1186
1185
  .preview-resize-handle:hover {
1187
1186
  background: #eff6ff;
@@ -173,8 +173,10 @@ $: if (rgb && typeof a === 'number') {
173
173
 
174
174
  <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
175
175
  <!-- svelte-ignore a11y-no-static-element-interactions -->
176
+ <!-- The picker is a drawing: saturation grows to the right and hue is a strip next to it, on every screen, so it is a left to right island. -->
176
177
  <div
177
178
  class={$$props.class}
179
+ dir="ltr"
178
180
  tabindex="0"
179
181
  style="width: {width}px; height: {height}px;"
180
182
  on:keydown={onKeydown}
@@ -184,8 +186,7 @@ $: if (rgb && typeof a === 'number') {
184
186
  id="{instanceId}_sv"
185
187
  width={pickerSize}
186
188
  height={pickerSize}
187
- style:top="{border_size}px"
188
- style:left="{border_size}px"
189
+ style="top: {border_size}px; left: {border_size}px; /* rtl-ok: the saturation square is drawn from the left edge of the picker */"
189
190
  use:pointer={update_sv}
190
191
  use:render_main_image={color.h}
191
192
  />
@@ -193,8 +194,7 @@ $: if (rgb && typeof a === 'number') {
193
194
  <canvas
194
195
  width={sliderWidth}
195
196
  height={pickerSize}
196
- style:top="{border_size}px"
197
- style:left="{pickerSize + gap_size}px"
197
+ style="top: {border_size}px; left: {pickerSize + gap_size}px; /* rtl-ok: the hue strip is drawn next to the square, from the left edge of the picker */"
198
198
  use:pointer={update_h}
199
199
  use:render_slider_image
200
200
  />