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 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: 'VRM / glTF 模型', extensions: ['vrm', 'glb', 'gltf'] }],
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;
@@ -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', { kind: 'model' }),
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', { kind: 'motion' }),
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.
@@ -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 = Object.prototype.hasOwnProperty.call(DEFAULT_STATUS_TEXT, state) ? DEFAULT_STATUS_TEXT[state] : '';
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,