@hypit/hypit 0.1.5 → 0.1.6

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 (31) hide show
  1. package/examples/semantic-composition/README.md +2 -1
  2. package/examples/semantic-composition/chat.svml +1 -1
  3. package/examples/semantic-composition/hypit.runtime.json +1 -1
  4. package/package.json +4 -1
  5. package/packages/caption-fine/README.md +7 -0
  6. package/packages/cli/src/view.ts +4 -3
  7. package/packages/film/src/surface.ts +4 -1
  8. package/packages/runtime-local/README.md +17 -0
  9. package/packages/script/README.md +15 -3
  10. package/packages/script/src/manifest.ts +2 -1
  11. package/packages/script/src/parser.ts +45 -6
  12. package/packages/studio/LOCALIZATION.md +98 -0
  13. package/packages/studio/README.md +13 -0
  14. package/packages/studio/locales/en.json +197 -0
  15. package/packages/studio/locales/zh-CN.json +191 -0
  16. package/packages/studio/src/localization-node.ts +46 -0
  17. package/packages/studio/src/localization.ts +85 -0
  18. package/packages/studio/src/style.css +15 -3
  19. package/packages/studio/src/ui/artifact-name.ts +8 -6
  20. package/packages/studio/src/ui/artifact-preview.ts +5 -4
  21. package/packages/studio/src/ui/code.ts +17 -16
  22. package/packages/studio/src/ui/comments.ts +26 -25
  23. package/packages/studio/src/ui/dropdown.ts +68 -0
  24. package/packages/studio/src/ui/i18n.ts +109 -0
  25. package/packages/studio/src/ui/icons.ts +1 -0
  26. package/packages/studio/src/ui/library.ts +76 -65
  27. package/packages/studio/src/ui/main.ts +84 -131
  28. package/packages/studio/src/ui/scrub-preview.ts +8 -6
  29. package/packages/studio/src/ui/stage.ts +28 -27
  30. package/packages/studio/src/ui/timeline.ts +17 -14
  31. package/packages/studio/start.ts +39 -22
@@ -1,3 +1,4 @@
1
+ import { uiLabel, uiAttribute, uiText, uiAttr, userText } from "./i18n.js";
1
2
  import { createArtifactPreview } from "./artifact-preview.js";
2
3
  import type { StudioArtifactView, StudioSnapshot } from "../shared.js";
3
4
  import { createOverlay } from "./overlay.js";
@@ -31,31 +32,31 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
31
32
  element.className = "stage";
32
33
  element.innerHTML = `
33
34
  <div class="stage-heading">
34
- <div class="stage-title">${icon("preview")}<strong>Preview</strong></div>
35
- <button type="button" class="stage-return" hidden>Back to composition</button>
35
+ <div class="stage-title">${icon("preview")}<strong>${uiLabel("player.preview")}</strong></div>
36
+ <button type="button" class="stage-return" hidden>${uiLabel("player.back-to-composition")}</button>
36
37
  </div>
37
38
  <div class="stage-viewport">
38
39
  <div class="stage-scaler">
39
- <iframe title="Preview" sandbox="allow-scripts allow-same-origin" allow="autoplay"></iframe>
40
+ <iframe ${uiAttribute("title", "player.preview")} sandbox="allow-scripts allow-same-origin" allow="autoplay"></iframe>
40
41
  </div>
41
42
  </div>
42
43
  <div class="stage-progress" hidden>
43
- <input class="stage-scrubber" type="range" min="0" max="0" value="0" step="0.01" aria-label="Media time" hidden>
44
+ <input class="stage-scrubber" type="range" min="0" max="0" value="0" step="0.01" ${uiAttribute("aria-label", "player.media-time")} hidden>
44
45
  </div>
45
46
  <div class="stage-bar">
46
47
  <span class="stage-time" hidden></span>
47
48
  <div class="stage-transport">
48
- <button type="button" data-previous aria-label="Previous frame" title="Previous frame">
49
+ <button type="button" data-previous ${uiAttribute("aria-label", "player.previous-frame")} ${uiAttribute("title", "player.previous-frame")}>
49
50
  ${icon("previous")}
50
51
  </button>
51
- <button type="button" data-play aria-label="Play">
52
+ <button type="button" data-play ${uiAttribute("aria-label", "player.play")}>
52
53
  <span data-icon>${icon("play")}</span>
53
54
  </button>
54
- <button type="button" data-next aria-label="Next frame" title="Next frame">
55
+ <button type="button" data-next ${uiAttribute("aria-label", "player.next-frame")} ${uiAttribute("title", "player.next-frame")}>
55
56
  ${icon("next")}
56
57
  </button>
57
58
  </div>
58
- <button type="button" data-mute aria-label="Mute" class="stage-mute">
59
+ <button type="button" data-mute ${uiAttribute("aria-label", "player.mute")} class="stage-mute">
59
60
  <span data-mute-icon>${icon("volume")}</span>
60
61
  </button>
61
62
  </div>`;
