@selvajs/ui 4.7.1 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/components/compute/AppLayout.svelte +8 -4
  2. package/dist/components/compute/CollapsedPanelStrip.svelte +8 -1
  3. package/dist/components/compute/ComputeApp.svelte +19 -2
  4. package/dist/components/compute/ComputeApp.svelte.d.ts +8 -0
  5. package/dist/components/preview/TabContent.svelte +5 -1
  6. package/dist/components/primitives/CalculateButton.svelte +8 -4
  7. package/dist/components/primitives/SolvingIndicator.svelte +7 -2
  8. package/dist/components/viewer/MeshMetadataDialog.svelte +6 -2
  9. package/dist/components/viewer/SceneManager.svelte +15 -9
  10. package/dist/components/viewer/Viewer.svelte +44 -23
  11. package/dist/components/viewer/Viewer.svelte.d.ts +8 -0
  12. package/dist/i18n/localeContext.svelte.d.ts +18 -0
  13. package/dist/i18n/localeContext.svelte.js +50 -0
  14. package/dist/i18n/messages.d.ts +54 -0
  15. package/dist/i18n/messages.js +112 -0
  16. package/dist/public.d.ts +3 -0
  17. package/dist/public.js +4 -1
  18. package/package.json +1 -1
  19. package/src/lib/components/compute/AppLayout.svelte +8 -4
  20. package/src/lib/components/compute/CollapsedPanelStrip.svelte +8 -1
  21. package/src/lib/components/compute/ComputeApp.svelte +19 -2
  22. package/src/lib/components/preview/TabContent.svelte +5 -1
  23. package/src/lib/components/primitives/CalculateButton.svelte +8 -4
  24. package/src/lib/components/primitives/SolvingIndicator.svelte +7 -2
  25. package/src/lib/components/viewer/MeshMetadataDialog.svelte +6 -2
  26. package/src/lib/components/viewer/SceneManager.svelte +15 -9
  27. package/src/lib/components/viewer/Viewer.svelte +44 -23
  28. package/src/lib/i18n/localeContext.svelte.ts +63 -0
  29. package/src/lib/i18n/messages.ts +190 -0
  30. package/src/lib/public.ts +14 -1
@@ -11,6 +11,10 @@
11
11
  import * as Resizable from '../primitives/resizable';
12
12
  import { Button } from '../..';
13
13
  import ParameterPresetManager from './ParameterPresetManager.svelte';
14
+ import { getLocaleContext } from '../../i18n/localeContext.svelte';
15
+
16
+ const locale = getLocaleContext();
17
+ const t = $derived(locale.messages);
14
18
 
15
19
  const COLLAPSED_WIDTH = 48;
16
20
 
@@ -74,7 +78,7 @@
74
78
  let isMobile = $state(false);
75
79
  let drawerOpen = $state(false);
76
80
 
77
- const activeLeftTabLabel = $derived(leftTabs[0]?.label ?? 'Parameters');
81
+ const activeLeftTabLabel = $derived(leftTabs[0]?.label ?? t.parametersTab);
78
82
 
