@hypit/hypit 0.1.5 → 0.1.7
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.
- package/README.md +1 -1
- package/examples/semantic-composition/README.md +2 -1
- package/examples/semantic-composition/chat.svml +1 -1
- package/examples/semantic-composition/hypit.runtime.json +1 -1
- package/package.json +4 -1
- package/packages/caption-fine/README.md +7 -0
- package/packages/cli/src/view.ts +4 -3
- package/packages/film/src/surface.ts +4 -1
- package/packages/provider-hypihub/README.md +17 -3
- package/packages/provider-hypihub/src/oauth.ts +1 -1
- package/packages/provider-hypihub/src/provider.ts +35 -9
- package/packages/provider-hypihub/src/upload.ts +37 -4
- package/packages/runtime-local/README.md +17 -0
- package/packages/script/README.md +15 -3
- package/packages/script/src/manifest.ts +2 -1
- package/packages/script/src/parser.ts +45 -6
- package/packages/studio/LOCALIZATION.md +98 -0
- package/packages/studio/README.md +13 -0
- package/packages/studio/locales/en.json +197 -0
- package/packages/studio/locales/zh-CN.json +191 -0
- package/packages/studio/src/localization-node.ts +46 -0
- package/packages/studio/src/localization.ts +85 -0
- package/packages/studio/src/style.css +15 -3
- package/packages/studio/src/ui/artifact-name.ts +8 -6
- package/packages/studio/src/ui/artifact-preview.ts +5 -4
- package/packages/studio/src/ui/code.ts +17 -16
- package/packages/studio/src/ui/comments.ts +26 -25
- package/packages/studio/src/ui/dropdown.ts +68 -0
- package/packages/studio/src/ui/i18n.ts +109 -0
- package/packages/studio/src/ui/icons.ts +1 -0
- package/packages/studio/src/ui/library.ts +76 -65
- package/packages/studio/src/ui/main.ts +84 -131
- package/packages/studio/src/ui/scrub-preview.ts +8 -6
- package/packages/studio/src/ui/stage.ts +28 -27
- package/packages/studio/src/ui/timeline.ts +17 -14
- package/packages/studio/start.ts +39 -22
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Localizing Studio
|
|
2
|
+
|
|
3
|
+
Studio owns its interface messages; a language pack supplies their wording. The
|
|
4
|
+
English catalog in [locales/en.json](locales/en.json) is the complete list of
|
|
5
|
+
message IDs. [Simplified Chinese](locales/zh-CN.json) uses exactly the same format
|
|
6
|
+
as any additional language. A pack is JSON data, not executable code.
|
|
7
|
+
|
|
8
|
+
## Add a language
|
|
9
|
+
|
|
10
|
+
Copy `locales/en.json`, change `locale` (a BCP 47 tag such as `ja` or `pt-BR`),
|
|
11
|
+
`name` (the language's native name), and translate the message values. Keep the
|
|
12
|
+
message IDs. `direction` is `ltr` or `rtl` and controls translated text; it does
|
|
13
|
+
not reverse the physical video timeline or canvas coordinates.
|
|
14
|
+
|
|
15
|
+
A small, valid starting point is:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"locale": "ja",
|
|
20
|
+
"name": "日本語",
|
|
21
|
+
"direction": "ltr",
|
|
22
|
+
"messages": {
|
|
23
|
+
"app.studio": "スタジオ",
|
|
24
|
+
"app.comments": "コメント",
|
|
25
|
+
"app.language": "言語"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Missing messages use English, so a partial translation is useful immediately.
|
|
31
|
+
Text is displayed as text, never interpreted as HTML. Preserve named variables
|
|
32
|
+
such as `{name}` and `{count}`; their order may change to suit the language.
|
|
33
|
+
Messages with `{count}` can be strings or plural forms:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{ "one": "{count} item", "other": "{count} items" }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use the language's `Intl.PluralRules` categories (`zero`, `one`, `two`, `few`,
|
|
40
|
+
`many`, `other`); `other` is required. Numbers and interface dates use the chosen
|
|
41
|
+
locale. Choose natural full sentences instead of assembling translated fragments.
|
|
42
|
+
|
|
43
|
+
Check and load your file:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
hypit studio --check-locale ./ja.json
|
|
47
|
+
hypit studio --run ./render.svrun --locale-pack ./ja.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The check lists missing translations and unknown IDs and reports malformed
|
|
51
|
+
messages or renamed variables. It does not open a Run or require a Runtime.
|
|
52
|
+
Relative file paths start at the command's working directory; prefix them with
|
|
53
|
+
`./` or `../`. Repeat `--locale-pack` to load multiple languages. Packs are read
|
|
54
|
+
at server startup; restart Studio after editing a pack. An explicitly loaded
|
|
55
|
+
pack with an existing locale replaces that locale's pack, with English fallback
|
|
56
|
+
for any omitted messages.
|
|
57
|
+
|
|
58
|
+
## Share a language pack
|
|
59
|
+
|
|
60
|
+
A local file is sufficient. For reuse, publish it through an ordinary package
|
|
61
|
+
under your own scope; there is no Hypit language registry or separate approval
|
|
62
|
+
step. Export the JSON file in the package's `package.json`, for example:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"name": "@your-scope/studio-ja",
|
|
67
|
+
"version": "1.0.0",
|
|
68
|
+
"files": ["ja.json"],
|
|
69
|
+
"exports": { "./locale.json": "./ja.json" }
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Install it using the project's package manager, then load it explicitly:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
hypit studio --run ./render.svrun --locale-pack @your-scope/studio-ja/locale.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Package exports resolve from the project, or `--package-root` when specified.
|
|
80
|
+
Studio reads the resolved JSON without importing package code. It neither scans
|
|
81
|
+
for language packs nor installs missing packages.
|
|
82
|
+
|
|
83
|
+
## Interface behavior and ownership
|
|
84
|
+
|
|
85
|
+
The globe menu to the left of **Studio / Comments** lists loaded languages by
|
|
86
|
+
their native names. The first visit follows the browser's language preferences;
|
|
87
|
+
subsequent visits retain the choice saved in that browser. Switching updates
|
|
88
|
+
marked labels in place, preserving the playhead, selection and comment draft.
|
|
89
|
+
|
|
90
|
+
Only Studio-owned interface text belongs in this catalog. Component names,
|
|
91
|
+
Companion-supplied labels, Source code, user comments and raw service diagnostics
|
|
92
|
+
retain their original text. Language packs do not change the Companion ABI,
|
|
93
|
+
Core, compilation, or rendering. A component remains the owner of its labels.
|
|
94
|
+
|
|
95
|
+
When adding Studio UI, give each message a stable ID in the English catalog and
|
|
96
|
+
use the helpers in `src/ui/i18n.ts` for text and attributes. Do not use English
|
|
97
|
+
wording as IDs, inspect DOM text to guess what needs translation, or put language
|
|
98
|
+
branches into components. Keep interpolation values separate from message text.
|
|
@@ -258,3 +258,16 @@ audibility regions and target-relative fades. Web Audio gain nodes schedule the
|
|
|
258
258
|
on the same clock, including silence and amplification above unity. Visual video elements stay muted;
|
|
259
259
|
the Film's selected AudioTracks own composition sound. Seeking is silent. This path is independent of
|
|
260
260
|
which component emitted a clip.
|
|
261
|
+
|
|
262
|
+
### Interface language
|
|
263
|
+
|
|
264
|
+
The globe menu to the left of **Studio / Comments** selects a language. English
|
|
265
|
+
and Simplified Chinese are included; additional JSON language packs can be loaded
|
|
266
|
+
with `--locale-pack`, from local files or installed package exports. The first
|
|
267
|
+
visit follows browser preferences, and later visits remember the user's choice.
|
|
268
|
+
Switching preserves the playhead, selection and comment draft.
|
|
269
|
+
|
|
270
|
+
[Localizing Studio](LOCALIZATION.md) explains the message catalog, plural forms,
|
|
271
|
+
translation checks and language-pack distribution. Only Studio-owned interface
|
|
272
|
+
text is translated; Companion labels, project content and raw diagnostics retain
|
|
273
|
+
their original text. The Companion ABI is unchanged.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
{
|
|
2
|
+
"locale": "en",
|
|
3
|
+
"name": "English",
|
|
4
|
+
"direction": "ltr",
|
|
5
|
+
"messages": {
|
|
6
|
+
"app.comments": "Comments",
|
|
7
|
+
"app.language": "Language",
|
|
8
|
+
"app.studio": "Studio",
|
|
9
|
+
"app.studio-view": "Studio view",
|
|
10
|
+
"comments.all": "All",
|
|
11
|
+
"comments.caught-up": "All caught up",
|
|
12
|
+
"comments.delete-comment": "Delete comment",
|
|
13
|
+
"comments.done": "Done",
|
|
14
|
+
"comments.draw-soon": "Draw — coming later",
|
|
15
|
+
"comments.edit-comment": "Edit comment",
|
|
16
|
+
"comments.edit-comment-text": "Edit comment text",
|
|
17
|
+
"comments.emoji-soon": "Emoji — coming later",
|
|
18
|
+
"comments.empty": "A note starts here",
|
|
19
|
+
"comments.empty-hint": "Pause on a frame and describe what you would like to change.",
|
|
20
|
+
"comments.filter-comments": "Filter comments",
|
|
21
|
+
"comments.filter-hint": "Switch filters to see the other comments.",
|
|
22
|
+
"comments.go-to-this-frame": "Go to this frame",
|
|
23
|
+
"comments.loading-comments": "Loading comments…",
|
|
24
|
+
"comments.mark-complete": "Mark complete",
|
|
25
|
+
"comments.no-completed-comments": "No completed comments",
|
|
26
|
+
"comments.open": "Open",
|
|
27
|
+
"comments.placeholder": "What would you like to change?",
|
|
28
|
+
"comments.read-failed": "Could not read comments.",
|
|
29
|
+
"comments.refresh-comments": "Refresh comments",
|
|
30
|
+
"comments.reopen-comment": "Reopen comment",
|
|
31
|
+
"comments.save-failed": "Could not save comment.",
|
|
32
|
+
"comments.send-comment": "Send comment",
|
|
33
|
+
"comments.send-shortcut": "Send comment (Enter)",
|
|
34
|
+
"comments.use-current-frame": "Use current frame",
|
|
35
|
+
"comments.write-a-comment": "Write a comment",
|
|
36
|
+
"common.cancel": "Cancel",
|
|
37
|
+
"common.cancelled": "Cancelled",
|
|
38
|
+
"common.compile-failed": "Compile failed",
|
|
39
|
+
"common.completed": "Completed",
|
|
40
|
+
"common.copied": "Copied",
|
|
41
|
+
"common.failed": "Failed",
|
|
42
|
+
"common.invalid-value": "Invalid value",
|
|
43
|
+
"common.needs-attention": "Needs attention",
|
|
44
|
+
"common.queued": "Queued",
|
|
45
|
+
"common.refresh": "Refresh",
|
|
46
|
+
"common.running": "Running",
|
|
47
|
+
"common.save": "Save",
|
|
48
|
+
"common.save-failed": "Save failed",
|
|
49
|
+
"common.saved": "Saved",
|
|
50
|
+
"common.saving": "Saving",
|
|
51
|
+
"common.waiting": "Waiting",
|
|
52
|
+
"inspector.add-color": "Add color",
|
|
53
|
+
"inspector.add-item": "Add item",
|
|
54
|
+
"inspector.anchor.program-end": "Timeline end{detail}",
|
|
55
|
+
"inspector.anchor.program-start": "Timeline start{detail}",
|
|
56
|
+
"inspector.anchor.segment-end": "Segment end{detail}",
|
|
57
|
+
"inspector.anchor.segment-start": "Segment start{detail}",
|
|
58
|
+
"inspector.anchor.token-end": "Word end{detail}",
|
|
59
|
+
"inspector.anchor.token-start": "Word start{detail}",
|
|
60
|
+
"inspector.aspect-ratio": "Aspect ratio",
|
|
61
|
+
"inspector.author": "Author",
|
|
62
|
+
"inspector.build": "Build",
|
|
63
|
+
"inspector.candidates": "Candidates",
|
|
64
|
+
"inspector.canvas": "Canvas",
|
|
65
|
+
"inspector.duration": "Duration",
|
|
66
|
+
"inspector.empty": "No details for this selection",
|
|
67
|
+
"inspector.end": "End",
|
|
68
|
+
"inspector.end-anchor": "End semantic anchor",
|
|
69
|
+
"inspector.frame-rate": "Frame rate",
|
|
70
|
+
"inspector.frames": "Frames",
|
|
71
|
+
"inspector.how": "How",
|
|
72
|
+
"inspector.invalid-fields": "Complete the required fields with valid values to save.",
|
|
73
|
+
"inspector.item-count": {
|
|
74
|
+
"one": "{count} item",
|
|
75
|
+
"other": "{count} items"
|
|
76
|
+
},
|
|
77
|
+
"inspector.moment": "Moment",
|
|
78
|
+
"inspector.moment-anchor": "Moment semantic anchor",
|
|
79
|
+
"inspector.move-down": "Move down",
|
|
80
|
+
"inspector.move-up": "Move up",
|
|
81
|
+
"inspector.project": "Project",
|
|
82
|
+
"inspector.properties": "Properties",
|
|
83
|
+
"inspector.remove-item": "Remove item",
|
|
84
|
+
"inspector.resolution": "Resolution",
|
|
85
|
+
"inspector.run": "Run",
|
|
86
|
+
"inspector.semantic-anchors": "Semantic anchors",
|
|
87
|
+
"inspector.sources": "Sources",
|
|
88
|
+
"inspector.start": "Start",
|
|
89
|
+
"inspector.start-anchor": "Start semantic anchor",
|
|
90
|
+
"inspector.structured-value": "Structured value",
|
|
91
|
+
"inspector.targets": "Targets",
|
|
92
|
+
"inspector.timeline": "Timeline",
|
|
93
|
+
"inspector.timing": "Timing",
|
|
94
|
+
"inspector.tracks": "Tracks",
|
|
95
|
+
"inspector.use-color": "Use {color}",
|
|
96
|
+
"inspector.when": "When",
|
|
97
|
+
"inspector.where": "Where",
|
|
98
|
+
"library.all-media": "All media",
|
|
99
|
+
"library.all-tasks": "All tasks",
|
|
100
|
+
"library.artifacts": "Artifacts",
|
|
101
|
+
"library.audio": "Audio",
|
|
102
|
+
"library.clear-task-filter": "Clear task filter",
|
|
103
|
+
"library.data": "Data",
|
|
104
|
+
"library.display-name": "Display name",
|
|
105
|
+
"library.enter-name": "Enter a name.",
|
|
106
|
+
"library.finished": "Finished",
|
|
107
|
+
"library.image": "Image",
|
|
108
|
+
"library.images": "Images",
|
|
109
|
+
"library.in-progress": "In progress",
|
|
110
|
+
"library.loading-media": "Loading media…",
|
|
111
|
+
"library.loading-more": "Loading more…",
|
|
112
|
+
"library.loading-tasks": "Loading tasks…",
|
|
113
|
+
"library.media-categories": "Media categories",
|
|
114
|
+
"library.media-category": "Media category",
|
|
115
|
+
"library.media-hint": "Images, videos and audio published as file Outputs appear here, including those from ongoing Builds.",
|
|
116
|
+
"library.name-failed": "Could not save the name",
|
|
117
|
+
"library.name-pending": "{name}\nNaming is available when this Build finishes",
|
|
118
|
+
"library.no-files-in-this-category": "No files in this category",
|
|
119
|
+
"library.no-media": "No media",
|
|
120
|
+
"library.no-tasks-here": "No tasks here",
|
|
121
|
+
"library.no-tasks-in-progress": "No tasks in progress",
|
|
122
|
+
"library.preview-name": "Preview {name}",
|
|
123
|
+
"library.preview-unavailable": "Preview unavailable",
|
|
124
|
+
"library.referenced-source-files": "Referenced source files",
|
|
125
|
+
"library.refresh-failed": "Refresh failed: {detail}",
|
|
126
|
+
"library.refresh-failed-retained": "Refresh failed: {detail} · Previous view retained",
|
|
127
|
+
"library.refresh-interrupted": "Refresh interrupted",
|
|
128
|
+
"library.refresh-media": "Refresh media",
|
|
129
|
+
"library.refresh-tasks": "Refresh tasks",
|
|
130
|
+
"library.refreshing": "Refreshing…",
|
|
131
|
+
"library.rename-hint": "{name}\nDouble-click or press F2 to rename",
|
|
132
|
+
"library.return-to-task": "Return to task",
|
|
133
|
+
"library.source": "Source",
|
|
134
|
+
"library.studio-library": "Studio library",
|
|
135
|
+
"library.task-categories": "Task categories",
|
|
136
|
+
"library.task-return": "{name} · Return to task",
|
|
137
|
+
"library.tasks": "Tasks",
|
|
138
|
+
"library.unavailable": "Library unavailable",
|
|
139
|
+
"library.updated": "Updated {time}",
|
|
140
|
+
"library.video": "Video",
|
|
141
|
+
"library.video-loading-preview": "Video · Loading preview",
|
|
142
|
+
"library.videos": "Videos",
|
|
143
|
+
"library.view-media": "View media",
|
|
144
|
+
"library.view-task-media": "View task media",
|
|
145
|
+
"player.audio-waveform": "Audio waveform",
|
|
146
|
+
"player.back-5-seconds": "Back 5 seconds",
|
|
147
|
+
"player.back-to-composition": "Back to composition",
|
|
148
|
+
"player.composition-time": "Composition time",
|
|
149
|
+
"player.elements-at-this-point": "Elements at this point",
|
|
150
|
+
"player.forward-5-seconds": "Forward 5 seconds",
|
|
151
|
+
"player.frame-loading": "Loading frame…",
|
|
152
|
+
"player.frame-unavailable": "Frame unavailable",
|
|
153
|
+
"player.hovered-frame": "Hovered frame",
|
|
154
|
+
"player.media-failed": "This media could not be opened in the browser.",
|
|
155
|
+
"player.media-time": "Media time",
|
|
156
|
+
"player.mute": "Mute",
|
|
157
|
+
"player.next-frame": "Next frame",
|
|
158
|
+
"player.pause": "Pause",
|
|
159
|
+
"player.play": "Play",
|
|
160
|
+
"player.preview": "Preview",
|
|
161
|
+
"player.previous-frame": "Previous frame",
|
|
162
|
+
"player.unmute": "Unmute",
|
|
163
|
+
"source.changed-outside-studio": "Changed outside Studio",
|
|
164
|
+
"source.edit-source": "Edit source",
|
|
165
|
+
"source.keep-lines-unwrapped": "Keep lines unwrapped",
|
|
166
|
+
"source.read-source": "Read source",
|
|
167
|
+
"source.save-failed-status": "Save failed ({status})",
|
|
168
|
+
"source.saving-before-switch": "Saving before switch",
|
|
169
|
+
"source.svml-source": "SVML source",
|
|
170
|
+
"source.unsaved": "Unsaved",
|
|
171
|
+
"source.wrap-lines": "Wrap lines",
|
|
172
|
+
"tasks.copy-error": "Copy error",
|
|
173
|
+
"tasks.copy-manually": "Select the text to copy",
|
|
174
|
+
"tasks.no-runtime": "No Runtime is selected. Saved tasks are available under Finished.",
|
|
175
|
+
"tasks.progress": "{completed}/{total} steps complete",
|
|
176
|
+
"tasks.refresh-hint": "Refresh to read the latest task status.",
|
|
177
|
+
"tasks.saved-only": "Saved project Results · no Runtime selected",
|
|
178
|
+
"timeline.fit-timeline": "Fit timeline",
|
|
179
|
+
"timeline.moment-name": "Moment {name}",
|
|
180
|
+
"timeline.overlapping-timeline-objects": "Overlapping timeline objects",
|
|
181
|
+
"timeline.resize-timeline-track-labels": "Resize timeline track labels",
|
|
182
|
+
"timeline.resize-track-labels": "Resize track labels",
|
|
183
|
+
"timeline.segment-name": "Segment {name}",
|
|
184
|
+
"timeline.segments": "Segments",
|
|
185
|
+
"timeline.select": "Select",
|
|
186
|
+
"timeline.selection-name": "Selection {name}",
|
|
187
|
+
"timeline.selections-and-moments": "Selections and Moments",
|
|
188
|
+
"timeline.words": "Words",
|
|
189
|
+
"timeline.zoom-in": "Zoom in",
|
|
190
|
+
"timeline.zoom-out": "Zoom out",
|
|
191
|
+
"inspector.color-picker": "{label} picker",
|
|
192
|
+
"timeline.segment-count": {
|
|
193
|
+
"one": "{count} segment",
|
|
194
|
+
"other": "{count} segments"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -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
|
+
}
|