@@ -124,7 +125,7 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
124
125
  event.preventDefault(); stop(); closeSelectionMenu();
125
126
  const menu = document.createElement("div");
126
127
  menu.className = "selection-menu";
127
- menu.setAttribute("role", "menu"); menu.setAttribute("aria-label", "Elements at this point");
128
+ menu.setAttribute("role", "menu"); uiAttr(menu, "aria-label", "player.elements-at-this-point");
128
129
  for (const hit of hits) {
129
130
  const option = document.createElement("button"); option.type = "button";
130
131
  option.setAttribute("role", "menuitem"); option.textContent = hit.display.title;
@@ -204,7 +205,7 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
204
205
  const wasPlaying = playing;
205
206
  playing = false;
206
207
  setIcon(playIcon, "play");
207
- play.setAttribute("aria-label", "Play");
208
+ uiAttr(play, "aria-label", "player.play");
208
209
  if (raf !== 0) cancelAnimationFrame(raf);
209
210
  raf = 0;
210
211
  // Settle the picture on the frame the transport stopped at.
@@ -219,7 +220,7 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
219
220
  if (playing) { stop(); return; }
220
221
  playing = true;
221
222
  setIcon(playIcon, "pause");
222
- play.setAttribute("aria-label", "Pause");
223
+ uiAttr(play, "aria-label", "player.pause");
223
224
  const total = state.snapshot.space.frameCount;
224
225
  const rate = fps(state.snapshot);
225
226
  fromFrame = state.playhead.frame >= total - 1 ? 0 : state.playhead.frame;
@@ -245,7 +246,7 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
245
246
 
246
247
  const applyMuted = (): void => {
247
248
  setIcon(muteIcon, muted ? "muted" : "volume");
248
- mute.setAttribute("aria-label", muted ? "Unmute" : "Mute");
249
+ uiAttr(mute, "aria-label", muted ? "player.unmute" : "player.mute");
249
250
  if (artifactPreview.media !== undefined) artifactPreview.media.muted = muted;
250
251
  (iframe.contentWindow as SeekWindow | null)?.__hypitSetMuted?.(muted);
251
252
  };
@@ -286,12 +287,12 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
286
287
  previous.disabled = next.disabled = play.disabled;
287
288
  scrubber.disabled = duration <= 0;
288
289
  scrubber.step = "0.01";
289
- scrubber.setAttribute("aria-label", "Media time");
290
+ uiAttr(scrubber, "aria-label", "player.media-time");
290
291
  scrubber.max = String(duration);
291
292
  scrubber.value = String(media?.currentTime ?? 0);
292
293
  time.textContent = `${clock(media?.currentTime ?? 0)} / ${clock(duration)}`;
293
294
  setIcon(playIcon, media !== undefined && !media.paused ? "pause" : "play");
294
- play.setAttribute("aria-label", media !== undefined && !media.paused ? "Pause" : "Play");
295
+ uiAttr(play, "aria-label", media !== undefined && !media.paused ? "player.pause" : "player.play");
295
296
  });
296
297
  viewport.append(artifactPreview.element);
297
298
  const compositionTime = (): void => {
@@ -302,7 +303,7 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
302
303
  scrubber.step = String(1 / rate);
303
304
  scrubber.max = String(Math.max(0, state.snapshot.space.frameCount - 1) / rate);
304
305
  scrubber.value = String(state.playhead.frame / rate);
305
- scrubber.setAttribute("aria-label", "Composition time");
306
+ uiAttr(scrubber, "aria-label", "player.composition-time");
306
307
  time.textContent = `${clock(state.playhead.frame / rate)} / ${clock(state.snapshot.space.frameCount / rate)}`;
307
308
  };
