@chenpu17/tcpmapping 2.1.1 → 2.4.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/CHANGELOG.md +108 -0
- package/README.md +113 -10
- package/bin/tcpmapping.js +476 -176
- package/docs/API.md +532 -0
- package/docs/screenshots/dashboard-en-dark.webp +0 -0
- package/docs/screenshots/dashboard-mobile-zh.webp +0 -0
- package/docs/screenshots/dashboard-zh-light.webp +0 -0
- package/docs/screenshots/login-zh-light.webp +0 -0
- package/docs//345/212/237/350/203/275/346/200/273/347/273/223.md +200 -0
- package/docs//345/256/211/345/205/250/351/205/215/347/275/256/350/257/264/346/230/216.md +154 -0
- package/package.json +6 -4
- package/public/common.js +411 -0
- package/public/dashboard.html +1624 -437
- package/public/login.html +430 -107
- package/server.js +1219 -464
- package/setup-password.js +137 -57
package/public/common.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享前端工具:多语言(zh-CN / en-US)与深色模式。
|
|
3
|
+
* 通过 data-i18n* 属性标记静态文案,动态文案使用 I18n.t(key, params)。
|
|
4
|
+
*/
|
|
5
|
+
(function (global) {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const LANG_STORAGE_KEY = 'tcpmapping.lang';
|
|
9
|
+
const THEME_STORAGE_KEY = 'tcpmapping.theme';
|
|
10
|
+
|
|
11
|
+
const messages = {
|
|
12
|
+
'zh-CN': {
|
|
13
|
+
'page.dashboard': 'TCP转发服务器 - 管理面板',
|
|
14
|
+
'page.login': 'TCP转发服务器 - 登录',
|
|
15
|
+
'brand.name': 'TCP 转发服务器',
|
|
16
|
+
'brand.subtitle': '管理面板',
|
|
17
|
+
'header.welcome': '欢迎,{name}',
|
|
18
|
+
'header.welcomeFallback': '管理员',
|
|
19
|
+
'header.logout': '退出登录',
|
|
20
|
+
|
|
21
|
+
'common.refresh': '刷新',
|
|
22
|
+
'common.cancel': '取消',
|
|
23
|
+
'common.edit': '编辑',
|
|
24
|
+
'common.delete': '删除',
|
|
25
|
+
'common.start': '启动',
|
|
26
|
+
'common.stop': '停止',
|
|
27
|
+
'common.running': '运行中',
|
|
28
|
+
'common.stopped': '已停止',
|
|
29
|
+
'common.connected': '已连接',
|
|
30
|
+
'common.connecting': '连接中',
|
|
31
|
+
'common.language': '语言',
|
|
32
|
+
'common.theme': '切换深色 / 浅色模式',
|
|
33
|
+
'common.requestFailed': '请求失败 (HTTP {status})',
|
|
34
|
+
'common.invalidResponse': '服务器返回了无效响应',
|
|
35
|
+
'common.sessionExpired': '登录状态已失效',
|
|
36
|
+
'common.networkError': '操作失败,请检查网络连接',
|
|
37
|
+
|
|
38
|
+
'stats.title': '系统状态',
|
|
39
|
+
'stats.totalMappings': '总映射数',
|
|
40
|
+
'stats.activeMappings': '活跃映射',
|
|
41
|
+
'stats.activeConnections': '活跃连接',
|
|
42
|
+
'stats.uptime': '运行时间',
|
|
43
|
+
'stats.totalTraffic': '累计流量',
|
|
44
|
+
|
|
45
|
+
'resources.title': '服务资源',
|
|
46
|
+
'resources.cpu': 'CPU 使用率',
|
|
47
|
+
'resources.cpuHint': '{count} 核 · 占整机',
|
|
48
|
+
'resources.memory': '内存占用',
|
|
49
|
+
'resources.memoryHint': '占系统内存 {percent}% · 堆 {heap}',
|
|
50
|
+
'resources.traffic': '实时流量',
|
|
51
|
+
'resources.trafficHint': '累计 ↑{up} ↓{down}',
|
|
52
|
+
'resources.sampledAt': '采样于 {time}',
|
|
53
|
+
'resources.waiting': '等待采样…',
|
|
54
|
+
|
|
55
|
+
'mappings.title': '端口映射管理',
|
|
56
|
+
'mappings.add': '+ 添加映射',
|
|
57
|
+
'mappings.empty': '暂无端口映射,点击上方按钮添加',
|
|
58
|
+
'mappings.thName': '名称',
|
|
59
|
+
'mappings.thListen': '监听端口',
|
|
60
|
+
'mappings.thTarget': '目标地址',
|
|
61
|
+
'mappings.thConnections': '连接数',
|
|
62
|
+
'mappings.thTraffic': '流量统计',
|
|
63
|
+
'mappings.thStatus': '状态',
|
|
64
|
+
'mappings.thActions': '操作',
|
|
65
|
+
'mappings.currentTraffic': '当前 ↑{up} ↓{down}',
|
|
66
|
+
'mappings.totalUpload': '累计上行流量',
|
|
67
|
+
'mappings.totalDownload': '累计下行流量',
|
|
68
|
+
|
|
69
|
+
'connections.title': '连接状态监控',
|
|
70
|
+
'connections.empty': '当前没有活跃连接',
|
|
71
|
+
'connections.activeCount': '活跃连接: {count}',
|
|
72
|
+
'connections.actualListenLabel': '实际监听:',
|
|
73
|
+
|
|
74
|
+
'history.title': '最近连接记录',
|
|
75
|
+
'history.hint': '记录已关闭连接的源地址、目的地址与持续时长,最多保留最近 500 条(服务重启后清空)。',
|
|
76
|
+
'history.thClosedAt': '关闭时间',
|
|
77
|
+
'history.thMapping': '映射',
|
|
78
|
+
'history.thSource': '源地址',
|
|
79
|
+
'history.thDestination': '目的地址',
|
|
80
|
+
'history.thDuration': '持续时长',
|
|
81
|
+
'history.thTraffic': '流量',
|
|
82
|
+
'history.thReason': '结束原因',
|
|
83
|
+
'history.empty': '暂无已关闭的连接记录',
|
|
84
|
+
'history.notEstablished': '(未建立)',
|
|
85
|
+
'history.allMappings': '全部映射',
|
|
86
|
+
|
|
87
|
+
'modal.addTitle': '添加端口映射',
|
|
88
|
+
'modal.editTitle': '编辑端口映射',
|
|
89
|
+
'modal.name': '映射名称',
|
|
90
|
+
'modal.namePlaceholder': '例如:Web服务',
|
|
91
|
+
'modal.listenHost': '监听地址',
|
|
92
|
+
'modal.listenHostPlaceholder': '0.0.0.0 表示所有网卡',
|
|
93
|
+
'modal.listenPort': '监听端口',
|
|
94
|
+
'modal.targetHost': '目标主机',
|
|
95
|
+
'modal.targetHostPlaceholder': 'IP地址或域名',
|
|
96
|
+
'modal.targetPort': '目标端口',
|
|
97
|
+
'modal.portPlaceholder': '1-65535',
|
|
98
|
+
'modal.confirmAdd': '确认添加',
|
|
99
|
+
'modal.saveChanges': '保存修改',
|
|
100
|
+
'modal.saving': '保存中...',
|
|
101
|
+
'modal.saveSuccess': '保存成功',
|
|
102
|
+
'modal.addSuccess': '添加成功',
|
|
103
|
+
'modal.operationFailed': '操作失败',
|
|
104
|
+
'modal.mappingMissing': '映射不存在,请刷新后重试',
|
|
105
|
+
|
|
106
|
+
'actions.deleteConfirm': '确定要删除映射「{name}」吗?',
|
|
107
|
+
'actions.deleteFailed': '删除失败: {message}',
|
|
108
|
+
'actions.startFailed': '启动失败: {message}',
|
|
109
|
+
'actions.stopFailed': '停止失败: {message}',
|
|
110
|
+
'actions.deleted': '映射已删除',
|
|
111
|
+
'actions.started': '映射已启动',
|
|
112
|
+
'actions.stopped': '映射已停止',
|
|
113
|
+
|
|
114
|
+
'login.subtitle': '管理界面登录',
|
|
115
|
+
'login.username': '用户名',
|
|
116
|
+
'login.usernamePlaceholder': '请输入用户名',
|
|
117
|
+
'login.password': '密码',
|
|
118
|
+
'login.passwordPlaceholder': '请输入密码',
|
|
119
|
+
'login.showPassword': '显示密码',
|
|
120
|
+
'login.hidePassword': '隐藏密码',
|
|
121
|
+
'login.submit': '登录',
|
|
122
|
+
'login.submitting': '登录中...',
|
|
123
|
+
'login.success': '登录成功,正在跳转...',
|
|
124
|
+
'login.failedNetwork': '登录失败,请检查网络连接',
|
|
125
|
+
'login.forgot': '忘记密码?运行 <code>tcpmapping setup-password</code> 或 <code>npm run reset-password</code> 重置',
|
|
126
|
+
'login.footer': 'TCP 端口转发服务器'
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
'en-US': {
|
|
130
|
+
'page.dashboard': 'TCP Forwarding Server - Admin',
|
|
131
|
+
'page.login': 'TCP Forwarding Server - Sign In',
|
|
132
|
+
'brand.name': 'TCP Forwarding Server',
|
|
133
|
+
'brand.subtitle': 'Admin Dashboard',
|
|
134
|
+
'header.welcome': 'Welcome, {name}',
|
|
135
|
+
'header.welcomeFallback': 'Administrator',
|
|
136
|
+
'header.logout': 'Log out',
|
|
137
|
+
|
|
138
|
+
'common.refresh': 'Refresh',
|
|
139
|
+
'common.cancel': 'Cancel',
|
|
140
|
+
'common.edit': 'Edit',
|
|
141
|
+
'common.delete': 'Delete',
|
|
142
|
+
'common.start': 'Start',
|
|
143
|
+
'common.stop': 'Stop',
|
|
144
|
+
'common.running': 'Running',
|
|
145
|
+
'common.stopped': 'Stopped',
|
|
146
|
+
'common.connected': 'Connected',
|
|
147
|
+
'common.connecting': 'Connecting',
|
|
148
|
+
'common.language': 'Language',
|
|
149
|
+
'common.theme': 'Toggle dark / light mode',
|
|
150
|
+
'common.requestFailed': 'Request failed (HTTP {status})',
|
|
151
|
+
'common.invalidResponse': 'The server returned an invalid response',
|
|
152
|
+
'common.sessionExpired': 'Session expired',
|
|
153
|
+
'common.networkError': 'Operation failed. Check your network connection.',
|
|
154
|
+
|
|
155
|
+
'stats.title': 'System Status',
|
|
156
|
+
'stats.totalMappings': 'Total mappings',
|
|
157
|
+
'stats.activeMappings': 'Active mappings',
|
|
158
|
+
'stats.activeConnections': 'Active connections',
|
|
159
|
+
'stats.uptime': 'Uptime',
|
|
160
|
+
'stats.totalTraffic': 'Total traffic',
|
|
161
|
+
|
|
162
|
+
'resources.title': 'Service Resources',
|
|
163
|
+
'resources.cpu': 'CPU usage',
|
|
164
|
+
'resources.cpuHint': '{count} cores · of total',
|
|
165
|
+
'resources.memory': 'Memory',
|
|
166
|
+
'resources.memoryHint': '{percent}% of system memory · heap {heap}',
|
|
167
|
+
'resources.traffic': 'Live traffic',
|
|
168
|
+
'resources.trafficHint': 'Total ↑{up} ↓{down}',
|
|
169
|
+
'resources.sampledAt': 'Sampled at {time}',
|
|
170
|
+
'resources.waiting': 'Waiting for sample…',
|
|
171
|
+
|
|
172
|
+
'mappings.title': 'Port Mappings',
|
|
173
|
+
'mappings.add': '+ Add Mapping',
|
|
174
|
+
'mappings.empty': 'No mappings yet. Click “Add Mapping” to create one.',
|
|
175
|
+
'mappings.thName': 'Name',
|
|
176
|
+
'mappings.thListen': 'Listen',
|
|
177
|
+
'mappings.thTarget': 'Target',
|
|
178
|
+
'mappings.thConnections': 'Connections',
|
|
179
|
+
'mappings.thTraffic': 'Traffic',
|
|
180
|
+
'mappings.thStatus': 'Status',
|
|
181
|
+
'mappings.thActions': 'Actions',
|
|
182
|
+
'mappings.currentTraffic': 'Current ↑{up} ↓{down}',
|
|
183
|
+
'mappings.totalUpload': 'Total upload',
|
|
184
|
+
'mappings.totalDownload': 'Total download',
|
|
185
|
+
|
|
186
|
+
'connections.title': 'Live Connections',
|
|
187
|
+
'connections.empty': 'No active connections',
|
|
188
|
+
'connections.activeCount': 'Active connections: {count}',
|
|
189
|
+
'connections.actualListenLabel': 'Local address:',
|
|
190
|
+
|
|
191
|
+
'history.title': 'Recent Connections',
|
|
192
|
+
'history.hint': 'Closed connections with source, destination and duration. Keeps the latest 500 records (cleared on restart).',
|
|
193
|
+
'history.thClosedAt': 'Closed At',
|
|
194
|
+
'history.thMapping': 'Mapping',
|
|
195
|
+
'history.thSource': 'Source',
|
|
196
|
+
'history.thDestination': 'Destination',
|
|
197
|
+
'history.thDuration': 'Duration',
|
|
198
|
+
'history.thTraffic': 'Traffic',
|
|
199
|
+
'history.thReason': 'Close Reason',
|
|
200
|
+
'history.empty': 'No closed connection records yet',
|
|
201
|
+
'history.notEstablished': ' (not established)',
|
|
202
|
+
'history.allMappings': 'All mappings',
|
|
203
|
+
|
|
204
|
+
'modal.addTitle': 'Add Port Mapping',
|
|
205
|
+
'modal.editTitle': 'Edit Port Mapping',
|
|
206
|
+
'modal.name': 'Mapping Name',
|
|
207
|
+
'modal.namePlaceholder': 'e.g. Web Service',
|
|
208
|
+
'modal.listenHost': 'Listen Address',
|
|
209
|
+
'modal.listenHostPlaceholder': '0.0.0.0 means all interfaces',
|
|
210
|
+
'modal.listenPort': 'Listen Port',
|
|
211
|
+
'modal.targetHost': 'Target Host',
|
|
212
|
+
'modal.targetHostPlaceholder': 'IP address or hostname',
|
|
213
|
+
'modal.targetPort': 'Target Port',
|
|
214
|
+
'modal.portPlaceholder': '1-65535',
|
|
215
|
+
'modal.confirmAdd': 'Add',
|
|
216
|
+
'modal.saveChanges': 'Save Changes',
|
|
217
|
+
'modal.saving': 'Saving...',
|
|
218
|
+
'modal.saveSuccess': 'Saved successfully',
|
|
219
|
+
'modal.addSuccess': 'Added successfully',
|
|
220
|
+
'modal.operationFailed': 'Operation failed',
|
|
221
|
+
'modal.mappingMissing': 'Mapping not found. Refresh and try again.',
|
|
222
|
+
|
|
223
|
+
'actions.deleteConfirm': 'Delete mapping “{name}”?',
|
|
224
|
+
'actions.deleteFailed': 'Delete failed: {message}',
|
|
225
|
+
'actions.startFailed': 'Start failed: {message}',
|
|
226
|
+
'actions.stopFailed': 'Stop failed: {message}',
|
|
227
|
+
'actions.deleted': 'Mapping deleted',
|
|
228
|
+
'actions.started': 'Mapping started',
|
|
229
|
+
'actions.stopped': 'Mapping stopped',
|
|
230
|
+
|
|
231
|
+
'login.subtitle': 'Sign in to the admin panel',
|
|
232
|
+
'login.username': 'Username',
|
|
233
|
+
'login.usernamePlaceholder': 'Enter your username',
|
|
234
|
+
'login.password': 'Password',
|
|
235
|
+
'login.passwordPlaceholder': 'Enter your password',
|
|
236
|
+
'login.showPassword': 'Show password',
|
|
237
|
+
'login.hidePassword': 'Hide password',
|
|
238
|
+
'login.submit': 'Sign in',
|
|
239
|
+
'login.submitting': 'Signing in...',
|
|
240
|
+
'login.success': 'Signed in, redirecting...',
|
|
241
|
+
'login.failedNetwork': 'Sign-in failed. Check your network connection.',
|
|
242
|
+
'login.forgot': 'Forgot your password? Run <code>tcpmapping setup-password</code> or <code>npm run reset-password</code> to reset it.',
|
|
243
|
+
'login.footer': 'TCP port forwarding server'
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const SUPPORTED_LANGUAGES = ['zh-CN', 'en-US'];
|
|
248
|
+
let currentLanguage = detectLanguage();
|
|
249
|
+
|
|
250
|
+
function detectLanguage() {
|
|
251
|
+
try {
|
|
252
|
+
const saved = localStorage.getItem(LANG_STORAGE_KEY);
|
|
253
|
+
if (messages[saved]) {
|
|
254
|
+
return saved;
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// localStorage 不可用时忽略
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const candidates = navigator.languages && navigator.languages.length
|
|
261
|
+
? navigator.languages
|
|
262
|
+
: [navigator.language || ''];
|
|
263
|
+
|
|
264
|
+
for (const candidate of candidates) {
|
|
265
|
+
const value = String(candidate).toLowerCase();
|
|
266
|
+
if (value.startsWith('zh')) return 'zh-CN';
|
|
267
|
+
if (value.startsWith('en')) return 'en-US';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return 'zh-CN';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function t(key, params) {
|
|
274
|
+
const dictionary = messages[currentLanguage] || messages['zh-CN'];
|
|
275
|
+
const template = dictionary[key] !== undefined
|
|
276
|
+
? dictionary[key]
|
|
277
|
+
: (messages['zh-CN'][key] !== undefined ? messages['zh-CN'][key] : key);
|
|
278
|
+
|
|
279
|
+
return String(template).replace(/\{(\w+)\}/g, (match, name) => {
|
|
280
|
+
return params && params[name] !== undefined ? String(params[name]) : match;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function applyStatic(root) {
|
|
285
|
+
const scope = root || document;
|
|
286
|
+
|
|
287
|
+
scope.querySelectorAll('[data-i18n]').forEach(element => {
|
|
288
|
+
element.textContent = t(element.getAttribute('data-i18n'));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
scope.querySelectorAll('[data-i18n-html]').forEach(element => {
|
|
292
|
+
element.innerHTML = t(element.getAttribute('data-i18n-html'));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
scope.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
|
|
296
|
+
element.setAttribute('placeholder', t(element.getAttribute('data-i18n-placeholder')));
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
scope.querySelectorAll('[data-i18n-title]').forEach(element => {
|
|
300
|
+
element.setAttribute('title', t(element.getAttribute('data-i18n-title')));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
scope.querySelectorAll('[data-i18n-aria-label]').forEach(element => {
|
|
304
|
+
element.setAttribute('aria-label', t(element.getAttribute('data-i18n-aria-label')));
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function setLanguage(language) {
|
|
309
|
+
if (!messages[language]) {
|
|
310
|
+
return currentLanguage;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
currentLanguage = language;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
localStorage.setItem(LANG_STORAGE_KEY, language);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
// 忽略存储失败
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
document.documentElement.setAttribute('lang', language === 'zh-CN' ? 'zh-CN' : 'en');
|
|
322
|
+
applyStatic();
|
|
323
|
+
|
|
324
|
+
const select = document.getElementById('languageSelect');
|
|
325
|
+
if (select) {
|
|
326
|
+
select.value = language;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
document.dispatchEvent(new CustomEvent('languagechange', { detail: { language } }));
|
|
330
|
+
return currentLanguage;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getLanguage() {
|
|
334
|
+
return currentLanguage;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
document.documentElement.setAttribute('lang', currentLanguage === 'zh-CN' ? 'zh-CN' : 'en');
|
|
338
|
+
|
|
339
|
+
// ==================== 主题 ====================
|
|
340
|
+
|
|
341
|
+
function getPreferredTheme() {
|
|
342
|
+
try {
|
|
343
|
+
const saved = localStorage.getItem(THEME_STORAGE_KEY);
|
|
344
|
+
if (saved === 'dark' || saved === 'light') {
|
|
345
|
+
return saved;
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
// 忽略存储失败
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return global.matchMedia && global.matchMedia('(prefers-color-scheme: dark)').matches
|
|
352
|
+
? 'dark'
|
|
353
|
+
: 'light';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function applyTheme(theme) {
|
|
357
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
358
|
+
document.querySelectorAll('[data-theme-toggle]').forEach(button => {
|
|
359
|
+
button.setAttribute('aria-pressed', theme === 'dark' ? 'true' : 'false');
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function setTheme(theme) {
|
|
364
|
+
const value = theme === 'dark' ? 'dark' : 'light';
|
|
365
|
+
try {
|
|
366
|
+
localStorage.setItem(THEME_STORAGE_KEY, value);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
// 忽略存储失败
|
|
369
|
+
}
|
|
370
|
+
applyTheme(value);
|
|
371
|
+
document.dispatchEvent(new CustomEvent('themechange', { detail: { theme: value } }));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function toggleTheme() {
|
|
375
|
+
setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 在页面渲染前应用主题,避免闪烁
|
|
379
|
+
applyTheme(getPreferredTheme());
|
|
380
|
+
|
|
381
|
+
document.addEventListener('click', event => {
|
|
382
|
+
const toggle = event.target.closest('[data-theme-toggle]');
|
|
383
|
+
if (toggle) {
|
|
384
|
+
toggleTheme();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
389
|
+
applyStatic();
|
|
390
|
+
|
|
391
|
+
const select = document.getElementById('languageSelect');
|
|
392
|
+
if (select) {
|
|
393
|
+
select.value = currentLanguage;
|
|
394
|
+
select.addEventListener('change', event => setLanguage(event.target.value));
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
global.I18n = {
|
|
399
|
+
t,
|
|
400
|
+
applyStatic,
|
|
401
|
+
setLanguage,
|
|
402
|
+
getLanguage,
|
|
403
|
+
supportedLanguages: SUPPORTED_LANGUAGES
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
global.Theme = {
|
|
407
|
+
setTheme,
|
|
408
|
+
toggleTheme,
|
|
409
|
+
current: () => document.documentElement.getAttribute('data-theme')
|
|
410
|
+
};
|
|
411
|
+
})(window);
|