79
83
  $effect(() => {
80
84
  const mqMobile = window.matchMedia('(max-width: 639px)');
@@ -200,7 +204,7 @@
200
204
  onclick={() => (drawerOpen = !drawerOpen)}
201
205
  ontouchstart={onDrawerTouchStart}
202
206
  ontouchend={onDrawerTouchEnd}
203
- aria-label={drawerOpen ? 'Collapse panel' : 'Expand panel'}
207
+ aria-label={drawerOpen ? t.collapsePanel : t.expandPanel}
204
208
  aria-expanded={drawerOpen}
205
209
  >
206
210
  <div class="drawer-pill"></div>
@@ -227,7 +231,7 @@
227
231
  <div class="my-4 gap-2 flex items-center">
228
232
  <div class="h-px flex-1 bg-border"></div>
229
233
  <span class="text-xs font-medium px-1 text-muted-foreground">
230
- {rightTabs[0]?.label ?? 'More'}
234
+ {rightTabs[0]?.label ?? t.moreTab}
231
235
  </span>
232
236
  <div class="h-px flex-1 bg-border"></div>
233
237
  </div>
@@ -257,7 +261,7 @@
257
261
  <div class="my-4 gap-2 flex items-center">
258
262
  <div class="h-px flex-1 bg-border"></div>
259
263
  <span class="text-xs font-medium px-1 text-muted-foreground">
260
- {rightTabs[0]?.label ?? 'More'}
264
+ {rightTabs[0]?.label ?? t.moreTab}
261
265
  </span>
262
266
  <div class="h-px flex-1 bg-border"></div>
263
267
  </div>
@@ -1,6 +1,10 @@
1
1
  <script lang="ts">
2
2
  import type { TabConfig } from '@selvajs/schemas';
3
3
  import Icon from '@iconify/svelte';
4
+ import { getLocaleContext } from '../../i18n/localeContext.svelte';
5
+
6
+ const locale = getLocaleContext();
7
+ const t = $derived(locale.messages);
4
8
 
5
9
  interface Props {
6
10
  side: 'left' | 'right';
@@ -29,7 +33,10 @@
29
33
  style="--collapsed-w: {collapsedWidth}px;"
30
34
  role="button"
31
35
  tabindex="0"
32
- aria-label="Expand {side} panel"
36
+ aria-label={t.expandSidePanel.replace(
37
+ '{side}',
38
+ side === 'left' ? t.panelSideLeft : t.panelSideRight
39
+ )}
33
40
  onclick={onExpand}
34
41
  onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && onExpand()}
35
42
  >
@@ -16,6 +16,8 @@
16
16
  import AppLayout from './AppLayout.svelte';
17
17
  import StateDisplay from '../primitives/StateDisplay.svelte';
18
18
  import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
19
+ import type { Locale } from '../../i18n/messages';
20
+ import { setLocaleContext, getLocaleContext } from '../../i18n/localeContext.svelte';
19
21
 
20
22
  import type { Snippet } from 'svelte';
21
23
 
@@ -54,6 +56,13 @@
54
56
  externalScopeKey?: string;
55
57
  // Renders client-sourced inputs with presentation === 'slot'; receives { inputId, displayName, slotLabel, value }.
56
58
  clientSlot?: ClientSlot;
59
+ /**
60
+ * UI language for the app's own chrome (viewer, panels, status text).
61
+ * Provided once here and read by every descendant via locale context.
62
+ * Defaults to English when unset. Does not translate schema-authored labels
63
+ * or Grasshopper-sourced names/metadata.
64
+ */
65
+ lang?: Locale;
57
66
  }
58
67
 
59
68
  let {
@@ -81,13 +90,21 @@
81
90
  header,
82
91
  onReady,
83
92
  externalScopeKey,
84
- clientSlot
93
+ clientSlot,
94
+ lang
85
95
  }: Props = $props();
86
96
 
87
97
  // Make the host's client-input slot available to InputControl deep in the tree.
88
98
  // svelte-ignore state_referenced_locally
89
99
  setClientSlot(clientSlot);
90
100
 
101
+ // Provide the UI locale once for the whole app subtree (viewer, panels, status
102
+ // text). Resolution: explicit `lang` → any host-provided locale → English. The
103
+ // getter is re-read reactively, so switching `lang` updates the chrome live.
104
+ const hostLocale = getLocaleContext();
105
+ setLocaleContext(() => lang ?? hostLocale.locale);
106
+ const t = $derived(getLocaleContext().messages);
107
+
91
108
  const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
92
109
 
93
110
  // Solve Session owns the value/lifecycle state machine; the request/response driver
@@ -182,7 +199,7 @@
182
199
  </div>
183
200
  {:else if !schema}
184
201
  <div class="min-h-100 flex items-center justify-center">
185
- <StateDisplay type="loading" size="large" message="Loading schema..." />
202
+ <StateDisplay type="loading" size="large" message={t.loadingSchema} />
186
203
  </div>
187
204
  {:else}
188
205
  {#key definitionKey}
@@ -3,6 +3,7 @@ import type { ActionButton } from '../../types/actionButton';
3
3
  import type { SolveFn } from '../../types/solveFn';
4
4
  import type { PresetLabels } from '../../types/presetLabels';
5
5
  import { type ClientSlot } from '../../contexts/clientSlotContext.svelte';
6
+ import type { Locale } from '../../i18n/messages';
6
7
  import type { Snippet } from 'svelte';
7
8
  interface Props {
8
9
  schema: UISchema;
@@ -38,6 +39,13 @@ interface Props {
38
39
  header?: Snippet;
39
40
  externalScopeKey?: string;
40
41
  clientSlot?: ClientSlot;
42
+ /**
43
+ * UI language for the app's own chrome (viewer, panels, status text).
44
+ * Provided once here and read by every descendant via locale context.
45
+ * Defaults to English when unset. Does not translate schema-authored labels
46
+ * or Grasshopper-sourced names/metadata.
47
+ */
48
+ lang?: Locale;
41
49
  }
42
50
  declare const ComputeApp: import("svelte").Component<Props, {}, "">;
43
51
  type ComputeApp = ReturnType<typeof ComputeApp>;
@@ -14,6 +14,10 @@
14
14
  import InputControl from './InputControl.svelte';
15
15
  import OutputDisplay from './OutputDisplay.svelte';
16
16
  import { evaluateGroupVisibility } from '../../schema/visibility-rules';
17
+ import { getLocaleContext } from '../../i18n/localeContext.svelte';
18
+
19
+ const locale = getLocaleContext();
20
+ const t = $derived(locale.messages);
17
21
 
18
22
  interface Props {
19
23
  tab: TabConfig;
@@ -79,7 +83,7 @@
79
83
  <ScrollArea class="h-full" orientation="vertical">
80
84
  <div class="p-4 tab-content-container">
81
85
  {#if tab.groups.length === 0}
82
- <StateDisplay type="empty" size="medium" message="This tab has no groups configured." />
86
+ <StateDisplay type="empty" size="medium" message={t.tabNoGroups} />
83
87
  {:else}
84
88
  <div class="gap-8 flex flex-col">
85
89
  {#each tab.groups as group (group.id)}
@@ -1,6 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { Button } from './button';
3
3
  import { Zap, Check, Loader, Play } from '@lucide/svelte';
4
+ import { getLocaleContext } from '../../i18n/localeContext.svelte';
5
+
6
+ const locale = getLocaleContext();
7
+ const t = $derived(locale.messages);
4
8
 
5
9
  interface Props {
6
10
  hasPendingChanges: boolean;
@@ -25,16 +29,16 @@
25
29
  >
26
30
  {#if isSolving}
27
31
  <Loader class="h-4 w-4 animate-spin" />
28
- Solving...
32
+ {t.solvingEllipsis}
29
33
  {:else if hasNeverSolved}
30
34
  <Play class="h-4 w-4" />
31
- Press to calculate
35
+ {t.pressToCalculate}
32
36
  {:else if hasPendingChanges}
33
37
  <Zap class="h-4 w-4" />
34
- Calculate
38
+ {t.calculate}
35
39
  {:else}
36
40
  <Check class="h-4 w-4" />
37
- Up to date
41
+ {t.upToDate}
38
42
  {/if}
39
43
  </Button>
40
44
  </div>
@@ -1,9 +1,14 @@
1
1
  <script lang="ts">
2
+ import { getLocaleContext } from '../../i18n/localeContext.svelte';
3
+
2
4
  interface Props {
3
5
  show: boolean;
4
6
  }
5
7
 
6
8
  let { show }: Props = $props();
9
+
10
+ const locale = getLocaleContext();
11
+ const t = $derived(locale.messages);
7
12
  </script>
8
13
 
9
14
  {#if show}
@@ -19,8 +24,8 @@
19
24
  ></div>
20
25
  </div>
21
26
  <div class="leading-tight flex flex-col">
22
- <span class="text-sm font-semibold">Solving</span>
23
- <span class="text-xs opacity-70">Grasshopper is calculating...</span>
27
+ <span class="text-sm font-semibold">{t.solving}</span>
28
+ <span class="text-xs opacity-70">{t.solvingSubtitle}</span>
24
29
  </div>
25
30
  </div>
26
31
  {/if}
@@ -1,5 +1,9 @@
1
1
  <script lang="ts">
2
2
  import * as Dialog from '../primitives/dialog/index.js';
3
+ import { getLocaleContext } from '../../i18n/localeContext.svelte';
4
+
5
+ const locale = getLocaleContext();
6
+ const t = $derived(locale.messages);
3
7
 
4
8
  interface Props {
5
9
  open: boolean;
@@ -50,12 +54,12 @@
50
54
  <Dialog.Title class="text-sm font-semibold">{meshName}</Dialog.Title>
51
55
  </div>
52
56
  {:else}
53
- <Dialog.Title class="sr-only">Object</Dialog.Title>
57
+ <Dialog.Title class="sr-only">{t.objectFallbackName}</Dialog.Title>
54
58
  {/if}
55
59
 
56
60
  <div class="max-h-80 overflow-y-auto">
57
61
  {#if !metadata || Object.keys(getFilteredMetadata()).length === 0}
58
- <p class="px-4 py-3 text-xs text-muted-foreground">No metadata</p>
62
+ <p class="px-4 py-3 text-xs text-muted-foreground">{t.noMetadata}</p>
59
63
  {:else}
60
64
  <table class="text-xs w-full border-collapse">
61
65
  <tbody>
@@ -2,6 +2,10 @@
2
2
  import * as THREE from 'three';
3
3
  import { Eye, EyeOff, ChevronRight, Search, X } from '@lucide/svelte';
4
4
  import { SvelteSet, SvelteMap } from 'svelte/reactivity';
5
+ import { getLocaleContext } from '../../i18n/localeContext.svelte';
6
+
7
+ const locale = getLocaleContext();
8
+ const t = $derived(locale.messages);
5
9
 
6
10
  interface Props {
7
11
  scene: THREE.Scene;
@@ -165,11 +169,11 @@
165
169
  <Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground/50" />
166
170
  <input
167
171
  class="min-w-0 text-xs flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground/50"
168
- placeholder="Search objects..."
172
+ placeholder={t.searchObjects}
169
173
  bind:value={searchQuery}
170
174
  />
171
175
  {#if searchQuery}
172
- <button onclick={() => (searchQuery = '')} aria-label="Clear search">
176
+ <button onclick={() => (searchQuery = '')} aria-label={t.clearSearch}>
173
177
  <X
174
178
  class="h-3.5 w-3.5 text-muted-foreground/50 transition-colors hover:text-muted-foreground"
175
179
  />
@@ -187,7 +191,7 @@
187
191
  <button
188
192
  class="rounded p-0.5 shrink-0 text-muted-foreground transition-colors hover:text-muted-foreground"
189
193
  onclick={() => toggleLayerCollapsed(layerName)}
190
- aria-label={collapsed ? 'Expand layer' : 'Collapse layer'}
194
+ aria-label={collapsed ? t.expandLayer : t.collapseLayer}
191
195
  >
192
196
  <ChevronRight
193
197
  class="h-3.5 w-3.5 transition-transform duration-150 {collapsed ? '' : 'rotate-90'}"
@@ -197,8 +201,8 @@
197
201
  <button
198
202
  class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
199
203
  onclick={() => toggleLayer(layerName, objects)}
200
- title={layerHidden ? 'Show layer' : 'Hide layer'}
201
- aria-label={layerHidden ? 'Show layer' : 'Hide layer'}
204
+ title={layerHidden ? t.showLayer : t.hideLayer}
205
+ aria-label={layerHidden ? t.showLayer : t.hideLayer}
202
206
  >
203
207
  {#if layerHidden}
204
208
  <EyeOff class="h-3.5 w-3.5 text-muted-foreground/40" />
@@ -249,8 +253,8 @@
249
253
  e.stopPropagation();
250
254
  toggleObject(object);
251
255
  }}
252
- title={hidden ? 'Show' : 'Hide'}
253
- aria-label={hidden ? 'Show object' : 'Hide object'}
256
+ title={hidden ? t.showObject : t.hideObject}
257
+ aria-label={hidden ? t.showObject : t.hideObject}
254
258
  >
255
259
  {#if hidden}
256
260
  <EyeOff class="h-3 w-3 text-muted-foreground/60" />
@@ -284,12 +288,14 @@
284
288
  {#if getSceneObjects().length === 0}
285
289
  <div class="py-12 flex flex-col items-center justify-center text-center">
286
290
  <EyeOff class="mb-2 h-5 w-5 text-muted-foreground/30" />
287
- <p class="text-xs text-muted-foreground">No objects</p>
291
+ <p class="text-xs text-muted-foreground">{t.noObjects}</p>
288
292
  </div>
289
293
  {:else if getFilteredLayerGroups().size === 0}
290
294
  <div class="py-12 flex flex-col items-center justify-center text-center">
291
295
  <Search class="mb-2 h-5 w-5 text-muted-foreground/30" />
292
- <p class="text-xs text-muted-foreground">No results for "{searchQuery}"</p>
296
+ <p class="text-xs text-muted-foreground">
297
+ {t.noResultsFor.replace('{query}', searchQuery)}
298
+ </p>
293
299
  </div>
294
300
  {/if}
295
301
  </div>
@@ -31,6 +31,8 @@
31
31
  import SceneManager from './SceneManager.svelte';
32
32
  import MeshMetadataDialog from './MeshMetadataDialog.svelte';
33
33
  import * as Resizable from '../primitives/resizable/index.js';
34
+ import type { Locale } from '../../i18n/messages';
35
+ import { setLocaleContext, getLocaleContext } from '../../i18n/localeContext.svelte';
34
36
 
35
37
  export interface ViewerConfig {
36
38
  showScreenshotButton?: boolean;
@@ -50,6 +52,13 @@
50
52
  isBlurred?: boolean;
51
53
  drawerOpen?: boolean;
52
54
  viewerConfig?: ViewerConfig;
55
+ /**
56
+ * UI language for the viewer's own chrome (tools menu, panels, dialogs).
57
+ * When set, the viewer provides it to its subtree. When omitted, the viewer
58
+ * reads the nearest locale context (set by the host app), defaulting to
59
+ * English. Does not translate Grasshopper-sourced names/metadata.
60
+ */
61
+ lang?: Locale;
53
62
  }
54
63
 
55
64
  const defaultViewerConfig: Required<ViewerConfig> = {
@@ -68,11 +77,23 @@
68
77
  isSolving = false,
69
78
  isBlurred = false,
70
79
  drawerOpen = false,
71
- viewerConfig = {}
80
+ viewerConfig = {},
81
+ lang
72
82
  }: Props = $props();
73
83
 
74
84
  const config = $derived({ ...defaultViewerConfig, ...viewerConfig });
75
85
 
86
+ // Read any host-provided locale before we (maybe) override it for our subtree.
87
+ const hostLocale = getLocaleContext();
88
+
89
+ // Resolution order: explicit `lang` prop → host locale context → default.
90
+ // Provide the resolved value to our subtree so the scene manager and metadata
91
+ // dialog read the same locale. The getter is re-read reactively, so switching
92
+ // the `lang` prop (or the host's locale) updates the chrome live.
93
+ setLocaleContext(() => lang ?? hostLocale.locale);
94
+ const locale = getLocaleContext();
95
+ const t = $derived(locale.messages);
96
+
76
97
  let canvas: HTMLCanvasElement;
77
98
  let scene: THREE.Scene | null = $state(null);
78
99
  let camera: THREE.PerspectiveCamera | null = null;
@@ -91,14 +112,14 @@
91
112
  let selectedMeshMetadata: Record<string, any> | null = $state(null);
92
113
  let selectedMeshName: string | null = $state(null);
93
114
 
94
- const VIEW_PRESETS: { preset: ViewPreset; label: string }[] = [
95
- { preset: 'top', label: 'Top' },
96
- { preset: 'front', label: 'Front' },
97
- { preset: 'right', label: 'Right' },
98
- { preset: 'back', label: 'Back' },
99
- { preset: 'left', label: 'Left' },
100
- { preset: 'bottom', label: 'Bottom' },
101
- { preset: 'iso', label: 'Isometric' }
115
+ const VIEW_PRESETS: { preset: ViewPreset; label: () => string }[] = [
116
+ { preset: 'top', label: () => t.viewTop },
117
+ { preset: 'front', label: () => t.viewFront },
118
+ { preset: 'right', label: () => t.viewRight },
119
+ { preset: 'back', label: () => t.viewBack },
120
+ { preset: 'left', label: () => t.viewLeft },
121
+ { preset: 'bottom', label: () => t.viewBottom },
122
+ { preset: 'iso', label: () => t.viewIso }
102
123
  ];
103
124
 
104
125
  // Hide button instantly when expanding, show after animation when collapsing
@@ -128,7 +149,7 @@
128
149
  ? (metadata: Record<string, string>) => {
129
150
  if (hasUsefulMetadata(metadata)) {
130
151
  selectedMeshMetadata = metadata;
131
- selectedMeshName = metadata?.name || 'Object';
152
+ selectedMeshName = metadata?.name || t.objectFallbackName;
132
153
  }
133
154
  }
134
155
  : undefined
@@ -265,8 +286,8 @@
265
286
  <DropdownMenu.Root>
266
287
  <DropdownMenu.Trigger
267
288
  class="h-10 w-10 shadow-lg hover:shadow-xl flex items-center justify-center rounded-lg border border-border bg-card/90 transition-all hover:bg-card active:scale-95 data-[state=open]:bg-secondary/60"
268
- title="Viewer tools"
269
- aria-label="Viewer tools"
289
+ title={t.toolsMenu}
290
+ aria-label={t.toolsMenu}
270
291
  >
271
292
  <Settings2 class="h-5 w-5 text-card-foreground" />
272
293
  </DropdownMenu.Trigger>
@@ -280,22 +301,22 @@
280
301
  <DropdownMenu.Item class={itemClass} onSelect={toggleProjection}>
281
302
  {#if projection === 'perspective'}
282
303
  <Square class="h-4 w-4" />
283
- Switch to 2D
304
+ {t.switchTo2D}
284
305
  {:else}
285
306
  <Box class="h-4 w-4" />
286
- Switch to 3D
307
+ {t.switchTo3D}
287
308
  {/if}
288
309
  </DropdownMenu.Item>
289
310
 
290
311
  <DropdownMenu.Item class={itemClass} onSelect={() => fitToView?.()}>
291
312
  <Frame class="h-4 w-4" />
292
- Fit to view
313
+ {t.fitToView}
293
314
  </DropdownMenu.Item>
294
315
 
295
316
  <DropdownMenu.Sub>
296
317
  <DropdownMenu.SubTrigger class="{itemClass} data-[state=open]:bg-muted">
297
318
  <Box class="h-4 w-4" />
298
- <span class="flex-1">Views</span>
319
+ <span class="flex-1">{t.views}</span>
299
320
  <ChevronRight class="h-4 w-4 text-muted-foreground" />
300
321
  </DropdownMenu.SubTrigger>
301
322
  <DropdownMenu.SubContent
@@ -307,7 +328,7 @@
307
328
  class="px-2 py-1.5 text-sm flex cursor-pointer items-center rounded-sm transition-colors outline-none select-none hover:bg-muted focus:bg-muted"
308
329
  onSelect={() => setView(preset)}
309
330
  >
310
- {label}
331
+ {label()}
311
332
  </DropdownMenu.Item>
312
333
  {/each}
313
334
  </DropdownMenu.SubContent>
@@ -319,7 +340,7 @@
319
340
  onSelect={toggleMeasure}
320
341
  >
321
342
  <Ruler class="h-4 w-4" />
322
- <span class="flex-1">Measure</span>
343
+ <span class="flex-1">{t.measure}</span>
323
344
  {#if measureActive}
324
345
  <Check class="h-4 w-4" />
325
346
  {/if}
@@ -332,7 +353,7 @@
332
353
  onSelect={toggleGrid}
333
354
  >
334
355
  <Grid3x3 class="h-4 w-4" />
335
- <span class="flex-1">Grid</span>
356
+ <span class="flex-1">{t.grid}</span>
336
357
  {#if gridVisible}
337
358
  <Check class="h-4 w-4" />
338
359
  {/if}
@@ -351,7 +372,7 @@
351
372
  onSelect={() => (sceneManagerOpen = !sceneManagerOpen)}
352
373
  >
353
374
  <Layers class="h-4 w-4" />
354
- <span class="flex-1">Scene manager</span>
375
+ <span class="flex-1">{t.sceneManager}</span>
355
376
  {#if sceneManagerOpen}
356
377
  <Check class="h-4 w-4" />
357
378
  {/if}
@@ -361,7 +382,7 @@
361
382
  {#if config.showScreenshotButton}
362
383
  <DropdownMenu.Item class={itemClass} onSelect={downloadScreenshot}>
363
384
  <Camera class="h-4 w-4" />
364
- Screenshot
385
+ {t.screenshot}
365
386
  </DropdownMenu.Item>
366
387
  {/if}
367
388
 
@@ -369,10 +390,10 @@
369
390
  <DropdownMenu.Item class={itemClass} onSelect={toggleFullscreen}>
370
391
  {#if isFullscreen}
371
392
  <Minimize class="h-4 w-4" />
372
- Exit fullscreen
393
+ {t.exitFullscreen}
373
394
  {:else}
374
395
  <Maximize class="h-4 w-4" />
375
- Fullscreen
396
+ {t.fullscreen}
376
397
  {/if}
377
398
  </DropdownMenu.Item>
378
399
  {/if}
@@ -1,3 +1,4 @@
1
+ import type { Locale } from '../../i18n/messages';
1
2
  export interface ViewerConfig {
2
3
  showScreenshotButton?: boolean;
3
4
  showFullscreenButton?: boolean;
@@ -15,6 +16,13 @@ interface Props {
15
16
  isBlurred?: boolean;
16
17
  drawerOpen?: boolean;
17
18
  viewerConfig?: ViewerConfig;
19
+ /**
20
+ * UI language for the viewer's own chrome (tools menu, panels, dialogs).
21
+ * When set, the viewer provides it to its subtree. When omitted, the viewer
22
+ * reads the nearest locale context (set by the host app), defaulting to
23
+ * English. Does not translate Grasshopper-sourced names/metadata.
24
+ */
25
+ lang?: Locale;
18
26
  }
19
27
  declare const Viewer: import("svelte").Component<Props, {}, "isFullscreen">;
20
28
  type Viewer = ReturnType<typeof Viewer>;
@@ -0,0 +1,18 @@
1
+ import { type Locale, type ViewerMessages } from './messages';
2
+ export interface LocaleContext {
3
+ /** Current locale. Called reactively — return reactive state to enable live switching. */
4
+ readonly locale: Locale;
5
+ /** Resolved message catalog for the current locale. */
6
+ readonly messages: ViewerMessages;
7
+ }
8
+ /**
9
+ * Provide the locale to descendants. Pass a getter so a reactive source (a
10
+ * `$state`, a store, the app's Paraglide locale) keeps consumers in sync.
11
+ */
12
+ export declare function setLocaleContext(getLocale: () => Locale | undefined): void;
13
+ /**
14
+ * Read the locale context. Falls back to an English-only context when no
15
+ * provider exists (e.g. a primitive used in isolation), so consumers never
16
+ * need a null check.
17
+ */
18
+ export declare function getLocaleContext(): LocaleContext;
@@ -0,0 +1,50 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { messagesFor, DEFAULT_LOCALE } from './messages';
3
+ // ============================================================================
4
+ // Viewer locale context
5
+ // ============================================================================
6
+ //
7
+ // Carries the current UI locale down to the viewer and its panels without
8
+ // threading a `lang` prop through every layer. The value is a getter so the
9
+ // host can back it with reactive state — flip the language and the viewer
10
+ // re-renders live.
11
+ //
12
+ // Resolution order for any consuming component:
13
+ // explicit `lang` prop → nearest locale context → English default
14
+ //
15
+ // Two ways to provide it:
16
+ // - Standalone: <Viewer lang="de" /> — Viewer provides the context itself.
17
+ // - In an app: call setLocaleContext(() => app.locale) once at the root; the
18
+ // viewer (and anything else) reads it. selva later wires its Paraglide
19
+ // locale in here.
20
+ const LOCALE_CONTEXT_KEY = Symbol('viewer-locale-context');
21
+ /**
22
+ * Provide the locale to descendants. Pass a getter so a reactive source (a
23
+ * `$state`, a store, the app's Paraglide locale) keeps consumers in sync.
24
+ */
25
+ export function setLocaleContext(getLocale) {
26
+ const ctx = {
27
+ get locale() {
28
+ return getLocale() ?? DEFAULT_LOCALE;
29
+ },
30
+ get messages() {
31
+ return messagesFor(getLocale());
32
+ }
33
+ };
34
+ setContext(LOCALE_CONTEXT_KEY, ctx);
35
+ }
36
+ /**
37
+ * Read the locale context. Falls back to an English-only context when no
38
+ * provider exists (e.g. a primitive used in isolation), so consumers never
39
+ * need a null check.
40
+ */
41
+ export function getLocaleContext() {
42
+ return (getContext(LOCALE_CONTEXT_KEY) ?? {
43
+ get locale() {
44
+ return DEFAULT_LOCALE;
45
+ },
46
+ get messages() {
47
+ return messagesFor(DEFAULT_LOCALE);
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,54 @@
1
+ export type Locale = 'en' | 'de';
2
+ export interface ViewerMessages {
3
+ toolsMenu: string;
4
+ switchTo2D: string;
5
+ switchTo3D: string;
6
+ fitToView: string;
7
+ views: string;
8
+ measure: string;
9
+ grid: string;
10
+ sceneManager: string;
11
+ screenshot: string;
12
+ fullscreen: string;
13
+ exitFullscreen: string;
14
+ viewTop: string;
15
+ viewFront: string;
16
+ viewRight: string;
17
+ viewBack: string;
18
+ viewLeft: string;
19
+ viewBottom: string;
20
+ viewIso: string;
21
+ searchObjects: string;
22
+ clearSearch: string;
23
+ expandLayer: string;
24
+ collapseLayer: string;
25
+ showLayer: string;
26
+ hideLayer: string;
27
+ showObject: string;
28
+ hideObject: string;
29
+ noObjects: string;
30
+ /** `{query}` is replaced with the current search text. */
31
+ noResultsFor: string;
32
+ objectFallbackName: string;
33
+ noMetadata: string;
34
+ loadingSchema: string;
35
+ parametersTab: string;
36
+ moreTab: string;
37
+ expandPanel: string;
38
+ collapsePanel: string;
39
+ /** `{side}` is replaced with `panelSideLeft` / `panelSideRight`. */
40
+ expandSidePanel: string;
41
+ panelSideLeft: string;
42
+ panelSideRight: string;
43
+ tabNoGroups: string;
44
+ solving: string;
45
+ solvingEllipsis: string;
46
+ solvingSubtitle: string;
47
+ pressToCalculate: string;
48
+ calculate: string;
49
+ upToDate: string;
50
+ }
51
+ export declare const VIEWER_MESSAGES: Record<Locale, ViewerMessages>;
52
+ export declare const DEFAULT_LOCALE: Locale;
53
+ /** Resolve a catalog for a locale, falling back to English for unknown locales. */
54
+ export declare function messagesFor(locale: Locale | undefined): ViewerMessages;