dsh-plugin-vrmpet3d 0.1.0 → 0.1.1
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 +2 -0
- package/desktop/main.js +45 -3
- package/desktop/preload.js +6 -2
- package/lib/client.core.js +209 -13
- package/lib/client.js +278 -42
- package/lib/config-page.html +71 -45
- package/lib/config-page.js +1120 -229
- package/lib/core.js +209 -13
- package/lib/desktop-page.js +144 -8
- package/lib/global-page.html +140 -99
- package/lib/index.js +150 -33
- package/lib/model-picker.js +139 -49
- package/lib/picker-page.html +34 -8
- package/lib/picker-page.js +192 -11
- package/lib/setup-page.html +68 -49
- package/lib/setup-page.js +395 -42
- package/lib/whale.js +191 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ no runtime dependencies · works offline
|
|
|
42
42
|
| 互动 | 点击与长按部位触发表情和碎碎念;拖动、缩放、旋转视角、视线跟随和弹簧骨物理 |
|
|
43
43
|
| 碎碎念 | 内置台词、自定义台词,或使用 DSH 模型 / OpenAI 兼容接口生成 AI 台词 |
|
|
44
44
|
| 导入 | 模型以文件导入、动作包以文件夹导入,复制到插件存储后不依赖原路径 |
|
|
45
|
+
| 界面语言 | 全部界面中英双语,默认跟随系统 / DSH 自动识别;「通用设置 → 显示与外观」和初次引导欢迎页各有一个语言选择(自动 / 中文 / English) |
|
|
45
46
|
|
|
46
47
|
## 运行要求
|
|
47
48
|
|
|
@@ -155,6 +156,7 @@ The marketplace screenshot order is declared in the root [screenshots.json](scre
|
|
|
155
156
|
| Interaction | Click or hold body parts for expressions and chatter; drag, resize, rotate the view, follow the pointer and use spring-bone physics |
|
|
156
157
|
| Chatter | Built-in or custom lines, or AI-generated lines using a DSH model or an OpenAI-compatible service |
|
|
157
158
|
| Import | Import models as files and motion packs as folders. Imported files are copied into plugin storage and no longer depend on their original paths |
|
|
159
|
+
| Language | The whole interface is Chinese and English, auto-detected from the system or DSH by default; a chooser (Auto / 中文 / English) sits in General settings → Display, and on the first-run guide's welcome screen |
|
|
158
160
|
|
|
159
161
|
### Requirements
|
|
160
162
|
|
package/desktop/main.js
CHANGED
|
@@ -37,6 +37,47 @@ const DEFAULT_PET_WINDOW = { width: 240, height: 320 };
|
|
|
37
37
|
*/
|
|
38
38
|
const VRMPET3D_STATUS_PREFIX = 'vrmpet3d-desktop: ';
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The words THIS process puts on screen itself: the two native dialog titles and the model filter's name.
|
|
42
|
+
*
|
|
43
|
+
* Nothing else here is a string a user reads — every other sentence in this file is a log line or a self-test
|
|
44
|
+
* report — and these three are the exception because Electron, not a page, draws them. The window title is
|
|
45
|
+
* deliberately NOT in this table: it names the launcher's own window (`vrmpet3d - model settings`) and is the same
|
|
46
|
+
* ASCII wherever the user is, so a translated one would add a second name for a window the user finds by that one.
|
|
47
|
+
*
|
|
48
|
+
* The launcher is a SEPARATE PROCESS with no access to `lib/core.js`, so it keeps its own copy of the repository's
|
|
49
|
+
* two-language shape (`{ zh: {...}, en: {...} }`, identical key sets) rather than importing the host's rules.
|
|
50
|
+
*/
|
|
51
|
+
const DIALOG_TEXT = {
|
|
52
|
+
zh: { motionTitle: '导入动作目录', modelTitle: '导入模型文件', modelFilter: 'VRM / glTF 模型' },
|
|
53
|
+
en: { motionTitle: 'Import motion folder', modelTitle: 'Import model file', modelFilter: 'VRM / glTF model' },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Which of the two this process draws in, for one request.
|
|
58
|
+
*
|
|
59
|
+
* The PAGE wins whenever it says something this process understands. The settings page knows the user's
|
|
60
|
+
* `preferences.language`, which may be a deliberate 中文 on an English machine or the reverse, so it is the better
|
|
61
|
+
* authority when it speaks; it may pass that as `{ kind, language }`, and `'auto'` (or nothing at all) is not a
|
|
62
|
+
* language, so the fallback is `app.getLocale()` — the machine's own locale, which is exactly what `'auto'` means
|
|
63
|
+
* for a surface with no DSH locale to consult.
|
|
64
|
+
*
|
|
65
|
+
* The field is OPTIONAL and nothing here requires it: an older page that sends only `{ kind }` (without a language argument) gets the locale, and a page that sends a value neither of the two languages
|
|
66
|
+
* ignores it rather than having it guessed at.
|
|
67
|
+
*/
|
|
68
|
+
function dialogLanguage(options) {
|
|
69
|
+
const wanted = options && typeof options === 'object' ? options.language : null;
|
|
70
|
+
if (wanted === 'zh' || wanted === 'en') return wanted;
|
|
71
|
+
let locale = '';
|
|
72
|
+
try {
|
|
73
|
+
// After `ready`, which every handler here is; a locale that cannot be read is not a reason to fail a dialog.
|
|
74
|
+
locale = app.getLocale() || '';
|
|
75
|
+
} catch (_error) {
|
|
76
|
+
locale = '';
|
|
77
|
+
}
|
|
78
|
+
return /^(zh|cn|chinese)/i.test(String(locale).trim()) ? 'zh' : 'en';
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
/**
|
|
41
82
|
* The payload the page expects for a cursor sample.
|
|
42
83
|
*
|
|
@@ -590,15 +631,16 @@ app.whenReady().then(async () => {
|
|
|
590
631
|
*/
|
|
591
632
|
ipcMain.handle('vrmpet3d:pick-path', async (_event, options) => {
|
|
592
633
|
const kind = options && typeof options === 'object' && options.kind === 'motion' ? 'motion' : 'model';
|
|
634
|
+
const text = DIALOG_TEXT[dialogLanguage(options)] || DIALOG_TEXT.en;
|
|
593
635
|
try {
|
|
594
636
|
const result = await dialog.showOpenDialog(win, kind === 'motion' ? {
|
|
595
|
-
title:
|
|
637
|
+
title: text.motionTitle,
|
|
596
638
|
properties: ['openDirectory'],
|
|
597
639
|
defaultPath: app.getPath('home'),
|
|
598
640
|
} : {
|
|
599
|
-
title:
|
|
641
|
+
title: text.modelTitle,
|
|
600
642
|
properties: ['openFile'],
|
|
601
|
-
filters: [{ name:
|
|
643
|
+
filters: [{ name: text.modelFilter, extensions: ['vrm', 'glb', 'gltf'] }],
|
|
602
644
|
defaultPath: app.getPath('home'),
|
|
603
645
|
});
|
|
604
646
|
if (result.canceled || !Array.isArray(result.filePaths) || !result.filePaths.length) return null;
|
package/desktop/preload.js
CHANGED
|
@@ -39,7 +39,9 @@ contextBridge.exposeInMainWorld('vrmpet3dDesktop', {
|
|
|
39
39
|
* plugin's own storage. The host stays the only writer of that list, and the `VRMPET3D_MODEL_FILES_FILE` /
|
|
40
40
|
* `VRMPET3D_IMPORT_DIR` overrides the suites rely on are not bypassed.
|
|
41
41
|
*/
|
|
42
|
-
pickModelFile: () => ipcRenderer.invoke('vrmpet3d:pick-path', {
|
|
42
|
+
pickModelFile: (language) => ipcRenderer.invoke('vrmpet3d:pick-path', {
|
|
43
|
+
kind: 'model', language: language === 'zh' || language === 'en' ? language : undefined,
|
|
44
|
+
}),
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
47
|
* The same dialog for an imported MOTION folder (`/vrmpet3d/motions`), which stays a DIRECTORY: a motion pack is
|
|
@@ -47,7 +49,9 @@ contextBridge.exposeInMainWorld('vrmpet3dDesktop', {
|
|
|
47
49
|
*
|
|
48
50
|
* One channel for both, with `kind` deciding a FILE dialog from a folder one and the title each shows.
|
|
49
51
|
*/
|
|
50
|
-
pickMotionDir: () => ipcRenderer.invoke('vrmpet3d:pick-path', {
|
|
52
|
+
pickMotionDir: (language) => ipcRenderer.invoke('vrmpet3d:pick-path', {
|
|
53
|
+
kind: 'motion', language: language === 'zh' || language === 'en' ? language : undefined,
|
|
54
|
+
}),
|
|
51
55
|
|
|
52
56
|
/**
|
|
53
57
|
* Tell the launcher that a VIEW ORBIT is starting or ending — and let IT take the pointer lock.
|
package/lib/client.core.js
CHANGED
|
@@ -79,16 +79,32 @@
|
|
|
79
79
|
* The `{duration}` token and its machinery stay — a line the user writes may still carry it (see
|
|
80
80
|
* {@link STATUS_DURATION_TOKEN}) — but the shipped line is DSH's token-free 「深度求索中 ···」, which is also what the
|
|
81
81
|
* app shows while the latest turn is not open.
|
|
82
|
+
*
|
|
83
|
+
* ONE TABLE PER LANGUAGE, keyed state → language, the same shape `SPEECH_LINES` uses for its pools and for the
|
|
84
|
+
* same reason: a status line is TEXT THE USER READS, so an interface in English that still said 「任务达成」 would
|
|
85
|
+
* be a half-translated one. The two halves are written rather than translated word for word, and the Chinese is
|
|
86
|
+
* the operator's own wording, unchanged since 2026-10-13.
|
|
87
|
+
*
|
|
88
|
+
* An EDIT wins over both: `statusEntryFor` only reaches for these while the user's own box is empty, so a line
|
|
89
|
+
* typed by hand is one line in one language and stays exactly that, in either interface language. That is the
|
|
90
|
+
* whole reason this is a per-language DEFAULT rather than a second field in the settings.
|
|
82
91
|
*/
|
|
83
92
|
var DEFAULT_STATUS_TEXT = {
|
|
84
|
-
resting: '',
|
|
85
|
-
working: '深度求索中 ···',
|
|
86
|
-
waiting: '我有问题要问你',
|
|
87
|
-
done: '任务达成',
|
|
88
|
-
error: '遇到问题了...',
|
|
89
|
-
offline: '',
|
|
93
|
+
resting: { zh: '', en: '' },
|
|
94
|
+
working: { zh: '深度求索中 ···', en: 'Working ···' },
|
|
95
|
+
waiting: { zh: '我有问题要问你', en: 'I have a question for you' },
|
|
96
|
+
done: { zh: '任务达成', en: 'Task complete' },
|
|
97
|
+
error: { zh: '遇到问题了...', en: 'Something went wrong...' },
|
|
98
|
+
offline: { zh: '', en: '' },
|
|
90
99
|
};
|
|
91
100
|
|
|
101
|
+
/** One state's shipped line in one language; `''` for a state that says nothing by default. */
|
|
102
|
+
function defaultStatusText(state, language) {
|
|
103
|
+
var entry = DEFAULT_STATUS_TEXT[state];
|
|
104
|
+
if (!entry || typeof entry !== 'object') return '';
|
|
105
|
+
return typeof entry[language === 'en' ? 'en' : 'zh'] === 'string' ? entry[language === 'en' ? 'en' : 'zh'] : '';
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
/**
|
|
93
109
|
* The token a status line may carry where the live elapsed time goes, and the units it is written with.
|
|
94
110
|
*
|
|
@@ -104,6 +120,155 @@
|
|
|
104
120
|
en: { hour: 'h ', minute: 'm ', second: 's' },
|
|
105
121
|
};
|
|
106
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The three answers to 「界面语言」, and the ONE rule that turns them into the language a surface draws in.
|
|
125
|
+
*
|
|
126
|
+
* `auto` is the shipped answer and means "whatever the surface is already running in": DSH's own locale inside
|
|
127
|
+
* the app, the browser's on a host-served page, and on a surface that can see neither (a suite, a headless
|
|
128
|
+
* host) English — the plugin's own fallback rather than a guess about the operator.
|
|
129
|
+
*
|
|
130
|
+
* The other two are the user overruling that, and they exist because the two questions are NOT the same one:
|
|
131
|
+
* DSH can be running in English while its owner wants the pet in Chinese, and the first-run guide is read once,
|
|
132
|
+
* before any of that has settled. Operator, 2026-10-15: 「把全部界面做中英双语自动识别,并在通用设置和初始安装
|
|
133
|
+
* 引导页的第一页欢迎页面中加上语言选择」.
|
|
134
|
+
*
|
|
135
|
+
* `auto` is STORED as `auto`, never resolved on write. A profile that stored the resolved 'zh' would freeze the
|
|
136
|
+
* language on the day it was created, and the user who later switched DSH to English would keep a Chinese pet
|
|
137
|
+
* with no way to tell that from a deliberate choice — the same mistake `DEFAULTS.petSurface` documents for
|
|
138
|
+
* 显示位置, and it is why this is a preference the CLEANER knows about rather than a value a page writes.
|
|
139
|
+
*/
|
|
140
|
+
var LANGUAGE_SETTINGS = ['auto', 'zh', 'en'];
|
|
141
|
+
|
|
142
|
+
/** `'auto'` for anything that is not one of the three — an unknown stored value is a reset, not an error. */
|
|
143
|
+
function cleanLanguage(value) {
|
|
144
|
+
return LANGUAGE_SETTINGS.indexOf(value) === -1 ? 'auto' : value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Which of the two languages this surface draws in, given the user's setting and what the environment says.
|
|
149
|
+
*
|
|
150
|
+
* `hints` is a LIST, most authoritative first — DSH's locale, then the document's own `lang`, then
|
|
151
|
+
* `navigator.language` — and the FIRST hint that says anything decides. That order is the rule and not a detail:
|
|
152
|
+
* DSH is the application the pet lives inside, so its language outranks the browser's, and a page DSH has
|
|
153
|
+
* already marked `lang="zh"` outranks a browser whose own preference is en-US.
|
|
154
|
+
*
|
|
155
|
+
* The first hint decides the ZH-or-not question instead of the chain continuing past it, because this plugin has
|
|
156
|
+
* exactly two languages. A hint of `fr` is not a reason to go looking for a Chinese one further down the chain;
|
|
157
|
+
* it is a reason to fall back to English, which is also what `auto` with nothing to go on means.
|
|
158
|
+
*/
|
|
159
|
+
function resolveLanguage(preference, hints) {
|
|
160
|
+
var wanted = cleanLanguage(preference);
|
|
161
|
+
if (wanted !== 'auto') return wanted;
|
|
162
|
+
var list = Array.isArray(hints) ? hints : [hints];
|
|
163
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
164
|
+
var hint = list[i];
|
|
165
|
+
if (typeof hint !== 'string' || !hint) continue;
|
|
166
|
+
return /^(zh|cn|chinese)/i.test(hint.trim()) ? 'zh' : 'en';
|
|
167
|
+
}
|
|
168
|
+
return 'en';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The environment's own two hints, in the order they outrank each other — what a surface that has no DSH locale
|
|
173
|
+
* to ask uses. `scope` exists so a suite can hand in a fake window instead of moving the real one.
|
|
174
|
+
*
|
|
175
|
+
* The BROWSER comes first, and the document's own `lang` second, which is the opposite of the order the app's
|
|
176
|
+
* widget uses — deliberately. Every page this plugin serves ships `<html lang="zh">` (the plugin was written in
|
|
177
|
+
* Chinese), so a page that read its own markup first could never be auto-detected as English: the hint would be
|
|
178
|
+
* its own default rather than anything about the user. `navigator.language` is the browser's answer and in the
|
|
179
|
+
* floating window it is Electron's, i.e. the machine's.
|
|
180
|
+
*/
|
|
181
|
+
function browserLanguageHints(scope) {
|
|
182
|
+
var win = scope || (typeof globalThis !== 'undefined' ? globalThis : null);
|
|
183
|
+
var doc = win && win.document;
|
|
184
|
+
var nav = win && win.navigator;
|
|
185
|
+
return [
|
|
186
|
+
nav ? (nav.language || (nav.languages && nav.languages[0])) : null,
|
|
187
|
+
doc && doc.documentElement ? doc.documentElement.lang : null,
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* One surface's own wording, in the two languages, chosen by {@link resolveLanguage}.
|
|
193
|
+
*
|
|
194
|
+
* Every surface keeps its OWN table — `lib/menu.js`'s `LABELS`, `lib/model-picker.js`'s `TEXT`, the settings
|
|
195
|
+
* pages' `TEXT` — rather than one global dictionary, because that is already this repository's shape (a surface
|
|
196
|
+
* owns the words it draws) and because a single table for six surfaces would make every one of them edit the
|
|
197
|
+
* same file. What IS shared is the SHAPE: `{ zh: {...}, en: {...} }`, this accessor, and {@link applyI18n}.
|
|
198
|
+
*
|
|
199
|
+
* A language half that is missing or empty falls back to the other one rather than to blank: a half-translated
|
|
200
|
+
* surface reads as Chinese in one spot, which is a bug somebody reports, where a blank label is a page that
|
|
201
|
+
* looks broken. `textOf` below is what makes a GAP visible, and a suite is what keeps one from shipping.
|
|
202
|
+
*/
|
|
203
|
+
function textFor(table, language) {
|
|
204
|
+
if (!table || typeof table !== 'object') return {};
|
|
205
|
+
var wanted = language === 'en' ? table.en : table.zh;
|
|
206
|
+
var other = language === 'en' ? table.zh : table.en;
|
|
207
|
+
if (wanted && typeof wanted === 'object') return wanted;
|
|
208
|
+
return other && typeof other === 'object' ? other : {};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** One entry out of a surface's table. A missing key returns the KEY, so a gap is visible on screen. */
|
|
212
|
+
function textOf(table, language, key) {
|
|
213
|
+
var value = textFor(table, language)[key];
|
|
214
|
+
return typeof value === 'string' ? value : key;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Fill a document's marked-up text from one surface's table.
|
|
219
|
+
*
|
|
220
|
+
* The markup carries the CHINESE text as well as the key, and that is on purpose: this plugin's pages all ship a
|
|
221
|
+
* bootstrap watchdog for the case where a script never ran, so a page whose script DID not run should still say
|
|
222
|
+
* something in one language rather than show nothing. `verify-desktop.mjs` asserts the two agree, which is what
|
|
223
|
+
* stops the markup and the table from drifting apart.
|
|
224
|
+
*
|
|
225
|
+
* Four attributes, one per place text lives: `data-i18n` writes `textContent`, `data-i18n-html` writes
|
|
226
|
+
* `innerHTML` and is for OUR OWN sentence with a `<strong>` in it (never for anything a user or a model wrote),
|
|
227
|
+
* and `data-i18n-placeholder` / `data-i18n-title` / `data-i18n-aria` write those three attributes.
|
|
228
|
+
*
|
|
229
|
+
* A key the table does not have leaves the markup ALONE rather than printing the key: the visible text stays the
|
|
230
|
+
* Chinese fallback, and the missing entry is the suite's business, not the user's.
|
|
231
|
+
*
|
|
232
|
+
* @returns {number} how many places were rewritten, so a suite can tell "applied nothing" from "no markup".
|
|
233
|
+
*/
|
|
234
|
+
function applyI18n(root, table, language) {
|
|
235
|
+
var doc = root && typeof root.querySelectorAll === 'function'
|
|
236
|
+
? root
|
|
237
|
+
: (typeof globalThis !== 'undefined' ? globalThis.document : null);
|
|
238
|
+
if (!doc || typeof doc.querySelectorAll !== 'function') return 0;
|
|
239
|
+
var words = textFor(table, language);
|
|
240
|
+
var selector = '[data-i18n],[data-i18n-html],[data-i18n-placeholder],[data-i18n-title],[data-i18n-aria]';
|
|
241
|
+
var nodes;
|
|
242
|
+
try {
|
|
243
|
+
nodes = doc.querySelectorAll(selector);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
var applied = 0;
|
|
248
|
+
var write = function (node, attribute, key, html) {
|
|
249
|
+
var value = words[key];
|
|
250
|
+
if (typeof value !== 'string') return;
|
|
251
|
+
if (attribute === 'data-i18n') node.textContent = value;
|
|
252
|
+
else if (html) node.innerHTML = value;
|
|
253
|
+
else node.setAttribute(attribute, value);
|
|
254
|
+
applied += 1;
|
|
255
|
+
};
|
|
256
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
257
|
+
var node = nodes[i];
|
|
258
|
+
var key = node.getAttribute('data-i18n');
|
|
259
|
+
if (key) write(node, 'data-i18n', key, false);
|
|
260
|
+
key = node.getAttribute('data-i18n-html');
|
|
261
|
+
if (key) write(node, 'data-i18n-html', key, true);
|
|
262
|
+
key = node.getAttribute('data-i18n-placeholder');
|
|
263
|
+
if (key) write(node, 'placeholder', key, false);
|
|
264
|
+
key = node.getAttribute('data-i18n-title');
|
|
265
|
+
if (key) write(node, 'title', key, false);
|
|
266
|
+
key = node.getAttribute('data-i18n-aria');
|
|
267
|
+
if (key) write(node, 'aria-label', key, false);
|
|
268
|
+
}
|
|
269
|
+
return applied;
|
|
270
|
+
}
|
|
271
|
+
|
|
107
272
|
/**
|
|
108
273
|
* The states whose prompt carries DSH's whale mark.
|
|
109
274
|
*
|
|
@@ -1088,6 +1253,12 @@
|
|
|
1088
1253
|
/** Default pose and behaviour knobs. */
|
|
1089
1254
|
var DEFAULTS = {
|
|
1090
1255
|
scope: 'global',
|
|
1256
|
+
/**
|
|
1257
|
+
* 界面语言 — `auto` follows the surface's own environment ({@link resolveLanguage}), and 中文 / English overrule
|
|
1258
|
+
* it. It sits first in the table rather than beside the appearance rows because it is the one preference that
|
|
1259
|
+
* decides how every OTHER row is spelled.
|
|
1260
|
+
*/
|
|
1261
|
+
language: 'auto',
|
|
1091
1262
|
size: 100,
|
|
1092
1263
|
/**
|
|
1093
1264
|
* WHERE the pet is shown, as the cleaner's DEFAULT rather than a string repeated beside it.
|
|
@@ -1933,6 +2104,13 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
1933
2104
|
var out = {};
|
|
1934
2105
|
var source = raw && typeof raw === 'object' ? raw : {};
|
|
1935
2106
|
out.scope = source.scope === 'current' ? 'current' : 'global';
|
|
2107
|
+
/**
|
|
2108
|
+
* 界面语言 — cleaned like every other enumerated preference: an unknown value (an older profile, a typo, a page
|
|
2109
|
+
* that sent the wrong thing) is `auto`, which resolves to the surface's own environment anyway. It is NOT
|
|
2110
|
+
* counted as authorship by `authoredWeight` over in `lib/settings.js`: a language is a way of looking at the
|
|
2111
|
+
* pet, not something the user wrote into it.
|
|
2112
|
+
*/
|
|
2113
|
+
out.language = cleanLanguage(source.language);
|
|
1936
2114
|
// An unknown surface falls back to the shipped default, not to a string repeated here.
|
|
1937
2115
|
out.petSurface = PET_SURFACES.indexOf(source.petSurface) === -1 ? DEFAULTS.petSurface : source.petSurface;
|
|
1938
2116
|
out.size = Number.isFinite(Number(source.size)) ? clamp(Number(source.size), SIZE_RANGE.min, SIZE_RANGE.max) : DEFAULTS.size;
|
|
@@ -2488,8 +2666,8 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2488
2666
|
// ---------------------------------------------------------------------------
|
|
2489
2667
|
|
|
2490
2668
|
/** One state's status line, exactly as the renderer draws it: text, then the tag. */
|
|
2491
|
-
function statusLineFor(preferences, state) {
|
|
2492
|
-
var entry = statusEntryFor(preferences, state);
|
|
2669
|
+
function statusLineFor(preferences, state, language) {
|
|
2670
|
+
var entry = statusEntryFor(preferences, state, language);
|
|
2493
2671
|
if (!entry) return '';
|
|
2494
2672
|
var tag = typeof entry.tag === 'string' ? entry.tag : '';
|
|
2495
2673
|
return (entry.text + tag).slice(0, STATUS_LIMITS.textLength + STATUS_LIMITS.tagLength);
|
|
@@ -2501,11 +2679,15 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
2501
2679
|
* Always an OBJECT, never null: every state has something to say or an explicit reason to say nothing,
|
|
2502
2680
|
* and a caller that had to distinguish "unconfigured" from "configured empty" would get it wrong in the
|
|
2503
2681
|
* one case that matters (a user who cleared a line).
|
|
2682
|
+
*
|
|
2683
|
+
* `language` picks WHICH shipped default fills an empty box — see {@link DEFAULT_STATUS_TEXT}. It is the third
|
|
2684
|
+
* parameter and optional, so a caller that does not know the surface's language keeps the Chinese default the
|
|
2685
|
+
* suites and the older call sites already pin.
|
|
2504
2686
|
*/
|
|
2505
|
-
function statusEntryFor(preferences, state) {
|
|
2687
|
+
function statusEntryFor(preferences, state, language) {
|
|
2506
2688
|
var map = preferences && preferences.status && typeof preferences.status === 'object' ? preferences.status : {};
|
|
2507
2689
|
var stored = map[state] && typeof map[state] === 'object' ? map[state] : {};
|
|
2508
|
-
var fallback =
|
|
2690
|
+
var fallback = defaultStatusText(state, language);
|
|
2509
2691
|
var text = typeof stored.text === 'string' ? stored.text : fallback;
|
|
2510
2692
|
var fallbackAnimation = Object.prototype.hasOwnProperty.call(DEFAULT_STATUS_ANIMATION, state)
|
|
2511
2693
|
? DEFAULT_STATUS_ANIMATION[state]
|
|
@@ -3634,11 +3816,17 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3634
3816
|
* clause is DROPPED — DSH's own token-free 「深度求索中」 rather than a number invented here. Passed per call so the
|
|
3635
3817
|
* two surfaces (the app's pet and the floating window) can only differ by what they are told, never by the rule.
|
|
3636
3818
|
*
|
|
3819
|
+
* `language` is the SIXTH parameter and it reaches two places: which shipped default an empty box falls back to
|
|
3820
|
+
* ({@link DEFAULT_STATUS_TEXT}), and the units a `{duration}` clause is written in — DSH writes 1分30秒 in one
|
|
3821
|
+
* language and 1m 30s in the other, so a line translated without its clock would read as a foreign number.
|
|
3822
|
+
* Optional, like the status-entry language above: a caller that has no language to pass keeps the Chinese answer
|
|
3823
|
+
* every existing suite pins.
|
|
3824
|
+
*
|
|
3637
3825
|
* @returns {{state: string, text: string, tag: string, animation: string, seconds: number}|null}
|
|
3638
3826
|
*/
|
|
3639
|
-
function statusFor(preferences, state, enteredAt, now, clock) {
|
|
3827
|
+
function statusFor(preferences, state, enteredAt, now, clock, language) {
|
|
3640
3828
|
if (STATUS_STATES.indexOf(state) === -1) return null;
|
|
3641
|
-
var entry = statusEntryFor(preferences, state);
|
|
3829
|
+
var entry = statusEntryFor(preferences, state, language);
|
|
3642
3830
|
if (!entry.text && !entry.tag) return null;
|
|
3643
3831
|
var at = Number.isFinite(now) ? now : Date.now();
|
|
3644
3832
|
/**
|
|
@@ -3691,7 +3879,7 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
3691
3879
|
}
|
|
3692
3880
|
text = elapsed === null
|
|
3693
3881
|
? durationClauseRemoved(text)
|
|
3694
|
-
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed));
|
|
3882
|
+
: text.split(STATUS_DURATION_TOKEN).join(formatRunDuration(elapsed, language));
|
|
3695
3883
|
}
|
|
3696
3884
|
var look = statusTone(state);
|
|
3697
3885
|
return {
|
|
@@ -7307,6 +7495,14 @@ var MOTION_STATES = STATUS_STATES.concat(INTERACTION_STATES, ['dancing']);
|
|
|
7307
7495
|
STATES: STATES,
|
|
7308
7496
|
INTERACTION_STATES: INTERACTION_STATES,
|
|
7309
7497
|
STATUS_STATES: STATUS_STATES,
|
|
7498
|
+
LANGUAGE_SETTINGS: LANGUAGE_SETTINGS,
|
|
7499
|
+
cleanLanguage: cleanLanguage,
|
|
7500
|
+
resolveLanguage: resolveLanguage,
|
|
7501
|
+
browserLanguageHints: browserLanguageHints,
|
|
7502
|
+
textFor: textFor,
|
|
7503
|
+
textOf: textOf,
|
|
7504
|
+
applyI18n: applyI18n,
|
|
7505
|
+
defaultStatusText: defaultStatusText,
|
|
7310
7506
|
DEFAULT_STATUS_TEXT: DEFAULT_STATUS_TEXT,
|
|
7311
7507
|
STATUS_TAGS: STATUS_TAGS,
|
|
7312
7508
|
STATUS_ANIMATIONS: STATUS_ANIMATIONS,
|