308
309
  scrubber.addEventListener("input", () => {
@@ -318,14 +319,14 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
318
319
  compositionTime();
319
320
  transport.hidden = mute.hidden = false;
320
321
  play.disabled = previous.disabled = next.disabled = false;
321
- previous.title = "Previous frame";
322
- previous.setAttribute("aria-label", previous.title);
323
- next.title = "Next frame";
324
- next.setAttribute("aria-label", next.title);
325
- title.textContent = "Preview";
322
+ uiAttr(previous, "title", "player.previous-frame");
323
+ uiAttr(previous, "aria-label", "player.previous-frame");
324
+ uiAttr(next, "title", "player.next-frame");
325
+ uiAttr(next, "aria-label", "player.next-frame");
326
+ uiText(title, "player.preview");
326
327
  title.removeAttribute("title");
327
328
  setIcon(playIcon, "play");
328
- play.setAttribute("aria-label", "Play");
329
+ uiAttr(play, "aria-label", "player.play");
329
330
  fit();
330
331
  if (state !== undefined && ready) seekPicture(state.playhead.frame);
331
332
  };
@@ -335,12 +336,12 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
335
336
  stop();
336
337
  scaler.hidden = true;
337
338
  back.hidden = false;
338
- title.textContent = artifact.displayName ?? artifact.output;
339
+ userText(title, artifact.displayName ?? artifact.output);
339
340
  title.title = artifact.output;
340
- previous.title = "Back 5 seconds";
341
- next.title = "Forward 5 seconds";
342
- previous.setAttribute("aria-label", previous.title);
343
- next.setAttribute("aria-label", next.title);
341
+ uiAttr(previous, "title", "player.back-5-seconds");
342
+ uiAttr(next, "title", "player.forward-5-seconds");
343
+ uiAttr(previous, "aria-label", "player.back-5-seconds");
344
+ uiAttr(next, "aria-label", "player.forward-5-seconds");
344
345
  artifactPreview.open(artifact, muted);
345
346
  selectedArtifact(artifact.id);
346
347
  };
@@ -404,7 +405,7 @@ export function createStage(store: Store, selectedArtifact: (id: string | undefi
404
405
  showComposition() { if (artifactPreview.selected !== undefined) returnToComposition(); },
405
406
  renameArtifact(artifact) {
406
407
  const selected = artifactPreview.selected;
407
- if (selected?.build === artifact.build && selected.output === artifact.output) title.textContent = artifact.displayName ?? artifact.output;
408
+ if (selected?.build === artifact.build && selected.output === artifact.output) userText(title, artifact.displayName ?? artifact.output);
408
409
  },
409
410
  };
410
411
  }
