@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
@@ -0,0 +1,191 @@
1
+ {
2
+ "locale": "zh-CN",
3
+ "name": "简体中文",
4
+ "direction": "ltr",
5
+ "messages": {
6
+ "app.comments": "评论",
7
+ "app.language": "语言",
8
+ "app.studio": "工作台",
9
+ "app.studio-view": "工作区视图",
10
+ "comments.all": "全部",
11
+ "comments.caught-up": "评论都已处理",
12
+ "comments.delete-comment": "删除评论",
13
+ "comments.done": "已完成",
14
+ "comments.draw-soon": "批注 · 即将支持",
15
+ "comments.edit-comment": "编辑评论",
16
+ "comments.edit-comment-text": "编辑评论内容",
17
+ "comments.emoji-soon": "表情 · 即将支持",
18
+ "comments.empty": "留下第一条评论",
19
+ "comments.empty-hint": "暂停在想调整的画面,说说你的想法。",
20
+ "comments.filter-comments": "筛选评论",
21
+ "comments.filter-hint": "切换筛选,查看其他评论。",
22
+ "comments.go-to-this-frame": "跳转到这一帧",
23
+ "comments.loading-comments": "加载评论中…",
24
+ "comments.mark-complete": "标为已完成",
25
+ "comments.no-completed-comments": "暂无已完成评论",
26
+ "comments.open": "待处理",
27
+ "comments.placeholder": "想调整哪里?",
28
+ "comments.read-failed": "无法读取评论。",
29
+ "comments.refresh-comments": "刷新评论",
30
+ "comments.reopen-comment": "标为待处理",
31
+ "comments.save-failed": "无法保存评论。",
32
+ "comments.send-comment": "发送评论",
33
+ "comments.send-shortcut": "发送评论(Enter)",
34
+ "comments.use-current-frame": "使用当前帧",
35
+ "comments.write-a-comment": "写评论",
36
+ "common.cancel": "取消",
37
+ "common.cancelled": "已取消",
38
+ "common.compile-failed": "编译失败",
39
+ "common.completed": "已完成",
40
+ "common.copied": "已复制",
41
+ "common.failed": "失败",
42
+ "common.invalid-value": "无效的值",
43
+ "common.needs-attention": "需要处理",
44
+ "common.queued": "排队中",
45
+ "common.refresh": "刷新",
46
+ "common.running": "运行中",
47
+ "common.save": "保存",
48
+ "common.save-failed": "保存失败",
49
+ "common.saved": "已保存",
50
+ "common.saving": "保存中",
51
+ "common.waiting": "等待中",
52
+ "inspector.add-color": "添加颜色",
53
+ "inspector.add-item": "添加条目",
54
+ "inspector.anchor.program-end": "时间线终点{detail}",
55
+ "inspector.anchor.program-start": "时间线起点{detail}",
56
+ "inspector.anchor.segment-end": "段尾{detail}",
57
+ "inspector.anchor.segment-start": "段头{detail}",
58
+ "inspector.anchor.token-end": "词尾{detail}",
59
+ "inspector.anchor.token-start": "词头{detail}",
60
+ "inspector.aspect-ratio": "画幅比例",
61
+ "inspector.author": "源文件",
62
+ "inspector.build": "构建",
63
+ "inspector.candidates": "候选实现",
64
+ "inspector.canvas": "画布",
65
+ "inspector.duration": "时长",
66
+ "inspector.empty": "当前选中项没有详细信息",
67
+ "inspector.end": "终点",
68
+ "inspector.end-anchor": "终点语义锚点",
69
+ "inspector.frame-rate": "帧率",
70
+ "inspector.frames": "帧数",
71
+ "inspector.how": "样式",
72
+ "inspector.invalid-fields": "请正确填写必填项后保存。",
73
+ "inspector.item-count": "{count} 项",
74
+ "inspector.moment": "时刻",
75
+ "inspector.moment-anchor": "时刻语义锚点",
76
+ "inspector.move-down": "下移",
77
+ "inspector.move-up": "上移",
78
+ "inspector.project": "项目",
79
+ "inspector.properties": "属性",
80
+ "inspector.remove-item": "移除条目",
81
+ "inspector.resolution": "分辨率",
82
+ "inspector.run": "运行方案",
83
+ "inspector.semantic-anchors": "语义锚点",
84
+ "inspector.sources": "引用文件",
85
+ "inspector.start": "起点",
86
+ "inspector.start-anchor": "起点语义锚点",
87
+ "inspector.structured-value": "结构化值",
88
+ "inspector.targets": "目标产物",
89
+ "inspector.timeline": "时间线",
90
+ "inspector.timing": "时间",
91
+ "inspector.tracks": "轨道",
92
+ "inspector.use-color": "使用 {color}",
93
+ "inspector.when": "时间",
94
+ "inspector.where": "位置",
95
+ "library.all-media": "全部素材",
96
+ "library.all-tasks": "全部任务",
97
+ "library.artifacts": "产物",
98
+ "library.audio": "音频",
99
+ "library.clear-task-filter": "清除任务筛选",
100
+ "library.data": "数据",
101
+ "library.display-name": "显示名称",
102
+ "library.enter-name": "请输入名称。",
103
+ "library.finished": "已结束",
104
+ "library.image": "图片",
105
+ "library.images": "图片",
106
+ "library.in-progress": "进行中",
107
+ "library.loading-media": "加载素材中…",
108
+ "library.loading-more": "加载更多…",
109
+ "library.loading-tasks": "加载任务中…",
110
+ "library.media-categories": "素材分类",
111
+ "library.media-category": "素材分类",
112
+ "library.media-hint": "这里展示已产出的图片、视频和音频,也包括正在构建的任务产物。",
113
+ "library.name-failed": "无法保存名称",
114
+ "library.name-pending": "{name}\n构建完成后可以重命名",
115
+ "library.no-files-in-this-category": "此分类暂无素材",
116
+ "library.no-media": "暂无素材",
117
+ "library.no-tasks-here": "暂无任务",
118
+ "library.no-tasks-in-progress": "没有进行中的任务",
119
+ "library.preview-name": "预览 {name}",
120
+ "library.preview-unavailable": "暂时无法预览",
121
+ "library.referenced-source-files": "引用的源文件",
122
+ "library.refresh-failed": "刷新失败:{detail}",
123
+ "library.refresh-failed-retained": "刷新失败:{detail} · 已保留上次视图",
124
+ "library.refresh-interrupted": "刷新已中断",
125
+ "library.refresh-media": "刷新素材",
126
+ "library.refresh-tasks": "刷新任务",
127
+ "library.refreshing": "刷新中…",
128
+ "library.rename-hint": "{name}\n双击或按 F2 重命名",
129
+ "library.return-to-task": "返回任务",
130
+ "library.source": "源文件",
131
+ "library.studio-library": "项目资源",
132
+ "library.task-categories": "任务分类",
133
+ "library.task-return": "{name} · 返回任务",
134
+ "library.tasks": "任务",
135
+ "library.unavailable": "资源暂时不可用",
136
+ "library.updated": "更新于 {time}",
137
+ "library.video": "视频",
138
+ "library.video-loading-preview": "视频 · 加载预览中",
139
+ "library.videos": "视频",
140
+ "library.view-media": "查看产物",
141
+ "library.view-task-media": "查看任务产物",
142
+ "player.audio-waveform": "音频波形",
143
+ "player.back-5-seconds": "后退 5 秒",
144
+ "player.back-to-composition": "返回作品",
145
+ "player.composition-time": "作品播放进度",
146
+ "player.elements-at-this-point": "此处的元素",
147
+ "player.forward-5-seconds": "前进 5 秒",
148
+ "player.frame-loading": "加载画面中…",
149
+ "player.frame-unavailable": "画面暂时不可用",
150
+ "player.hovered-frame": "悬停画面",
151
+ "player.media-failed": "浏览器无法打开此素材。",
152
+ "player.media-time": "素材播放进度",
153
+ "player.mute": "静音",
154
+ "player.next-frame": "下一帧",
155
+ "player.pause": "暂停",
156
+ "player.play": "播放",
157
+ "player.preview": "预览",
158
+ "player.previous-frame": "上一帧",
159
+ "player.unmute": "取消静音",
160
+ "source.changed-outside-studio": "文件已在外部更改",
161
+ "source.edit-source": "编辑源文件",
162
+ "source.keep-lines-unwrapped": "不自动换行",
163
+ "source.read-source": "查看源文件",
164
+ "source.save-failed-status": "保存失败({status})",
165
+ "source.saving-before-switch": "切换前保存中",
166
+ "source.svml-source": "SVML 源文件",
167
+ "source.unsaved": "未保存",
168
+ "source.wrap-lines": "自动换行",
169
+ "tasks.copy-error": "复制错误信息",
170
+ "tasks.copy-manually": "请选择文字复制",
171
+ "tasks.no-runtime": "尚未选择运行环境,可在「已结束」中查看保存的任务。",
172
+ "tasks.progress": "已完成 {completed}/{total} 步",
173
+ "tasks.refresh-hint": "点击刷新,查看最新任务状态。",
174
+ "tasks.saved-only": "项目中保存的结果 · 未选择运行环境",
175
+ "timeline.fit-timeline": "适配时间线",
176
+ "timeline.moment-name": "时刻 {name}",
177
+ "timeline.overlapping-timeline-objects": "重叠的时间线对象",
178
+ "timeline.resize-timeline-track-labels": "调整轨道名称区宽度",
179
+ "timeline.resize-track-labels": "调整轨道名称区宽度",
180
+ "timeline.segment-name": "片段 {name}",
181
+ "timeline.segments": "片段",
182
+ "timeline.select": "选择",
183
+ "timeline.selection-name": "区间 {name}",
184
+ "timeline.selections-and-moments": "区间与时刻",
185
+ "timeline.words": "词语",
186
+ "timeline.zoom-in": "放大",
187
+ "timeline.zoom-out": "缩小",
188
+ "inspector.color-picker": "{label}取色器",
189
+ "timeline.segment-count": "{count} 个片段"
190
+ }
191
+ }
@@ -0,0 +1,46 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import type { Plugin } from "vite";
5
+ import { builtinLanguages, languageCoverage, readLanguagePack } from "./localization.js";
6
+ import type { LanguagePack } from "./localization.js";
7
+
8
+ export async function loadLanguagePack(specifier: string, cwd: string, packageRoot: string): Promise<LanguagePack> {
9
+ const path = isAbsolute(specifier) || specifier.startsWith(".")
10
+ ? resolve(cwd, specifier)
11
+ : createRequire(resolve(packageRoot, "package.json")).resolve(specifier);
12
+ try { return readLanguagePack(JSON.parse(await readFile(path, "utf8"))); }
13
+ catch (error) { throw new Error(`Language pack ${specifier}: ${error instanceof Error ? error.message : String(error)}`); }
14
+ }
15
+
16
+ export function describeLanguagePack(pack: LanguagePack): string {
17
+ const { missing, unknown } = languageCoverage(pack);
18
+ return [`${pack.name} (${pack.locale})`,
19
+ `Missing translations (${missing.length}; English is used): ${missing.join(", ") || "none"}`,
20
+ `Unknown message IDs (${unknown.length}): ${unknown.join(", ") || "none"}`,
21
+ ].join("\n");
22
+ }
23
+
24
+ export async function studioLanguages(specifiers: readonly string[], cwd: string, packageRoot: string): Promise<readonly LanguagePack[]> {
25
+ const packs = new Map(builtinLanguages.map(pack => [pack.locale, pack]));
26
+ for (const specifier of specifiers) {
27
+ const pack = await loadLanguagePack(specifier, cwd, packageRoot);
28
+ packs.set(pack.locale, pack);
29
+ }
30
+ return [...packs.values()];
31
+ }
32
+
33
+ export function studioLocalizationPlugin(packs: readonly LanguagePack[]): Plugin {
34
+ return {
35
+ name: "hypit-studio-localization",
36
+ configureServer(server) {
37
+ server.middlewares.use((request, response, next) => {
38
+ if (new URL(request.url ?? "/", "http://localhost").pathname !== "/__studio/locales") return next();
39
+ if (request.method !== "GET") { response.statusCode = 405; response.end(); return; }
40
+ response.setHeader("content-type", "application/json; charset=utf-8");
41
+ response.setHeader("cache-control", "no-store");
42
+ response.end(JSON.stringify(packs));
43
+ });
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,85 @@
1
+ import english from "../locales/en.json" with { type: "json" };
2
+ import chinese from "../locales/zh-CN.json" with { type: "json" };
3
+
4
+ export type Message = keyof typeof english.messages;
5
+ export type MessageValue = string | ({ readonly other: string } & Partial<Record<Intl.LDMLPluralRule, string>>);
6
+ export type LanguagePack = {
7
+ readonly locale: string;
8
+ /** Native language name, displayed without translation. */
9
+ readonly name: string;
10
+ readonly direction: "ltr" | "rtl";
11
+ readonly messages: Readonly<Record<string, MessageValue>>;
12
+ };
13
+ export const defaultLanguage: LanguagePack = english as LanguagePack;
14
+ export const builtinLanguages: readonly LanguagePack[] = [defaultLanguage, chinese as LanguagePack];
15
+ export type MessageValues = Readonly<Record<string, string | number>>;
16
+ const plurals = new Set(["zero", "one", "two", "few", "many", "other"]);
17
+ const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
18
+ const variants = (message: MessageValue): readonly string[] => typeof message === "string" ? [message] : Object.values(message);
19
+ const variables = (text: string): string[] => [...new Set([...text.matchAll(/\{(\w+)\}/gu)].map(match => match[1]!))].sort();
20
+
21
+ /** Read data, never executable translation code. Partial translations remain useful. */
22
+ export function readLanguagePack(value: unknown): LanguagePack {
23
+ if (!record(value) || typeof value.locale !== "string" || typeof value.name !== "string" || !value.name.trim()
24
+ || (value.direction !== "ltr" && value.direction !== "rtl") || !record(value.messages)) {
25
+ throw new Error("A Studio language pack needs locale, name, direction (ltr/rtl) and messages.");
26
+ }
27
+ const locale = Intl.getCanonicalLocales(value.locale)[0];
28
+ if (!locale) throw new Error("A Studio language pack needs a BCP 47 locale.");
29
+ const messages: Record<string, MessageValue> = {};
30
+ for (const [id, message] of Object.entries(value.messages)) {
31
+ if (typeof message !== "string" && (!record(message) || typeof message.other !== "string"
32
+ || Object.entries(message).some(([form, text]) => !plurals.has(form) || typeof text !== "string"))) {
33
+ throw new Error(`${id}: expected text or plural forms with an 'other' form.`);
34
+ }
35
+ const translated = message as MessageValue;
36
+ const original = Object.hasOwn(defaultLanguage.messages, id) ? defaultLanguage.messages[id] : undefined;
37
+ if (original !== undefined) {
38
+ const expected = variables(variants(original)[0]!).join(",");
39
+ for (const text of variants(translated)) {
40
+ if (variables(text).join(",") !== expected) throw new Error(`${id}: preserve the named variables {${expected.split(",").join("}, {")}}.`);
41
+ }
42
+ if (typeof translated !== "string" && !variables(variants(original)[0]!).includes("count")) {
43
+ throw new Error(`${id}: plural forms require a count variable.`);
44
+ }
45
+ }
46
+ messages[id] = translated;
47
+ }
48
+ return { locale, name: value.name, direction: value.direction, messages };
49
+ }
50
+
51
+ export function languageCoverage(pack: LanguagePack): { missing: string[]; unknown: string[] } {
52
+ return {
53
+ missing: Object.keys(defaultLanguage.messages).filter(id => !Object.hasOwn(pack.messages, id)),
54
+ unknown: Object.keys(pack.messages).filter(id => !Object.hasOwn(defaultLanguage.messages, id)),
55
+ };
56
+ }
57
+
58
+ export function chooseLanguage(packs: readonly LanguagePack[], preferences: readonly string[]): LanguagePack {
59
+ for (const preference of preferences) {
60
+ let locale: string;
61
+ try { locale = Intl.getCanonicalLocales(preference)[0]!; } catch { continue; }
62
+ const exact = packs.find(pack => pack.locale === locale);
63
+ if (exact) return exact;
64
+ // Match regional/script variants without a list of special language cases.
65
+ const requested = new Intl.Locale(locale).maximize();
66
+ const related = packs.find(pack => {
67
+ const candidate = new Intl.Locale(pack.locale).maximize();
68
+ return candidate.language === requested.language && candidate.script === requested.script;
69
+ });
70
+ if (related) return related;
71
+ }
72
+ return packs.find(pack => pack.locale === defaultLanguage.locale) ?? defaultLanguage;
73
+ }
74
+
75
+ export function formatMessage(pack: LanguagePack, id: Message, values: MessageValues = {}): string {
76
+ const translated = pack.messages[id];
77
+ const message = translated ?? defaultLanguage.messages[id]!;
78
+ const locale = translated === undefined ? defaultLanguage.locale : pack.locale;
79
+ const template = typeof message === "string" ? message
80
+ : message[new Intl.PluralRules(locale).select(Number(values.count))] ?? message.other;
81
+ return template.replace(/\{(\w+)\}/gu, (match, key: string) => {
82
+ const value = values[key];
83
+ return typeof value === "number" ? new Intl.NumberFormat(pack.locale).format(value) : value ?? match;
84
+ });
85
+ }
@@ -142,7 +142,7 @@ button:focus-visible, textarea:focus-visible {
142
142
  white-space: nowrap;
143
143
  }
144
144
  .topbar-right { justify-self: end; display: flex; align-items: center; min-width: 0; }
145
- .view-tabs { display: flex; align-items: center; gap: 2px; margin-left: 16px; padding: 2px; border-radius: 5px; background: var(--subtab-surface); }
145
+ .view-tabs { display: flex; flex: 0 0 auto; white-space: nowrap; align-items: center; gap: 2px; margin-left: 16px; padding: 2px; border-radius: 5px; background: var(--subtab-surface); }
146
146
  .view-tabs button {
147
147
  display: flex; align-items: center; gap: 7px; height: 24px; padding: 0 10px;
148
148
  border: 0; border-radius: 4px; color: var(--text-3); background: transparent;
@@ -150,6 +150,18 @@ button:focus-visible, textarea:focus-visible {
150
150
  .view-tabs button[aria-selected="true"] { color: var(--subtab-active-text); background: var(--subtab-active-surface); }
151
151
  .view-tabs button:focus-visible { outline: 1px solid #8a8f99; outline-offset: -1px; }
152
152
  .view-tabs .icon { width: 14px; height: 14px; }
153
+ .parameter-select.language-select { width: auto; flex: 0 0 auto; margin-left: 10px; }
154
+ .language-select .language-toggle {
155
+ display: grid; place-items: center; grid-template-columns: 1fr;
156
+ width: 28px; height: 28px; padding: 0;
157
+ color: var(--subtab-active-text); background: var(--subtab-surface);
158
+ border-color: var(--line);
159
+ }
160
+ .language-toggle .icon { width: 16px; height: 16px; }
161
+ .language-select .parameter-select-menu { width: max-content; min-width: 150px; max-width: 240px; }
162
+ .language-select .parameter-select-option { font-family: var(--font-ui); text-align: start; }
163
+ .topbar { position: relative; z-index: 40; }
164
+
153
165
  .meta {
154
166
  color: var(--text-3);
155
167
  font: 500 11px/1 var(--font-ui);
@@ -1560,8 +1572,8 @@ button:focus-visible, textarea:focus-visible {
1560
1572
  .failure:empty { display: none; }
1561
1573
 
1562
1574
  @media (max-width: 920px) {
1563
- .meta, .topbar-right { display: none; }
1564
- .topbar { grid-template-columns: 1fr minmax(120px, 1fr); }
1575
+ .meta, .topbar-right .status { display: none; }
1576
+ .topbar { grid-template-columns: auto minmax(0, 1fr) auto; }
1565
1577
  .upper-shell { grid-template-columns: minmax(320px, var(--source-width)) 8px minmax(300px, 1fr); }
1566
1578
  .workspace-handle, .workspace-panel { display: none; }
1567
1579
  }
@@ -1,3 +1,5 @@
1
+ import { uiAttr, uiText, userText } from "./i18n.js";
2
+
1
3
  /** Two-line label with an expanding editor; the caller owns persistence. */
2
4
  export function createArtifactName(options: {
3
5
  readonly name: string;
@@ -15,7 +17,7 @@ export function createArtifactName(options: {
15
17
  text.className = "artifact-name-text";
16
18
  text.textContent = name;
17
19
  label.append(text);
18
- label.title = options.editable ? `${name}\nDouble-click or press F2 to rename` : `${name}\nNaming is available when this Build finishes`;
20
+ uiAttr(label, "title", options.editable ? "library.rename-hint" : "library.name-pending", { name });
19
21
  element.append(label);
20
22
  label.addEventListener("click", (event) => { event.stopPropagation(); options.select(); });
21
23
  let editing = false;
@@ -30,7 +32,7 @@ export function createArtifactName(options: {
30
32
  field.className = "artifact-name-editor";
31
33
  field.value = name;
32
34
  field.rows = 1;
33
- field.setAttribute("aria-label", "Display name");
35
+ uiAttr(field, "aria-label", "library.display-name");
34
36
  const error = document.createElement("span");
35
37
  error.className = "artifact-name-error";
36
38
  error.setAttribute("role", "status");
@@ -53,17 +55,17 @@ export function createArtifactName(options: {
53
55
  if (!editing || saving) return;
54
56
  const value = field.value.trim();
55
57
  if (value === name) { close(); return; }
56
- if (value.length === 0) { error.textContent = "Enter a name."; return; }
58
+ if (value.length === 0) { uiText(error, "library.enter-name"); return; }
57
59
  saving = true;
58
60
  field.readOnly = true;
59
- error.textContent = "Saving…";
61
+ uiText(error, "common.saving");
60
62
  try {
61
63
  name = await options.save(value);
62
64
  text.textContent = name;
63
- label.title = `${name}\nDouble-click or press F2 to rename`;
65
+ uiAttr(label, "title", "library.rename-hint", { name });
64
66
  close();
65
67
  } catch (cause) {
66
- error.textContent = cause instanceof Error ? cause.message : String(cause);
68
+ userText(error, cause instanceof Error ? cause.message : String(cause));
67
69
  } finally {
68
70
  saving = false;
69
71
  field.readOnly = false;
@@ -1,3 +1,4 @@
1
+ import { uiAttr, uiText, userText } from "./i18n.js";
1
2
  import type { StudioArtifactView } from "../shared.js";
2
3
  import { audioPreview } from "./material-preview.js";
3
4
 
@@ -35,7 +36,7 @@ export function createArtifactPreview(changed: () => void) {
35
36
  error.setAttribute("role", "status");
36
37
  const failed = () => {
37
38
  if (generation !== current) return;
38
- error.textContent = "This media could not be opened in the browser.";
39
+ uiText(error, "player.media-failed");
39
40
  error.hidden = false;
40
41
  changed();
41
42
  };
@@ -61,14 +62,14 @@ export function createArtifactPreview(changed: () => void) {
61
62
  if (audio) {
62
63
  const wave = document.createElement("div");
63
64
  wave.className = "stage-audio-wave";
64
- wave.textContent = "Audio";
65
+ uiText(wave, "library.audio");
65
66
  element.append(wave);
66
67
  void audioPreview(url).then((waveform) => {
67
68
  if (generation !== current || waveform === undefined) return;
68
69
  const image = document.createElement("img");
69
70
  image.src = waveform;
70
- image.alt = "Audio waveform";
71
- wave.replaceChildren(image);
71
+ uiAttr(image, "alt", "player.audio-waveform");
72
+ userText(wave, ""); wave.replaceChildren(image);
72
73
  });
73
74
  }
74
75
  }
@@ -1,3 +1,4 @@
1
+ import { t, userText, uiAttribute, uiText, uiAttr } from "./i18n.js";
1
2
  import type { Range, StudioSnapshot, StudioSourceView } from "../shared.js";
2
3
  import { icon, setIcon } from "./icons.js";
3
4
  import { intentTones } from "./markers.js";
@@ -77,10 +78,10 @@ export function createCodePane(): CodePane {
77
78
  <strong class="code-location" data-path></strong>
78
79
  <div class="code-actions">
79
80
  <span class="code-save-state" data-save-state></span>
80
- <button type="button" class="icon-button code-wrap" data-wrap aria-label="Wrap lines" aria-pressed="false" title="Wrap lines">
81
+ <button type="button" class="icon-button code-wrap" data-wrap ${uiAttribute("aria-label", "source.wrap-lines")} aria-pressed="false" ${uiAttribute("title", "source.wrap-lines")}>
81
82
  ${icon("wrap")}
82
83
  </button>
83
- <button type="button" class="icon-button code-mode" data-mode aria-label="Edit source" title="Edit source">
84
+ <button type="button" class="icon-button code-mode" data-mode ${uiAttribute("aria-label", "source.edit-source")} ${uiAttribute("title", "source.edit-source")}>
84
85
  <span data-mode-icon>${icon("edit")}</span>
85
86
  </button>
86
87
  </div>`;
@@ -88,7 +89,7 @@ export function createCodePane(): CodePane {
88
89
  element.className = "code";
89
90
  element.innerHTML = `
90
91
  <div class="code-scroll"><svg class="range-canvas" aria-hidden="true"></svg></div>
91
- <textarea class="code-editor" data-editor spellcheck="false" aria-label="SVML source"></textarea>`;
92
+ <textarea class="code-editor" data-editor spellcheck="false" ${uiAttribute("aria-label", "source.svml-source")}></textarea>`;
92
93
  const path = toolbar.querySelector<HTMLElement>("[data-path]")!;
93
94
  const scroll = element.querySelector<HTMLElement>(".code-scroll")!;
94
95
  const canvas = element.querySelector<SVGSVGElement>(".range-canvas")!;
@@ -116,8 +117,8 @@ export function createCodePane(): CodePane {
116
117
  wrapped = value;
117
118
  element.classList.toggle("is-wrapped", wrapped);
118
119
  wrap.setAttribute("aria-pressed", String(wrapped));
119
- wrap.setAttribute("aria-label", wrapped ? "Keep lines unwrapped" : "Wrap lines");
120
- wrap.title = wrapped ? "Keep lines unwrapped" : "Wrap lines";
120
+ uiAttr(wrap, "aria-label", wrapped ? "source.keep-lines-unwrapped" : "source.wrap-lines");
121
+ uiAttr(wrap, "title", wrapped ? "source.keep-lines-unwrapped" : "source.wrap-lines");
121
122
  window.requestAnimationFrame(draw);
122
123
  };
123
124
 
@@ -125,8 +126,8 @@ export function createCodePane(): CodePane {
125
126
  editing = value;
126
127
  element.classList.toggle("is-editing", editing);
127
128
  setIcon(modeIcon, editing ? "eye" : "edit");
128
- mode.setAttribute("aria-label", editing ? "Read source" : "Edit source");
129
- mode.title = editing ? "Read source" : "Edit source";
129
+ uiAttr(mode, "aria-label", editing ? "source.read-source" : "source.edit-source");
130
+ uiAttr(mode, "title", editing ? "source.read-source" : "source.edit-source");
130
131
  if (editing) {
131
132
  editor.value = sourceText;
132
133
  editingRevision = snapshotRevision;
@@ -143,7 +144,7 @@ export function createCodePane(): CodePane {
143
144
  saveInFlight = true;
144
145
  let accepted = false;
145
146
  let acceptedRevision = savingRevision;
146
- saveState.textContent = "Saving";
147
+ uiText(saveState, "common.saving");
147
148
  saveState.className = "code-save-state saving";
148
149
  try {
149
150
  const response = await fetch("/__studio/source", {
@@ -153,7 +154,7 @@ export function createCodePane(): CodePane {
153
154
  });
154
155
  if (!response.ok) {
155
156
  const reason = await response.text();
156
- throw new Error(reason || `Save failed (${response.status})`);
157
+ throw new Error(reason || t("source.save-failed-status", { status: response.status }));
157
158
  }
158
159
  const result = await response.json() as { readonly revision?: unknown };
159
160
  if (typeof result.revision === "number") acceptedRevision = result.revision;
@@ -163,15 +164,15 @@ export function createCodePane(): CodePane {
163
164
  sourceText = savingText;
164
165
  dirty = false;
165
166
  }
166
- saveState.textContent = "Saved";
167
+ uiText(saveState, "common.saved");
167
168
  saveState.className = "code-save-state saved";
168
169
  window.setTimeout(() => {
169
- if (!dirty) saveState.textContent = "";
170
+ if (!dirty) userText(saveState, "");
170
171
  }, 1200);
171
172
  } catch (error) {
172
- saveState.textContent = error instanceof Error && error.message.includes("changed")
173
- ? "Changed outside Studio"
174
- : "Save failed";
173
+ uiText(saveState, error instanceof Error && error.message.includes("changed")
174
+ ? "source.changed-outside-studio"
175
+ : "common.save-failed");
175
176
  saveState.className = "code-save-state error";
176
177
  } finally {
177
178
  saveInFlight = false;
@@ -189,7 +190,7 @@ export function createCodePane(): CodePane {
189
190
  wrap.addEventListener("click", () => setWrapped(!wrapped));
190
191
  editor.addEventListener("input", () => {
191
192
  dirty = true;
192
- saveState.textContent = "Unsaved";
193
+ uiText(saveState, "source.unsaved");
193
194
  saveState.className = "code-save-state dirty";
194
195
  if (saveTimer !== undefined) window.clearTimeout(saveTimer);
195
196
  saveTimer = window.setTimeout(() => void save(), 480);
@@ -308,7 +309,7 @@ export function createCodePane(): CodePane {
308
309
  imports: [],
309
310
  };
310
311
  if (activeSource !== undefined && activeSource.path !== sourceFile.path && dirty) {
311
- saveState.textContent = "Saving before switch";
312
+ uiText(saveState, "source.saving-before-switch");
312
313
  saveState.className = "code-save-state saving";
313
314
  void save();
314
315
  return false;