@@ -1,3 +1,4 @@
1
+ import { userText, uiAttribute, uiText, uiAttr } from "./i18n.js";
1
2
  import type {
2
3
  SemanticToken,
3
4
  StudioSnapshot,
@@ -89,13 +90,13 @@ export function createTimeline(store: Store): Timeline {
89
90
  element.innerHTML = `
90
91
  <div class="timeline-toolbar">
91
92
  <div class="timeline-tools">
92
- <button type="button" class="tool-button active" aria-label="Select" title="Select">${icon("select")}</button>
93
+ <button type="button" class="tool-button active" ${uiAttribute("aria-label", "timeline.select")} ${uiAttribute("title", "timeline.select")}>${icon("select")}</button>
93
94
  </div>
94
95
  <div class="timeline-time" data-timeline-time>00:00:00</div>
95
96
  <div class="timeline-view-actions">
96
- <button type="button" class="icon-button" data-zoom-out aria-label="Zoom out" title="Zoom out">${icon("minus")}</button>
97
- <button type="button" class="icon-button" data-zoom-fit aria-label="Fit timeline" title="Fit timeline">${icon("fit")}</button>
98
- <button type="button" class="icon-button" data-zoom-in aria-label="Zoom in" title="Zoom in">${icon("plus")}</button>
97
+ <button type="button" class="icon-button" data-zoom-out ${uiAttribute("aria-label", "timeline.zoom-out")} ${uiAttribute("title", "timeline.zoom-out")}>${icon("minus")}</button>
98
+ <button type="button" class="icon-button" data-zoom-fit ${uiAttribute("aria-label", "timeline.fit-timeline")} ${uiAttribute("title", "timeline.fit-timeline")}>${icon("fit")}</button>
99
+ <button type="button" class="icon-button" data-zoom-in ${uiAttribute("aria-label", "timeline.zoom-in")} ${uiAttribute("title", "timeline.zoom-in")}>${icon("plus")}</button>
99
100
  </div>
100
101
  </div>
101
102
  <div class="timeline-body" data-timeline-body>
@@ -138,8 +139,8 @@ export function createTimeline(store: Store): Timeline {
138
139
  remember: "hypit-studio.v4.timeline-label-width",
139
140
  });
140
141
  labelHandle.classList.add("timeline-label-handle");
141
- labelHandle.setAttribute("aria-label", "Resize timeline track labels");
142
- labelHandle.title = "Resize track labels";
142
+ uiAttr(labelHandle, "aria-label", "timeline.resize-timeline-track-labels");
143
+ uiAttr(labelHandle, "title", "timeline.resize-track-labels");
143
144
  body.insertBefore(labelHandle, lanes);
144
145
 
145
146
  let state: State | undefined;
@@ -501,12 +502,14 @@ export function createTimeline(store: Store): Timeline {
501
502
  ];
502
503
  const laneHeight = bands.reduce((height, band) => height + band.height, itemMetrics.insetYPx * 2);
503
504
  const label = createTrackLabel(
504
- presentation.label ?? "Timeline",
505
+ presentation.label ?? "",
505
506
  presentation.tone,
506
507
  presentation.icon,
507
- `${snapshot.semantic.segments.length} segment${snapshot.semantic.segments.length === 1 ? "" : "s"}`,
508
+ "",
508
509
  laneHeight,
509
510
  );
511
+ if (presentation.label === undefined) uiText(label.querySelector("strong")!, "inspector.timeline");
512
+ uiAttr(label, "title", "timeline.segment-count", { count: snapshot.semantic.segments.length });
510
513
  label.classList.add("track-label-semantic");
511
514
  label.style.height = `calc(var(--timeline-ruler-height) + ${laneHeight}px)`;
512
515
  labels.append(label);
@@ -529,7 +532,7 @@ export function createTimeline(store: Store): Timeline {
529
532
  band.className = `semantic-band semantic-band-${kind}`;
530
533
  band.style.top = `${bandTop}px`;
531
534
  band.style.setProperty("--semantic-band-height", `${height}px`);
532
- band.setAttribute("aria-label", kind === "intent" ? "Selections and Moments" : kind === "word" ? "Words" : "Segments");
535
+ uiAttr(band, "aria-label", kind === "intent" ? "timeline.selections-and-moments" : kind === "word" ? "timeline.words" : "timeline.segments");
533
536
  lane.append(band);
534
537
  bandTop += height;
535
538
  }
@@ -546,7 +549,7 @@ export function createTimeline(store: Store): Timeline {
546
549
  node.className = "semantic-cell semantic-segment";
547
550
  node.tabIndex = 0;
548
551
  node.setAttribute("role", "button");
549
- node.setAttribute("aria-label", `Segment ${segment.id}`);
552
+ uiAttr(node, "aria-label", "timeline.segment-name", { name: segment.id });
550
553
  node.dataset.semanticSegment = segment.id;
551
554
  node.style.left = `${from * 100}%`;
552
555
  const segmentWidth = Math.max(0, to - from) * 100;
@@ -626,7 +629,7 @@ export function createTimeline(store: Store): Timeline {
626
629
  node.type = "button";
627
630
  node.className = "semantic-cell semantic-selection";
628
631
  node.tabIndex = 0;
629
- node.setAttribute("aria-label", `Selection ${selection.id}`);
632
+ uiAttr(node, "aria-label", "timeline.selection-name", { name: selection.id });
630
633
  node.style.left = `${from * 100}%`;
631
634
  node.style.width = `max(2px, ${Math.max(0, to - from) * 100}%)`;
632
635
  node.title = selection.id;
@@ -652,7 +655,7 @@ export function createTimeline(store: Store): Timeline {
652
655
  node.type = "button";
653
656
  node.className = "semantic-moment";
654
657
  node.tabIndex = 0;
655
- node.setAttribute("aria-label", `Moment ${moment.id}`);
658
+ uiAttr(node, "aria-label", "timeline.moment-name", { name: moment.id });
656
659
  node.style.left = `${at * 100}%`;
657
660
  node.title = moment.id;
658
661
  node.addEventListener("click", (event) => {
@@ -674,7 +677,7 @@ export function createTimeline(store: Store): Timeline {
674
677
  const menu = document.createElement("div");
675
678
  menu.className = "timeline-overlap-menu";
676
679
  menu.setAttribute("role", "menu");
677
- menu.setAttribute("aria-label", "Overlapping timeline objects");
680
+ uiAttr(menu, "aria-label", "timeline.overlapping-timeline-objects");
678
681
  for (const hit of [...hits].reverse()) {
679
682
  const option = document.createElement("button");
680
683
  option.type = "button";
@@ -835,7 +838,7 @@ export function createTimeline(store: Store): Timeline {
835
838
  semanticNodes = [];
836
839
  const corner = document.createElement("div");
837
840
  corner.className = "label-corner";
838
- corner.textContent = "";
841
+ userText(corner, "");
839
842
  if (snapshot.semantic === undefined || snapshot.semantic.segments.length === 0) labels.append(corner);
840
843
  buildSemanticLane(snapshot);
841
844
  const nextClipNodes: { node: HTMLElement; start: number; end: number; id: string; selectionGroup?: string }[] = [];
@@ -10,6 +10,14 @@ Open a Run in the browser to inspect its composition, Sources and Results.
10
10
 
11
11
  hypit studio --run <build.svrun> [--runtime <hypit.runtime.json>]
12
12
  [--port <number>] [--workspace <directory>] [--package-root <directory>]
13
+ [--locale-pack <./language.json | installed-package/language.json>]...
14
+
15
+ hypit studio --check-locale <./language.json | installed-package/language.json>
16
+ [--package-root <directory>]
17
+
18
+ Language packs are explicit JSON data. Relative files use the current directory;
19
+ package exports resolve from --package-root (the project by default).
20
+ --check-locale lists missing translations and unknown IDs without opening a Run.
13
21
 
14
22
  Relative command-line paths start at the current directory; --workspace selects
15
23
  the project, without rebasing those paths. Otherwise the nearest package.json
@@ -24,9 +32,9 @@ function invalidArguments(message: string): never {
24
32
  throw new Error(`${message}. See hypit studio --help.`);
25
33
  }
26
34
 
27
- function argumentsByName(argv: readonly string[]): ReadonlyMap<string, string> {
28
- const accepted = new Set(["run", "runtime", "port", "workspace", "package-root"]);
29
- const result = new Map<string, string>();
35
+ function argumentsByName(argv: readonly string[]): ReadonlyMap<string, readonly string[]> {
36
+ const accepted = new Set(["run", "runtime", "port", "workspace", "package-root", "locale-pack", "check-locale"]);
37
+ const result = new Map<string, string[]>();
30
38
  for (let index = 0; index < argv.length; index += 2) {
31
39
  const flag = argv[index];
32
40
  const value = argv[index + 1];
@@ -35,7 +43,7 @@ function argumentsByName(argv: readonly string[]): ReadonlyMap<string, string> {
35
43
  }
36
44
  const name = flag.slice(2);
37
45
  if (!accepted.has(name)) invalidArguments(`Unknown option --${name}`);
38
- result.set(name, value);
46
+ result.set(name, [...result.get(name) ?? [], value]);
39
47
  }
40
48
  return result;
41
49
  }
@@ -47,22 +55,12 @@ export async function runStudio(argv: readonly string[], io: Pick<CliIo, "write"
47
55
  }
48
56
  const values = argumentsByName(argv[0] === "--" ? argv.slice(1) : argv);
49
57
  const invokedFrom = process.env.INIT_CWD ?? process.cwd();
50
- const runArgument = values.get("run");
51
- if (runArgument === undefined || runArgument.trim().length === 0) invalidArguments("Missing --run");
52
- const { createServer } = await import("vite");
58
+ const runArgument = values.get("run")?.at(-1);
59
+
53
60
  const { findRuntimeProfile, resolveProjectRoot } = await import("@hypit/project-context-node");
54
- const { resolveDistributionPackageImport } = await import("@hypit/package-loader-node");
55
- const { videoCliDistribution, videoStudioCompanionPackages } = await import("@hypit/video-cli");
56
- const { openStudioBuildLibrary } = await import("./src/build-library.js");
57
- const { loadStudioCompanionRegistry } = await import("./src/companion-assembly.js");
58
- const { loadStudioDomain } = await import("./src/domain.js");
59
- const { loadStudioRun } = await import("./src/run.js");
60
- const { studioPlugin } = await import("./src/server.js");
61
- const { studioFeedbackPlugin } = await import("./src/feedback-server.js");
62
- const { inspectStudioRun } = await import("./src/studio-preflight.js");
63
- const runPath = resolve(invokedFrom, runArgument);
64
- const packageRootArgument = values.get("package-root");
65
- const workspaceArgument = values.get("workspace");
61
+
62
+ const packageRootArgument = values.get("package-root")?.at(-1);
63
+ const workspaceArgument = values.get("workspace")?.at(-1);
66
64
  const requestedWorkspaceRoot = workspaceArgument === undefined
67
65
  ? undefined
68
66
  : resolve(invokedFrom, workspaceArgument);
@@ -73,7 +71,26 @@ export async function runStudio(argv: readonly string[], io: Pick<CliIo, "write"
73
71
  const packageRoot = packageRootArgument === undefined
74
72
  ? workspaceRoot
75
73
  : resolve(invokedFrom, packageRootArgument);
76
- const runtimeArgument = values.get("runtime");
74
+ const { describeLanguagePack, loadLanguagePack, studioLanguages, studioLocalizationPlugin } = await import("./src/localization-node.js");
75
+ const checkLocale = values.get("check-locale")?.at(-1);
76
+ if (checkLocale !== undefined) {
77
+ io.write(describeLanguagePack(await loadLanguagePack(checkLocale, invokedFrom, packageRoot)) + "\n");
78
+ return;
79
+ }
80
+ if (runArgument === undefined || runArgument.trim().length === 0) invalidArguments("Missing --run");
81
+ const runPath = resolve(invokedFrom, runArgument);
82
+ const { createServer } = await import("vite");
83
+ const { resolveDistributionPackageImport } = await import("@hypit/package-loader-node");
84
+ const { videoCliDistribution, videoStudioCompanionPackages } = await import("@hypit/video-cli");
85
+ const { openStudioBuildLibrary } = await import("./src/build-library.js");
86
+ const { loadStudioCompanionRegistry } = await import("./src/companion-assembly.js");
87
+ const { loadStudioDomain } = await import("./src/domain.js");
88
+ const { loadStudioRun } = await import("./src/run.js");
89
+ const { studioPlugin } = await import("./src/server.js");
90
+ const { studioFeedbackPlugin } = await import("./src/feedback-server.js");
91
+ const { inspectStudioRun } = await import("./src/studio-preflight.js");
92
+ const languages = await studioLanguages(values.get("locale-pack") ?? [], invokedFrom, packageRoot);
93
+ const runtimeArgument = values.get("runtime")?.at(-1);
77
94
  const selectedRuntime = runtimeArgument === undefined
78
95
  ? await findRuntimeProfile(workspaceRoot)
79
96
  : undefined;
@@ -89,7 +106,7 @@ export async function runStudio(argv: readonly string[], io: Pick<CliIo, "write"
89
106
  ...(packageRoot === workspaceRoot ? [] : [` Package root ${packageRoot}`]),
90
107
  "",
91
108
  ].join("\n"));
92
- const port = Number(values.get("port") ?? "5179");
109
+ const port = Number(values.get("port")?.at(-1) ?? "5179");
93
110
  if (!Number.isSafeInteger(port) || port <= 0) invalidArguments("--port must be a positive integer");
94
111
 
95
112
  const distributionPackageRoot = videoCliDistribution.packageRoot ?? resolve(here, "../..");
@@ -137,7 +154,7 @@ export async function runStudio(argv: readonly string[], io: Pick<CliIo, "write"
137
154
  // available for Source and material previews.
138
155
  fs: { allow: [workspaceRoot, packageRoot, distributionPackageRoot, here] },
139
156
  },
140
- plugins: [distributionImports, studioFeedbackPlugin(workspaceRoot, runPath), studioPlugin({
157
+ plugins: [distributionImports, studioLocalizationPlugin(languages), studioFeedbackPlugin(workspaceRoot, runPath), studioPlugin({
141
158
  source,
142
159
  runPath,
143
160
  workspaceRoot,