itismyskillmarket 1.3.17 → 1.3.18

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/gui/app.js CHANGED
@@ -15,6 +15,491 @@ const state = {
15
15
  previousView: 'skills',
16
16
  };
17
17
 
18
+ // -----------------------------------------------------------------------------
19
+ // i18n 国际化系统
20
+ // -----------------------------------------------------------------------------
21
+
22
+ const translations = {
23
+ en: {
24
+ // 导航
25
+ 'nav.skills': 'Skills',
26
+ 'nav.installed': 'Installed',
27
+ 'nav.platforms': 'Platforms',
28
+ 'nav.admin': 'Admin',
29
+ 'nav.help': 'Help',
30
+ 'nav.back': 'Back',
31
+
32
+ // 视图标题
33
+ 'title.availableSkills': 'Available Skills',
34
+ 'title.installedSkills': 'Installed Skills',
35
+ 'title.platforms': 'Available Platforms',
36
+ 'title.help': 'Help & Configuration',
37
+ 'title.admin': 'Admin Dashboard',
38
+
39
+ // 按钮
40
+ 'btn.install': 'Install',
41
+ 'btn.uninstall': 'Uninstall',
42
+ 'btn.update': 'Update',
43
+ 'btn.refresh': '🔄 Refresh',
44
+ 'btn.updateAll': '🔄 Update All',
45
+ 'btn.info': 'Info',
46
+ 'btn.confirm': 'Confirm',
47
+ 'btn.cancel': 'Cancel',
48
+
49
+ // 加载状态
50
+ 'loading.skills': 'Loading skills...',
51
+ 'loading.platforms': 'Loading platforms...',
52
+ 'loading.details': 'Loading skill details...',
53
+ 'loading.stats': 'Loading stats...',
54
+ 'loading.published': 'Loading published skills...',
55
+ 'loading.generic': 'Loading...',
56
+
57
+ // 空状态
58
+ 'empty.noSkills': 'No skills found',
59
+ 'empty.noPlatforms': 'No platforms found',
60
+ 'empty.noPublishedSkills': 'No published skills found',
61
+ 'empty.noTags': 'No dist-tags found.',
62
+ 'empty.couldNotLoadTags': 'Could not load tags.',
63
+
64
+ // 搜索
65
+ 'search.placeholder': '🔍 Search skills...',
66
+
67
+ // 分页
68
+ 'pagination.prev': '← Prev',
69
+ 'pagination.next': 'Next →',
70
+ 'pagination.pageInfo': 'Page {page} of {totalPages}',
71
+
72
+ // 每页数量
73
+ 'pageSize.10': '10 per page',
74
+ 'pageSize.20': '20 per page',
75
+ 'pageSize.50': '50 per page',
76
+
77
+ // 状态
78
+ 'status.available': '✅ Available',
79
+ 'status.unavailable': '❌ Not detected',
80
+ 'status.skillsInstalled': '{count} skills installed',
81
+
82
+ // 详情视图
83
+ 'detail.description': 'Description',
84
+ 'detail.details': 'Details',
85
+ 'detail.id': 'ID',
86
+ 'detail.version': 'Version',
87
+ 'detail.license': 'License',
88
+ 'detail.author': 'Author',
89
+ 'detail.homepage': 'Homepage',
90
+ 'detail.repository': 'Repository',
91
+ 'detail.platforms': 'Platforms',
92
+ 'detail.versions': 'Versions (last {count})',
93
+ 'detail.latest': 'latest',
94
+ 'detail.noDescription': 'No description',
95
+ 'detail.nA': 'N/A',
96
+
97
+ // Toast 消息
98
+ 'toast.installing': 'Installing {skillId}...',
99
+ 'toast.installSuccess': '{skillId} installed successfully!',
100
+ 'toast.uninstalling': 'Uninstalling {skillId}...',
101
+ 'toast.uninstallSuccess': '{skillId} uninstalled!',
102
+ 'toast.updating': 'Updating {skillId}...',
103
+ 'toast.updateSuccess': '{skillId} updated!',
104
+ 'toast.updateAll': 'Updating all skills...',
105
+ 'toast.updateAllSuccess': 'All skills updated!',
106
+
107
+ // 确认对话框
108
+ 'confirm.uninstall': 'Are you sure you want to uninstall {skillId}?',
109
+ 'confirm.updateAll': 'Update all installed skills?',
110
+
111
+ // 警告
112
+ 'warning.fetchErrors': '⚠ {count} skill(s) failed to load details from npm registry. Refresh to retry.',
113
+
114
+ // Admin 视图
115
+ 'admin.publishedSkillsCount': 'Published Skills ({count})',
116
+ 'admin.stats.totalSkills': 'Published Skills',
117
+ 'admin.stats.totalVersions': 'Total Versions',
118
+ 'admin.stats.avgVersions': 'Avg Versions/Skill',
119
+ 'admin.stats.withMetadata': 'With Metadata',
120
+ 'admin.stats.totalSize': '{value} MB',
121
+ 'admin.stats.platformsCovered': 'Platforms Covered',
122
+
123
+ // Admin Modal - Deprecate
124
+ 'admin.deprecating': 'Deprecating {skillId}...',
125
+ 'admin.deprecateTitle': 'Deprecate: {skillId}',
126
+ 'admin.deprecateVersion': 'Version',
127
+ 'admin.deprecateVersionPlaceholder': '(leave empty for all versions)',
128
+ 'admin.deprecateMessage': 'Message',
129
+ 'admin.deprecateDefaultMsg': 'This skill is deprecated. Please use an alternative.',
130
+ 'admin.deprecateWarning': '⚠ Deprecating will mark this skill as deprecated in the npm registry.',
131
+ 'admin.confirmDeprecate': 'Confirm Deprecate',
132
+
133
+ // Admin Modal - Unpublish
134
+ 'admin.unpublishing': 'Unpublishing {skillId}...',
135
+ 'admin.unpublishTitle': 'Unpublish: {skillId}',
136
+ 'admin.unpublishVersion': 'Version',
137
+ 'admin.unpublishVersionPlaceholder': '(leave empty for entire package)',
138
+ 'admin.unpublishForce': 'Force unpublish entire package (required if no version specified)',
139
+ 'admin.unpublishDanger': '⚠ This action cannot be undone! Packages can be restored within 72 hours.',
140
+ 'admin.confirmUnpublish': 'Confirm Unpublish',
141
+
142
+ // Admin Modal - Tags
143
+ 'admin.settingTag': 'Setting tag {tag}...',
144
+ 'admin.removingTag': 'Removing tag {tag}...',
145
+ 'admin.tagsTitle': 'Tags: {skillId}',
146
+ 'admin.currentTags': 'Current Tags',
147
+ 'admin.setTag': 'Set Tag',
148
+ 'admin.removeTag': 'Remove Tag',
149
+ 'admin.tagName': 'Tag',
150
+ 'admin.tagNamePlaceholder': 'e.g. beta, latest',
151
+ 'admin.tagVersion': 'Version',
152
+ 'admin.tagVersionPlaceholder': 'e.g. 1.0.1',
153
+ 'admin.tagNameRemovePlaceholder': 'e.g. beta',
154
+ 'admin.tag.default': ' (default)',
155
+ 'admin.tagRequired': 'Tag and version are required',
156
+ 'admin.tagNameRequired': 'Tag name is required',
157
+
158
+ // Admin Modal - Owners
159
+ 'admin.addingOwner': 'Adding owner {user}...',
160
+ 'admin.removingOwner': 'Removing owner {user}...',
161
+ 'admin.ownersTitle': 'Owners: {skillId}',
162
+ 'admin.addOwner': 'Add Owner',
163
+ 'admin.removeOwner': 'Remove Owner',
164
+ 'admin.npmUser': 'npm User',
165
+ 'admin.npmUserPlaceholder': 'npm username',
166
+ 'admin.usernameRequired': 'Username is required',
167
+
168
+ // Admin Modal - Access
169
+ 'admin.settingAccess': 'Setting access to {level}...',
170
+ 'admin.accessTitle': 'Access: {skillId}',
171
+ 'admin.accessDesc': 'Set the package access level. Public packages are visible to everyone. Restricted packages require authentication to install.',
172
+ 'admin.accessPublic': 'Public',
173
+ 'admin.accessRestricted': 'Restricted',
174
+ 'admin.setAccess': 'Set Access',
175
+
176
+ // Help 视图
177
+ 'help.envVars': 'Environment Variables',
178
+ 'help.setEnvVars': 'Set the following environment variables to override defaults:',
179
+ 'help.envVar.primaryScope': 'Primary npm scope for publishing/lookup',
180
+ 'help.envVar.fallbackScope': 'Fallback scope (backward compatibility)',
181
+ 'help.envVar.scopeList': 'Comma-separated list of scopes to search',
182
+ 'help.envVar.registryUrl': 'npm registry URL',
183
+ 'help.envVar.personalLink': 'Personal link prefix (for publish output)',
184
+ 'help.commands': 'Common Commands',
185
+
186
+ // 语言
187
+ 'lang.en': 'EN',
188
+ 'lang.zh': '中',
189
+
190
+ // 通用错误
191
+ 'error.generic': 'Error',
192
+ },
193
+
194
+ zh: {
195
+ // 导航
196
+ 'nav.skills': '技能',
197
+ 'nav.installed': '已安装',
198
+ 'nav.platforms': '平台',
199
+ 'nav.admin': '管理',
200
+ 'nav.help': '帮助',
201
+ 'nav.back': '返回',
202
+
203
+ // 视图标题
204
+ 'title.availableSkills': '可用技能',
205
+ 'title.installedSkills': '已安装技能',
206
+ 'title.platforms': '可用平台',
207
+ 'title.help': '帮助与配置',
208
+ 'title.admin': '管理面板',
209
+
210
+ // 按钮
211
+ 'btn.install': '安装',
212
+ 'btn.uninstall': '卸载',
213
+ 'btn.update': '更新',
214
+ 'btn.refresh': '🔄 刷新',
215
+ 'btn.updateAll': '🔄 全部更新',
216
+ 'btn.info': '详情',
217
+ 'btn.confirm': '确认',
218
+ 'btn.cancel': '取消',
219
+
220
+ // 加载状态
221
+ 'loading.skills': '加载技能中...',
222
+ 'loading.platforms': '加载平台中...',
223
+ 'loading.details': '加载技能详情中...',
224
+ 'loading.stats': '加载统计中...',
225
+ 'loading.published': '加载已发布技能中...',
226
+ 'loading.generic': '加载中...',
227
+
228
+ // 空状态
229
+ 'empty.noSkills': '未找到技能',
230
+ 'empty.noPlatforms': '未找到平台',
231
+ 'empty.noPublishedSkills': '未找到已发布的技能',
232
+ 'empty.noTags': '暂无 dist-tags。',
233
+ 'empty.couldNotLoadTags': '无法加载 tags。',
234
+
235
+ // 搜索
236
+ 'search.placeholder': '🔍 搜索技能...',
237
+
238
+ // 分页
239
+ 'pagination.prev': '← 上一页',
240
+ 'pagination.next': '下一页 →',
241
+ 'pagination.pageInfo': '第 {page} 页 / 共 {totalPages} 页',
242
+
243
+ // 每页数量
244
+ 'pageSize.10': '每页 10 条',
245
+ 'pageSize.20': '每页 20 条',
246
+ 'pageSize.50': '每页 50 条',
247
+
248
+ // 状态
249
+ 'status.available': '✅ 可用',
250
+ 'status.unavailable': '❌ 未检测到',
251
+ 'status.skillsInstalled': '已安装 {count} 个技能',
252
+
253
+ // 详情视图
254
+ 'detail.description': '描述',
255
+ 'detail.details': '详细信息',
256
+ 'detail.id': 'ID',
257
+ 'detail.version': '版本',
258
+ 'detail.license': '许可证',
259
+ 'detail.author': '作者',
260
+ 'detail.homepage': '主页',
261
+ 'detail.repository': '代码仓库',
262
+ 'detail.platforms': '支持平台',
263
+ 'detail.versions': '版本记录(最近 {count} 个)',
264
+ 'detail.latest': '最新',
265
+ 'detail.noDescription': '暂无描述',
266
+ 'detail.nA': 'N/A',
267
+
268
+ // Toast 消息
269
+ 'toast.installing': '正在安装 {skillId}...',
270
+ 'toast.installSuccess': '{skillId} 安装成功!',
271
+ 'toast.uninstalling': '正在卸载 {skillId}...',
272
+ 'toast.uninstallSuccess': '{skillId} 已卸载!',
273
+ 'toast.updating': '正在更新 {skillId}...',
274
+ 'toast.updateSuccess': '{skillId} 已更新!',
275
+ 'toast.updateAll': '正在更新所有技能...',
276
+ 'toast.updateAllSuccess': '所有技能已更新!',
277
+
278
+ // 确认对话框
279
+ 'confirm.uninstall': '确定要卸载 {skillId} 吗?',
280
+ 'confirm.updateAll': '更新所有已安装的技能?',
281
+
282
+ // 警告
283
+ 'warning.fetchErrors': '⚠ {count} 个技能从 npm registry 加载详情失败。刷新重试。',
284
+
285
+ // Admin 视图
286
+ 'admin.publishedSkillsCount': '已发布技能({count} 个)',
287
+ 'admin.stats.totalSkills': '已发布技能',
288
+ 'admin.stats.totalVersions': '总版本数',
289
+ 'admin.stats.avgVersions': '平均版本数',
290
+ 'admin.stats.withMetadata': '含元数据',
291
+ 'admin.stats.totalSize': '{value} MB',
292
+ 'admin.stats.platformsCovered': '覆盖平台',
293
+
294
+ // Admin Modal - Deprecate
295
+ 'admin.deprecating': '正在废弃 {skillId}...',
296
+ 'admin.deprecateTitle': '废弃: {skillId}',
297
+ 'admin.deprecateVersion': '版本',
298
+ 'admin.deprecateVersionPlaceholder': '(留空表示所有版本)',
299
+ 'admin.deprecateMessage': '消息',
300
+ 'admin.deprecateDefaultMsg': '此技能已废弃,请使用其他替代方案。',
301
+ 'admin.deprecateWarning': '⚠ 废弃操作将在 npm registry 中标记此技能为已废弃。',
302
+ 'admin.confirmDeprecate': '确认废弃',
303
+
304
+ // Admin Modal - Unpublish
305
+ 'admin.unpublishing': '正在取消发布 {skillId}...',
306
+ 'admin.unpublishTitle': '取消发布: {skillId}',
307
+ 'admin.unpublishVersion': '版本',
308
+ 'admin.unpublishVersionPlaceholder': '(留空表示整个包)',
309
+ 'admin.unpublishForce': '强制取消发布整个包(未指定版本时必须勾选)',
310
+ 'admin.unpublishDanger': '⚠ 此操作不可撤销!包可在 72 小时内恢复。',
311
+ 'admin.confirmUnpublish': '确认取消发布',
312
+
313
+ // Admin Modal - Tags
314
+ 'admin.settingTag': '正在设置标签 {tag}...',
315
+ 'admin.removingTag': '正在移除标签 {tag}...',
316
+ 'admin.tagsTitle': '标签: {skillId}',
317
+ 'admin.currentTags': '当前标签',
318
+ 'admin.setTag': '设置标签',
319
+ 'admin.removeTag': '移除标签',
320
+ 'admin.tagName': '标签名',
321
+ 'admin.tagNamePlaceholder': '例如:beta, latest',
322
+ 'admin.tagVersion': '版本',
323
+ 'admin.tagVersionPlaceholder': '例如:1.0.1',
324
+ 'admin.tagNameRemovePlaceholder': '例如:beta',
325
+ 'admin.tag.default': '(默认)',
326
+ 'admin.tagRequired': '标签和版本不能为空',
327
+ 'admin.tagNameRequired': '标签名不能为空',
328
+
329
+ // Admin Modal - Owners
330
+ 'admin.addingOwner': '正在添加维护者 {user}...',
331
+ 'admin.removingOwner': '正在移除维护者 {user}...',
332
+ 'admin.ownersTitle': '维护者: {skillId}',
333
+ 'admin.addOwner': '添加维护者',
334
+ 'admin.removeOwner': '移除维护者',
335
+ 'admin.npmUser': 'npm 用户名',
336
+ 'admin.npmUserPlaceholder': 'npm 用户名',
337
+ 'admin.usernameRequired': '用户名不能为空',
338
+
339
+ // Admin Modal - Access
340
+ 'admin.settingAccess': '正在设置访问权限为 {level}...',
341
+ 'admin.accessTitle': '访问权限: {skillId}',
342
+ 'admin.accessDesc': '设置包的访问级别。公开包对所有人可见。受限包需要认证才能安装。',
343
+ 'admin.accessPublic': '公开',
344
+ 'admin.accessRestricted': '受限',
345
+ 'admin.setAccess': '设置权限',
346
+
347
+ // Help 视图
348
+ 'help.envVars': '环境变量配置',
349
+ 'help.setEnvVars': '设置以下环境变量可覆盖默认配置:',
350
+ 'help.envVar.primaryScope': '主要 npm scope,用于发布/查找 skill',
351
+ 'help.envVar.fallbackScope': '回退 scope(兼容旧安装)',
352
+ 'help.envVar.scopeList': '搜索时尝试的 scope 列表(逗号分隔)',
353
+ 'help.envVar.registryUrl': 'npm registry 地址',
354
+ 'help.envVar.personalLink': '个人链接前缀(publish 输出用)',
355
+ 'help.commands': '常用命令',
356
+
357
+ // 语言
358
+ 'lang.en': 'EN',
359
+ 'lang.zh': '中',
360
+
361
+ // 通用错误
362
+ 'error.generic': '错误',
363
+ }
364
+ };
365
+
366
+ let currentLanguage = 'en';
367
+
368
+ // 翻译函数 - 支持变量替换
369
+ function t(key, params = {}) {
370
+ let text = translations[currentLanguage]?.[key] || translations['en'][key] || key;
371
+ // 替换变量 {varName}
372
+ Object.keys(params).forEach(k => {
373
+ text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), params[k]);
374
+ });
375
+ return text;
376
+ }
377
+
378
+ // 检测语言(localStorage + 浏览器语言)
379
+ function detectLanguage() {
380
+ const saved = localStorage.getItem('skm-language');
381
+ if (saved && ['en', 'zh'].includes(saved)) {
382
+ return saved;
383
+ }
384
+ // 浏览器语言检测
385
+ const browserLang = navigator.language || navigator.userLanguage || '';
386
+ return browserLang.startsWith('zh') ? 'zh' : 'en';
387
+ }
388
+
389
+ // 设置语言并重新渲染
390
+ function setLanguage(lang) {
391
+ if (!['en', 'zh'].includes(lang)) return;
392
+ currentLanguage = lang;
393
+ localStorage.setItem('skm-language', lang);
394
+
395
+ // 更新 select 显示
396
+ const langSelect = document.getElementById('lang-select');
397
+ if (langSelect) langSelect.value = lang;
398
+
399
+ // 更新语言选项显示
400
+ updateLanguageOptions();
401
+
402
+ // 重新渲染当前视图
403
+ reRenderCurrentView();
404
+ }
405
+
406
+ // 更新语言选项显示(根据当前语言显示选项文本)
407
+ function updateLanguageOptions() {
408
+ const langSelect = document.getElementById('lang-select');
409
+ if (!langSelect) return;
410
+
411
+ langSelect.innerHTML = `
412
+ <option value="en"${currentLanguage === 'en' ? ' selected' : ''}>${t('lang.en')}</option>
413
+ <option value="zh"${currentLanguage === 'zh' ? ' selected' : ''}>${t('lang.zh')}</option>
414
+ `;
415
+ }
416
+
417
+ // 应用翻译到静态 HTML 元素
418
+ function applyI18nToStaticElements() {
419
+ // 导航按钮
420
+ const navSkills = document.querySelector('.nav-btn[data-view="skills"]');
421
+ const navInstalled = document.querySelector('.nav-btn[data-view="installed"]');
422
+ const navPlatforms = document.querySelector('.nav-btn[data-view="platforms"]');
423
+ const navAdmin = document.querySelector('.nav-btn[data-view="admin"]');
424
+ const navHelp = document.querySelector('.nav-btn[data-view="help"]');
425
+
426
+ if (navSkills) navSkills.innerHTML = `📋 ${t('nav.skills')}`;
427
+ if (navInstalled) navInstalled.innerHTML = `✅ ${t('nav.installed')}`;
428
+ if (navPlatforms) navPlatforms.innerHTML = `💻 ${t('nav.platforms')}`;
429
+ if (navAdmin) navAdmin.innerHTML = `⚙️ ${t('nav.admin')}`;
430
+ if (navHelp) navHelp.innerHTML = `📖 ${t('nav.help')}`;
431
+
432
+ // 视图标题
433
+ const titles = [
434
+ { selector: '#view-skills .view-header h2', key: 'title.availableSkills' },
435
+ { selector: '#view-installed .view-header h2', key: 'title.installedSkills' },
436
+ { selector: '#view-platforms .view-header h2', key: 'title.platforms' },
437
+ { selector: '#view-help .view-header h2', key: 'title.help' },
438
+ { selector: '#view-admin .view-header h2', key: 'title.admin' },
439
+ ];
440
+
441
+ titles.forEach(({ selector, key }) => {
442
+ const el = document.querySelector(selector);
443
+ if (el) el.textContent = t(key);
444
+ });
445
+
446
+ // 搜索框 placeholder
447
+ const searchInput = document.getElementById('search-input');
448
+ if (searchInput) searchInput.placeholder = t('search.placeholder');
449
+
450
+ // page-size select 选项
451
+ const pageSizeSelect = document.getElementById('page-size');
452
+ if (pageSizeSelect) {
453
+ const currentValue = pageSizeSelect.value;
454
+ pageSizeSelect.innerHTML = `
455
+ <option value="10"${currentValue === '10' ? ' selected' : ''}>${t('pageSize.10')}</option>
456
+ <option value="20"${currentValue === '20' ? ' selected' : ''}>${t('pageSize.20')}</option>
457
+ <option value="50"${currentValue === '50' ? ' selected' : ''}>${t('pageSize.50')}</option>
458
+ `;
459
+ }
460
+
461
+ // 按钮文本
462
+ const refreshSkills = document.getElementById('refresh-skills');
463
+ const refreshAdmin = document.getElementById('refresh-admin');
464
+ const updateAll = document.getElementById('update-all');
465
+ const backBtn = document.querySelector('#view-skill-detail .btn-secondary');
466
+
467
+ if (refreshSkills) refreshSkills.innerHTML = `🔄 ${t('btn.refresh')}`;
468
+ if (refreshAdmin) refreshAdmin.innerHTML = `🔄 ${t('btn.refresh')}`;
469
+ if (updateAll) updateAll.innerHTML = `🔄 ${t('btn.updateAll')}`;
470
+ if (backBtn) backBtn.innerHTML = `← ${t('nav.back')}`;
471
+ }
472
+
473
+ // 重新渲染当前视图
474
+ function reRenderCurrentView() {
475
+ // 更新语言选项
476
+ updateLanguageOptions();
477
+
478
+ // 应用翻译到静态元素
479
+ applyI18nToStaticElements();
480
+
481
+ switch(state.currentView) {
482
+ case 'skills':
483
+ loadSkills();
484
+ break;
485
+ case 'installed':
486
+ loadInstalled();
487
+ break;
488
+ case 'platforms':
489
+ loadPlatforms();
490
+ break;
491
+ case 'help':
492
+ loadHelp();
493
+ break;
494
+ case 'admin':
495
+ loadAdminDashboard();
496
+ break;
497
+ case 'skill-detail':
498
+ // 详情视图不需要特殊处理
499
+ break;
500
+ }
501
+ }
502
+
18
503
  // -----------------------------------------------------------------------------
19
504
  // 初始化
20
505
  // -----------------------------------------------------------------------------
@@ -129,14 +614,30 @@ function initializeControls() {
129
614
  const refreshAdmin = document.getElementById('refresh-admin');
130
615
  if (refreshAdmin) refreshAdmin.addEventListener('click', () => loadAdminDashboard());
131
616
 
132
- // Admin 模态框 - 点击外部关闭
133
- const modalEl = document.getElementById('modal');
134
- if (modalEl) {
135
- modalEl.addEventListener('click', (e) => {
136
- if (e.target === modalEl) closeModal();
137
- });
138
- }
139
- }
617
+ // Admin 模态框 - 点击外部关闭
618
+ const modalEl = document.getElementById('modal');
619
+ if (modalEl) {
620
+ modalEl.addEventListener('click', (e) => {
621
+ if (e.target === modalEl) closeModal();
622
+ });
623
+ }
624
+
625
+ // 语言切换
626
+ const langSelect = document.getElementById('lang-select');
627
+ if (langSelect) {
628
+ // 初始化当前语言
629
+ currentLanguage = detectLanguage();
630
+ langSelect.value = currentLanguage;
631
+ updateLanguageOptions();
632
+
633
+ // 应用翻译到静态元素
634
+ applyI18nToStaticElements();
635
+
636
+ langSelect.addEventListener('change', () => {
637
+ setLanguage(langSelect.value);
638
+ });
639
+ }
640
+ }
140
641
 
141
642
  // -----------------------------------------------------------------------------
142
643
  // Skills 列表
@@ -144,79 +645,79 @@ function initializeControls() {
144
645
 
145
646
  async function loadSkills() {
146
647
  const container = document.getElementById('skills-list');
147
- container.innerHTML = '<div class="loading">Loading skills...</div>';
148
-
648
+ container.innerHTML = `<div class="loading">${t('loading.skills')}</div>`;
649
+
149
650
  try {
150
651
  const params = new URLSearchParams({
151
652
  page: state.currentPage.toString(),
152
653
  limit: state.pageSize.toString(),
153
654
  });
154
-
655
+
155
656
  if (state.searchQuery) {
156
657
  params.append('search', state.searchQuery);
157
658
  }
158
-
659
+
159
660
  const response = await fetch(`/api/skills?${params}`);
160
661
  const data = await response.json();
161
-
662
+
162
663
  if (data.error) {
163
- container.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
664
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${data.error}</div>`;
164
665
  return;
165
666
  }
166
-
667
+
167
668
  renderSkills(data.skills || data, container);
168
669
  renderPagination(data.page, data.totalPages || 1);
169
670
  renderFetchWarning(data.fetchErrors);
170
671
  } catch (err) {
171
- container.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
672
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
172
673
  }
173
674
  }
174
675
 
175
676
  async function loadInstalled() {
176
677
  const container = document.getElementById('installed-list');
177
- container.innerHTML = '<div class="loading">Loading...</div>';
178
-
678
+ container.innerHTML = `<div class="loading">${t('loading.generic')}</div>`;
679
+
179
680
  try {
180
681
  const response = await fetch('/api/installed');
181
682
  const skills = await response.json();
182
-
683
+
183
684
  if (skills.error) {
184
- container.innerHTML = `<div class="loading">Error: ${skills.error}</div>`;
685
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${skills.error}</div>`;
185
686
  return;
186
687
  }
187
-
688
+
188
689
  renderSkills(skills, container, true);
189
690
  } catch (err) {
190
- container.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
691
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
191
692
  }
192
693
  }
193
694
 
194
695
  function renderSkills(skills, container, isInstalled = false) {
195
696
  if (!skills || skills.length === 0) {
196
- container.innerHTML = '<div class="loading">No skills found</div>';
697
+ container.innerHTML = `<div class="loading">${t('empty.noSkills')}</div>`;
197
698
  return;
198
699
  }
199
-
700
+
200
701
  container.innerHTML = skills.map(skill => createSkillCard(skill, isInstalled)).join('');
201
702
  }
202
703
 
203
704
  function createSkillCard(skill, isInstalled) {
204
705
  const platforms = skill.platforms || [];
205
706
  const platformTags = platforms.map(p => `<span class="platform-tag">${p}</span>`).join('');
206
-
707
+
207
708
  return `
208
709
  <div class="skill-card" onclick="showSkillDetail('${skill.id}')">
209
710
  <h3>${skill.displayName || skill.id}</h3>
210
711
  <div class="skill-id">${skill.id}@${skill.version || 'latest'}</div>
211
- <p>${skill.description || 'No description'}</p>
712
+ <p>${skill.description || t('detail.noDescription')}</p>
212
713
  <div class="platforms">${platformTags}</div>
213
714
  <div class="actions">
214
715
  ${isInstalled ? `
215
- <button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); uninstallSkill('${skill.id}')">Uninstall</button>
216
- <button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); updateSkill('${skill.id}')">Update</button>
217
- <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); showSkillDetail('${skill.id}')">Info</button>
716
+ <button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); uninstallSkill('${skill.id}')">${t('btn.uninstall')}</button>
717
+ <button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); updateSkill('${skill.id}')">${t('btn.update')}</button>
718
+ <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); showSkillDetail('${skill.id}')">${t('btn.info')}</button>
218
719
  ` : `
219
- <button class="btn btn-success btn-sm" onclick="event.stopPropagation(); installSkill('${skill.id}')">Install</button>
720
+ <button class="btn btn-success btn-sm" onclick="event.stopPropagation(); installSkill('${skill.id}')">${t('btn.install')}</button>
220
721
  `}
221
722
  </div>
222
723
  </div>
@@ -236,7 +737,7 @@ function renderFetchWarning(fetchErrors) {
236
737
  const warning = document.createElement('div');
237
738
  warning.id = 'fetch-warning';
238
739
  warning.style.cssText = 'background: #664400; color: #ffcc00; padding: 8px 16px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem;';
239
- warning.textContent = `⚠ ${fetchErrors} skill(s) failed to load details from npm registry. Refresh to retry.`;
740
+ warning.textContent = t('warning.fetchErrors', { count: fetchErrors });
240
741
  document.querySelector('.view-header').after(warning);
241
742
  }
242
743
 
@@ -247,20 +748,20 @@ function renderFetchWarning(fetchErrors) {
247
748
  function renderPagination(currentPage, totalPages) {
248
749
  state.currentPage = currentPage;
249
750
  state.totalPages = totalPages;
250
-
751
+
251
752
  const container = document.getElementById('pagination');
252
-
753
+
253
754
  if (totalPages <= 1) {
254
755
  container.innerHTML = '';
255
756
  return;
256
757
  }
257
-
758
+
258
759
  let html = `
259
- <button ${currentPage <= 1 ? 'disabled' : ''} onclick="changePage(${currentPage - 1})">← Prev</button>
260
- <span class="page-info">Page ${currentPage} of ${totalPages}</span>
261
- <button ${currentPage >= totalPages ? 'disabled' : ''} onclick="changePage(${currentPage + 1})">Next →</button>
760
+ <button ${currentPage <= 1 ? 'disabled' : ''} onclick="changePage(${currentPage - 1})">${t('pagination.prev')}</button>
761
+ <span class="page-info">${t('pagination.pageInfo', { page: currentPage, totalPages: totalPages })}</span>
762
+ <button ${currentPage >= totalPages ? 'disabled' : ''} onclick="changePage(${currentPage + 1})">${t('pagination.next')}</button>
262
763
  `;
263
-
764
+
264
765
  container.innerHTML = html;
265
766
  }
266
767
 
@@ -276,39 +777,39 @@ function changePage(page) {
276
777
 
277
778
  async function loadPlatforms() {
278
779
  const container = document.getElementById('platforms-list');
279
- container.innerHTML = '<div class="loading">Loading platforms...</div>';
280
-
780
+ container.innerHTML = `<div class="loading">${t('loading.platforms')}</div>`;
781
+
281
782
  try {
282
783
  const response = await fetch('/api/platforms');
283
784
  const platforms = await response.json();
284
-
785
+
285
786
  if (platforms.error) {
286
- container.innerHTML = `<div class="loading">Error: ${platforms.error}</div>`;
787
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${platforms.error}</div>`;
287
788
  return;
288
789
  }
289
-
790
+
290
791
  renderPlatforms(platforms, container);
291
792
  } catch (err) {
292
- container.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
793
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
293
794
  }
294
795
  }
295
796
 
296
797
  function renderPlatforms(platforms, container) {
297
798
  if (!platforms || platforms.length === 0) {
298
- container.innerHTML = '<div class="loading">No platforms found</div>';
799
+ container.innerHTML = `<div class="loading">${t('empty.noPlatforms')}</div>`;
299
800
  return;
300
801
  }
301
-
802
+
302
803
  container.innerHTML = platforms.map(platform => `
303
804
  <div class="platform-card">
304
805
  <div>
305
806
  <h3>${platform.name}</h3>
306
807
  <div class="status ${platform.available ? 'status-available' : 'status-unavailable'}">
307
- ${platform.available ? '✅ Available' : '❌ Not detected'}
808
+ ${platform.available ? t('status.available') : t('status.unavailable')}
308
809
  </div>
309
810
  </div>
310
811
  <div>
311
- ${platform.installedCount ? `<span>${platform.installedCount} skills installed</span>` : ''}
812
+ ${platform.installedCount ? `<span>${t('status.skillsInstalled', { count: platform.installedCount })}</span>` : ''}
312
813
  </div>
313
814
  </div>
314
815
  `).join('');
@@ -333,11 +834,11 @@ async function loadHelp() {
333
834
 
334
835
  function renderHelp(config, container) {
335
836
  const envVars = [
336
- { var: 'SKM_NPM_SCOPE', default: config.npmScope, desc: '主要 npm scope,用于发布/查找 skill' },
337
- { var: 'SKM_NPM_SCOPE_FALLBACK', default: config.npmScopeFallback, desc: '回退 scope(兼容旧安装)' },
338
- { var: 'SKM_NPM_SCOPES', default: config.skillScopes.join(', '), desc: '搜索时尝试的 scope 列表(逗号分隔)' },
339
- { var: 'SKM_NPM_REGISTRY', default: config.npmRegistry, desc: 'npm registry 地址' },
340
- { var: 'SKM_URL', default: config.skmUrl, desc: '个人链接前缀(publish 输出用)' },
837
+ { var: 'SKM_NPM_SCOPE', default: config.npmScope, desc: t('help.envVar.primaryScope') },
838
+ { var: 'SKM_NPM_SCOPE_FALLBACK', default: config.npmScopeFallback, desc: t('help.envVar.fallbackScope') },
839
+ { var: 'SKM_NPM_SCOPES', default: config.skillScopes.join(', '), desc: t('help.envVar.scopeList') },
840
+ { var: 'SKM_NPM_REGISTRY', default: config.npmRegistry, desc: t('help.envVar.registryUrl') },
841
+ { var: 'SKM_URL', default: config.skmUrl, desc: t('help.envVar.personalLink') },
341
842
  ];
342
843
 
343
844
  container.innerHTML = `
@@ -412,77 +913,77 @@ skm publish &lt;skill-name&gt; --version 1.0.1</pre>
412
913
 
413
914
  async function installSkill(skillId) {
414
915
  try {
415
- showToast(`Installing ${skillId}...`, 'info');
916
+ showToast(t('toast.installing', { skillId: skillId }), 'info');
416
917
  const response = await fetch('/api/install', {
417
918
  method: 'POST',
418
919
  headers: { 'Content-Type': 'application/json' },
419
920
  body: JSON.stringify({ skillId })
420
921
  });
421
-
922
+
422
923
  const result = await response.json();
423
-
924
+
424
925
  if (result.error) {
425
- showToast(`Error: ${result.error}`, 'error');
926
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
426
927
  } else {
427
- showToast(`✅ ${skillId} installed successfully!`, 'success');
928
+ showToast(t('toast.installSuccess', { skillId: skillId }), 'success');
428
929
  if (state.currentView === 'installed') loadInstalled();
429
930
  }
430
931
  } catch (err) {
431
- showToast(`Error: ${err.message}`, 'error');
932
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
432
933
  }
433
934
  }
434
935
 
435
936
  async function uninstallSkill(skillId) {
436
- if (!confirm(`Are you sure you want to uninstall ${skillId}?`)) return;
437
-
937
+ if (!confirm(t('confirm.uninstall', { skillId: skillId }))) return;
938
+
438
939
  try {
439
- showToast(`Uninstalling ${skillId}...`, 'info');
940
+ showToast(t('toast.uninstalling', { skillId: skillId }), 'info');
440
941
  const response = await fetch('/api/uninstall', {
441
942
  method: 'POST',
442
943
  headers: { 'Content-Type': 'application/json' },
443
944
  body: JSON.stringify({ skillId })
444
945
  });
445
-
946
+
446
947
  const result = await response.json();
447
-
948
+
448
949
  if (result.error) {
449
- showToast(`Error: ${result.error}`, 'error');
950
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
450
951
  } else {
451
- showToast(`✅ ${skillId} uninstalled!`, 'success');
952
+ showToast(t('toast.uninstallSuccess', { skillId: skillId }), 'success');
452
953
  loadInstalled();
453
954
  }
454
955
  } catch (err) {
455
- showToast(`Error: ${err.message}`, 'error');
956
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
456
957
  }
457
958
  }
458
959
 
459
960
  async function updateSkill(skillId) {
460
961
  try {
461
- showToast(`Updating ${skillId}...`, 'info');
962
+ showToast(t('toast.updating', { skillId: skillId }), 'info');
462
963
  const response = await fetch('/api/update', {
463
964
  method: 'POST',
464
965
  headers: { 'Content-Type': 'application/json' },
465
966
  body: JSON.stringify({ skillId })
466
967
  });
467
-
968
+
468
969
  const result = await response.json();
469
-
970
+
470
971
  if (result.error) {
471
- showToast(`Error: ${result.error}`, 'error');
972
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
472
973
  } else {
473
- showToast(`✅ ${skillId} updated!`, 'success');
974
+ showToast(t('toast.updateSuccess', { skillId: skillId }), 'success');
474
975
  if (state.currentView === 'installed') loadInstalled();
475
976
  }
476
977
  } catch (err) {
477
- showToast(`Error: ${err.message}`, 'error');
978
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
478
979
  }
479
980
  }
480
981
 
481
982
  async function updateAllSkills() {
482
- if (!confirm('Update all installed skills?')) return;
983
+ if (!confirm(t('confirm.updateAll'))) return;
483
984
 
484
985
  try {
485
- showToast('Updating all skills...', 'info');
986
+ showToast(t('toast.updateAll'), 'info');
486
987
  const response = await fetch('/api/update', {
487
988
  method: 'POST',
488
989
  headers: { 'Content-Type': 'application/json' },
@@ -492,13 +993,13 @@ async function updateAllSkills() {
492
993
  const result = await response.json();
493
994
 
494
995
  if (result.error) {
495
- showToast(`Error: ${result.error}`, 'error');
996
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
496
997
  } else {
497
- showToast('✅ All skills updated!', 'success');
998
+ showToast(t('toast.updateAllSuccess'), 'success');
498
999
  loadInstalled();
499
1000
  }
500
1001
  } catch (err) {
501
- showToast(`Error: ${err.message}`, 'error');
1002
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
502
1003
  }
503
1004
  }
504
1005
 
@@ -508,7 +1009,7 @@ async function updateAllSkills() {
508
1009
 
509
1010
  async function showSkillDetail(skillId) {
510
1011
  const content = document.getElementById('skill-detail-content');
511
- content.innerHTML = '<div class="loading">Loading skill details...</div>';
1012
+ content.innerHTML = `<div class="loading">${t('loading.details')}</div>`;
512
1013
 
513
1014
  // 切换到详情视图
514
1015
  const btn = document.querySelector(`.nav-btn[data-view="skill-detail"]`);
@@ -526,7 +1027,7 @@ async function showSkillDetail(skillId) {
526
1027
  const data = await response.json();
527
1028
 
528
1029
  if (data.error) {
529
- content.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
1030
+ content.innerHTML = `<div class="loading">${t('error.generic')}: ${data.error}</div>`;
530
1031
  return;
531
1032
  }
532
1033
 
@@ -540,34 +1041,34 @@ async function showSkillDetail(skillId) {
540
1041
  <div class="detail-name">${data.name}@${data.version}</div>
541
1042
 
542
1043
  <div class="detail-section">
543
- <h3>Description</h3>
544
- <div class="description-text">${data.description || 'No description'}</div>
1044
+ <h3>${t('detail.description')}</h3>
1045
+ <div class="description-text">${data.description || t('detail.noDescription')}</div>
545
1046
  </div>
546
1047
 
547
1048
  <div class="detail-section">
548
- <h3>Details</h3>
549
- <div class="detail-row"><strong>ID:</strong> ${data.id}</div>
550
- <div class="detail-row"><strong>Version:</strong> ${data.version}</div>
551
- ${data.license ? `<div class="detail-row"><strong>License:</strong> ${data.license}</div>` : ''}
552
- ${data.author ? `<div class="detail-row"><strong>Author:</strong> ${data.author}</div>` : ''}
553
- ${data.homepage ? `<div class="detail-row"><strong>Homepage:</strong> <a href="${data.homepage}" target="_blank">${data.homepage}</a></div>` : ''}
554
- ${data.repository ? `<div class="detail-row"><strong>Repository:</strong> <a href="${data.repository}" target="_blank">${data.repository}</a></div>` : ''}
1049
+ <h3>${t('detail.details')}</h3>
1050
+ <div class="detail-row"><strong>${t('detail.id')}:</strong> ${data.id}</div>
1051
+ <div class="detail-row"><strong>${t('detail.version')}:</strong> ${data.version}</div>
1052
+ ${data.license ? `<div class="detail-row"><strong>${t('detail.license')}:</strong> ${data.license}</div>` : ''}
1053
+ ${data.author ? `<div class="detail-row"><strong>${t('detail.author')}:</strong> ${data.author}</div>` : ''}
1054
+ ${data.homepage ? `<div class="detail-row"><strong>${t('detail.homepage')}:</strong> <a href="${data.homepage}" target="_blank">${data.homepage}</a></div>` : ''}
1055
+ ${data.repository ? `<div class="detail-row"><strong>${t('detail.repository')}:</strong> <a href="${data.repository}" target="_blank">${data.repository}</a></div>` : ''}
555
1056
  </div>
556
1057
 
557
1058
  <div class="detail-section">
558
- <h3>Platforms</h3>
1059
+ <h3>${t('detail.platforms')}</h3>
559
1060
  <div class="platform-tags">
560
- ${platforms.length ? platforms.map(p => `<span class="platform-tag">${p}</span>`).join('') : '<span class="platform-tag">N/A</span>'}
1061
+ ${platforms.length ? platforms.map(p => `<span class="platform-tag">${p}</span>`).join('') : `<span class="platform-tag">${t('detail.nA')}</span>`}
561
1062
  </div>
562
1063
  </div>
563
1064
 
564
1065
  ${versions.length ? `
565
1066
  <div class="detail-section">
566
- <h3>Versions (last ${versions.length})</h3>
1067
+ <h3>${t('detail.versions', { count: versions.length })}</h3>
567
1068
  <div class="version-list">
568
1069
  ${versions.slice().reverse().map(v => `
569
1070
  <div class="version-item">
570
- <span>${v} ${v === data.version ? '<span class="version-latest">latest</span>' : ''}</span>
1071
+ <span>${v} ${v === data.version ? `<span class="version-latest">${t('detail.latest')}</span>` : ''}</span>
571
1072
  </div>
572
1073
  `).join('')}
573
1074
  </div>
@@ -575,12 +1076,12 @@ async function showSkillDetail(skillId) {
575
1076
  ` : ''}
576
1077
 
577
1078
  <div class="detail-actions">
578
- <button class="btn btn-success" onclick="installSkill('${escapedId}')">Install</button>
1079
+ <button class="btn btn-success" onclick="installSkill('${escapedId}')">${t('btn.install')}</button>
579
1080
  </div>
580
1081
  </div>
581
1082
  `;
582
1083
  } catch (err) {
583
- content.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
1084
+ content.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
584
1085
  }
585
1086
  }
586
1087
 
@@ -622,24 +1123,24 @@ async function loadAdminDashboard() {
622
1123
 
623
1124
  async function loadAdminStats() {
624
1125
  const container = document.getElementById('admin-stats');
625
- container.innerHTML = '<div class="loading">Loading stats...</div>';
1126
+ container.innerHTML = `<div class="loading">${t('loading.stats')}</div>`;
626
1127
 
627
1128
  try {
628
1129
  const response = await fetch('/api/admin/stats');
629
1130
  const data = await response.json();
630
1131
 
631
1132
  if (data.error) {
632
- container.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
1133
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${data.error}</div>`;
633
1134
  return;
634
1135
  }
635
1136
 
636
1137
  const cards = [
637
- { value: data.totalSkills, label: 'Published Skills' },
638
- { value: data.totalVersions, label: 'Total Versions' },
639
- { value: data.averageVersions, label: 'Avg Versions/Skill' },
640
- { value: data.withMetadata, label: 'With Metadata' },
641
- { value: `${data.totalSizeMB} MB`, label: 'Total Size' },
642
- { value: data.platformCount, label: 'Platforms Covered' },
1138
+ { value: data.totalSkills, label: t('admin.stats.totalSkills') },
1139
+ { value: data.totalVersions, label: t('admin.stats.totalVersions') },
1140
+ { value: data.averageVersions, label: t('admin.stats.avgVersions') },
1141
+ { value: data.withMetadata, label: t('admin.stats.withMetadata') },
1142
+ { value: t('admin.stats.totalSize', { value: data.totalSizeMB }), label: t('admin.stats.totalSize', { value: '' }).replace(': ', '') },
1143
+ { value: data.platformCount, label: t('admin.stats.platformsCovered') },
643
1144
  ];
644
1145
 
645
1146
  container.innerHTML = cards.map(c => `
@@ -649,13 +1150,13 @@ async function loadAdminStats() {
649
1150
  </div>
650
1151
  `).join('');
651
1152
  } catch (err) {
652
- container.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
1153
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
653
1154
  }
654
1155
  }
655
1156
 
656
1157
  async function loadAdminSkills() {
657
1158
  const container = document.getElementById('admin-skills-list');
658
- container.innerHTML = '<div class="loading">Loading published skills...</div>';
1159
+ container.innerHTML = `<div class="loading">${t('loading.published')}</div>`;
659
1160
 
660
1161
  try {
661
1162
  const params = new URLSearchParams({ limit: '100' });
@@ -663,25 +1164,25 @@ async function loadAdminSkills() {
663
1164
  const data = await response.json();
664
1165
 
665
1166
  if (data.error) {
666
- container.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
1167
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${data.error}</div>`;
667
1168
  return;
668
1169
  }
669
1170
 
670
1171
  const skills = data.skills || [];
671
1172
  renderAdminSkills(skills, container);
672
1173
  } catch (err) {
673
- container.innerHTML = `<div class="loading">Error: ${err.message}</div>`;
1174
+ container.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
674
1175
  }
675
1176
  }
676
1177
 
677
1178
  function renderAdminSkills(skills, container) {
678
1179
  if (!skills || skills.length === 0) {
679
- container.innerHTML = '<div class="loading">No published skills found</div>';
1180
+ container.innerHTML = `<div class="loading">${t('empty.noPublishedSkills')}</div>`;
680
1181
  return;
681
1182
  }
682
1183
 
683
1184
  container.innerHTML = `
684
- <h3 style="color: var(--text-secondary); margin-bottom: 12px; font-size: 1rem;">Published Skills (${skills.length})</h3>
1185
+ <h3 style="color: var(--text-secondary); margin-bottom: 12px; font-size: 1rem;">${t('admin.publishedSkillsCount', { count: skills.length })}</h3>
685
1186
  ${skills.map(skill => {
686
1187
  const id = skill.id || skill.name;
687
1188
  const desc = (skill.description || '').slice(0, 80);
@@ -693,11 +1194,11 @@ function renderAdminSkills(skills, container) {
693
1194
  ${desc ? `<div class="admin-skill-desc">${desc}</div>` : ''}
694
1195
  </div>
695
1196
  <div class="admin-actions">
696
- <button class="admin-btn admin-btn-deprecate" onclick="showAdminDeprecateModal('${id}')">Deprecate</button>
697
- <button class="admin-btn admin-btn-unpublish" onclick="showAdminUnpublishModal('${id}')">Unpublish</button>
698
- <button class="admin-btn admin-btn-tag" onclick="showAdminTagModal('${id}')">Tags</button>
699
- <button class="admin-btn admin-btn-owner" onclick="showAdminOwnerModal('${id}')">Owners</button>
700
- <button class="admin-btn admin-btn-access" onclick="showAdminAccessModal('${id}')">Access</button>
1197
+ <button class="admin-btn admin-btn-deprecate" onclick="showAdminDeprecateModal('${id}')">${t('admin.deprecateTitle', { skillId: '' }).replace(': ', '')}</button>
1198
+ <button class="admin-btn admin-btn-unpublish" onclick="showAdminUnpublishModal('${id}')">${t('admin.unpublishTitle', { skillId: '' }).replace(': ', '')}</button>
1199
+ <button class="admin-btn admin-btn-tag" onclick="showAdminTagModal('${id}')">${t('admin.tagsTitle', { skillId: '' }).replace(': ', '')}</button>
1200
+ <button class="admin-btn admin-btn-owner" onclick="showAdminOwnerModal('${id}')">${t('admin.ownersTitle', { skillId: '' }).replace(': ', '')}</button>
1201
+ <button class="admin-btn admin-btn-access" onclick="showAdminAccessModal('${id}')">${t('admin.accessTitle', { skillId: '' }).replace(': ', '')}</button>
701
1202
  </div>
702
1203
  </div>
703
1204
  `;
@@ -714,21 +1215,21 @@ function showAdminDeprecateModal(skillId) {
714
1215
  const modalBody = document.getElementById('modal-body');
715
1216
 
716
1217
  modalBody.innerHTML = `
717
- <h2>Deprecate: ${skillId}</h2>
1218
+ <h2>${t('admin.deprecateTitle', { skillId: skillId })}</h2>
718
1219
  <div class="admin-modal-section">
719
1220
  <div class="admin-input-group">
720
- <label>Version</label>
721
- <input type="text" id="deprecate-version" placeholder="(leave empty for all versions)">
1221
+ <label>${t('admin.deprecateVersion')}</label>
1222
+ <input type="text" id="deprecate-version" placeholder="${t('admin.deprecateVersionPlaceholder')}">
722
1223
  </div>
723
1224
  <div class="admin-input-group">
724
- <label>Message</label>
725
- <input type="text" id="deprecate-msg" value="This skill is deprecated. Please use an alternative.">
1225
+ <label>${t('admin.deprecateMessage')}</label>
1226
+ <input type="text" id="deprecate-msg" value="${t('admin.deprecateDefaultMsg')}">
726
1227
  </div>
727
- <p class="admin-warning-text">⚠ Deprecating will mark this skill as deprecated in the npm registry.</p>
1228
+ <p class="admin-warning-text">${t('admin.deprecateWarning')}</p>
728
1229
  </div>
729
1230
  <div class="actions">
730
- <button class="btn btn-danger" onclick="execAdminDeprecate('${skillId}')">Confirm Deprecate</button>
731
- <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
1231
+ <button class="btn btn-danger" onclick="execAdminDeprecate('${skillId}')">${t('admin.confirmDeprecate')}</button>
1232
+ <button class="btn btn-secondary" onclick="closeModal()">${t('btn.cancel')}</button>
732
1233
  </div>
733
1234
  `;
734
1235
  modal.classList.remove('hidden');
@@ -739,7 +1240,7 @@ async function execAdminDeprecate(skillId) {
739
1240
  const message = document.getElementById('deprecate-msg').value;
740
1241
 
741
1242
  try {
742
- showToast(`Deprecating ${skillId}...`, 'info');
1243
+ showToast(t('admin.deprecating', { skillId: skillId }), 'info');
743
1244
  const response = await fetch('/api/admin/deprecate', {
744
1245
  method: 'POST',
745
1246
  headers: { 'Content-Type': 'application/json' },
@@ -747,14 +1248,14 @@ async function execAdminDeprecate(skillId) {
747
1248
  });
748
1249
  const result = await response.json();
749
1250
  if (result.error) {
750
- showToast(`Error: ${result.error}`, 'error');
1251
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
751
1252
  } else {
752
1253
  showToast(`✅ ${result.message}`, 'success');
753
1254
  closeModal();
754
1255
  loadAdminDashboard();
755
1256
  }
756
1257
  } catch (err) {
757
- showToast(`Error: ${err.message}`, 'error');
1258
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
758
1259
  }
759
1260
  }
760
1261
 
@@ -763,21 +1264,21 @@ function showAdminUnpublishModal(skillId) {
763
1264
  const modalBody = document.getElementById('modal-body');
764
1265
 
765
1266
  modalBody.innerHTML = `
766
- <h2>Unpublish: ${skillId}</h2>
1267
+ <h2>${t('admin.unpublishTitle', { skillId: skillId })}</h2>
767
1268
  <div class="admin-modal-section">
768
1269
  <div class="admin-input-group">
769
- <label>Version</label>
770
- <input type="text" id="unpublish-version" placeholder="(leave empty for entire package)">
1270
+ <label>${t('admin.unpublishVersion')}</label>
1271
+ <input type="text" id="unpublish-version" placeholder="${t('admin.unpublishVersionPlaceholder')}">
771
1272
  </div>
772
1273
  <div class="admin-checkbox-group">
773
1274
  <input type="checkbox" id="unpublish-force">
774
- <label for="unpublish-force">Force unpublish entire package (required if no version specified)</label>
1275
+ <label for="unpublish-force">${t('admin.unpublishForce')}</label>
775
1276
  </div>
776
- <p class="admin-danger-text">⚠ This action cannot be undone! Packages can be restored within 72 hours.</p>
1277
+ <p class="admin-danger-text">${t('admin.unpublishDanger')}</p>
777
1278
  </div>
778
1279
  <div class="actions">
779
- <button class="btn btn-danger" onclick="execAdminUnpublish('${skillId}')">Confirm Unpublish</button>
780
- <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
1280
+ <button class="btn btn-danger" onclick="execAdminUnpublish('${skillId}')">${t('admin.confirmUnpublish')}</button>
1281
+ <button class="btn btn-secondary" onclick="closeModal()">${t('btn.cancel')}</button>
781
1282
  </div>
782
1283
  `;
783
1284
  modal.classList.remove('hidden');
@@ -788,7 +1289,7 @@ async function execAdminUnpublish(skillId) {
788
1289
  const force = document.getElementById('unpublish-force').checked;
789
1290
 
790
1291
  try {
791
- showToast(`Unpublishing ${skillId}...`, 'info');
1292
+ showToast(t('admin.unpublishing', { skillId: skillId }), 'info');
792
1293
  const response = await fetch('/api/admin/unpublish', {
793
1294
  method: 'POST',
794
1295
  headers: { 'Content-Type': 'application/json' },
@@ -796,14 +1297,14 @@ async function execAdminUnpublish(skillId) {
796
1297
  });
797
1298
  const result = await response.json();
798
1299
  if (result.error) {
799
- showToast(`Error: ${result.error}`, 'error');
1300
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
800
1301
  } else {
801
1302
  showToast(`✅ ${result.message}`, 'success');
802
1303
  closeModal();
803
1304
  loadAdminDashboard();
804
1305
  }
805
1306
  } catch (err) {
806
- showToast(`Error: ${err.message}`, 'error');
1307
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
807
1308
  }
808
1309
  }
809
1310
 
@@ -812,32 +1313,32 @@ function showAdminTagModal(skillId) {
812
1313
  const modalBody = document.getElementById('modal-body');
813
1314
 
814
1315
  modalBody.innerHTML = `
815
- <h2>Tags: ${skillId}</h2>
1316
+ <h2>${t('admin.tagsTitle', { skillId: skillId })}</h2>
816
1317
  <div id="admin-tag-current">
817
- <div class="loading">Loading current tags...</div>
1318
+ <div class="loading">${t('loading.generic')}</div>
818
1319
  </div>
819
1320
  <div class="admin-modal-section" style="margin-top: 16px;">
820
- <h3>Set Tag</h3>
1321
+ <h3>${t('admin.setTag')}</h3>
821
1322
  <div class="admin-input-group">
822
- <label>Tag</label>
823
- <input type="text" id="tag-set-name" placeholder="e.g. beta, latest">
1323
+ <label>${t('admin.tagName')}</label>
1324
+ <input type="text" id="tag-set-name" placeholder="${t('admin.tagNamePlaceholder')}">
824
1325
  </div>
825
1326
  <div class="admin-input-group">
826
- <label>Version</label>
827
- <input type="text" id="tag-set-version" placeholder="e.g. 1.0.1">
1327
+ <label>${t('admin.tagVersion')}</label>
1328
+ <input type="text" id="tag-set-version" placeholder="${t('admin.tagVersionPlaceholder')}">
828
1329
  </div>
829
- <button class="btn btn-primary btn-sm" onclick="execAdminTagSet('${skillId}')">Set Tag</button>
1330
+ <button class="btn btn-primary btn-sm" onclick="execAdminTagSet('${skillId}')">${t('admin.setTag')}</button>
830
1331
  </div>
831
1332
  <div class="admin-modal-section">
832
- <h3>Remove Tag</h3>
1333
+ <h3>${t('admin.removeTag')}</h3>
833
1334
  <div class="admin-input-group">
834
- <label>Tag</label>
835
- <input type="text" id="tag-rm-name" placeholder="e.g. beta">
1335
+ <label>${t('admin.tagName')}</label>
1336
+ <input type="text" id="tag-rm-name" placeholder="${t('admin.tagNameRemovePlaceholder')}">
836
1337
  </div>
837
- <button class="btn btn-danger btn-sm" onclick="execAdminTagRemove('${skillId}')">Remove Tag</button>
1338
+ <button class="btn btn-danger btn-sm" onclick="execAdminTagRemove('${skillId}')">${t('admin.removeTag')}</button>
838
1339
  </div>
839
1340
  <div class="actions" style="margin-top: 16px;">
840
- <button class="btn btn-secondary" onclick="closeModal()">Close</button>
1341
+ <button class="btn btn-secondary" onclick="closeModal()">${t('btn.cancel')}</button>
841
1342
  </div>
842
1343
  `;
843
1344
  modal.classList.remove('hidden');
@@ -858,25 +1359,25 @@ async function loadAdminTags(skillId) {
858
1359
  if (result.success && result.tags) {
859
1360
  const entries = Object.entries(result.tags);
860
1361
  if (entries.length === 0) {
861
- container.innerHTML = '<p style="color: var(--text-muted); font-size: 0.85rem;">No dist-tags found.</p>';
1362
+ container.innerHTML = `<p style="color: var(--text-muted); font-size: 0.85rem;">${t('empty.noTags')}</p>`;
862
1363
  } else {
863
1364
  container.innerHTML = `
864
- <h3>Current Tags</h3>
1365
+ <h3>${t('admin.currentTags')}</h3>
865
1366
  <div class="admin-tag-list">
866
1367
  ${entries.map(([tag, ver]) => `
867
1368
  <span class="admin-tag-item">
868
1369
  ${tag} <span class="tag-version">→ ${ver}</span>
869
- ${tag === 'latest' ? '<span style="color: var(--accent); font-size: 0.75rem;"> (default)</span>' : ''}
1370
+ ${tag === 'latest' ? `<span style="color: var(--accent); font-size: 0.75rem;">${t('admin.tag.default')}</span>` : ''}
870
1371
  </span>
871
1372
  `).join('')}
872
1373
  </div>
873
1374
  `;
874
1375
  }
875
1376
  } else {
876
- container.innerHTML = '<p style="color: var(--text-muted); font-size: 0.85rem;">Could not load tags.</p>';
1377
+ container.innerHTML = `<p style="color: var(--text-muted); font-size: 0.85rem;">${t('empty.couldNotLoadTags')}</p>`;
877
1378
  }
878
1379
  } catch (err) {
879
- container.innerHTML = `<p style="color: #ff6666; font-size: 0.85rem;">Error: ${err.message}</p>`;
1380
+ container.innerHTML = `<p style="color: #ff6666; font-size: 0.85rem;">${t('error.generic')}: ${err.message}</p>`;
880
1381
  }
881
1382
  }
882
1383
 
@@ -885,12 +1386,12 @@ async function execAdminTagSet(skillId) {
885
1386
  const version = document.getElementById('tag-set-version').value;
886
1387
 
887
1388
  if (!tag || !version) {
888
- showToast('Tag and version are required', 'error');
1389
+ showToast(t('admin.tagRequired'), 'error');
889
1390
  return;
890
1391
  }
891
1392
 
892
1393
  try {
893
- showToast(`Setting tag ${tag}...`, 'info');
1394
+ showToast(t('admin.settingTag', { tag: tag }), 'info');
894
1395
  const response = await fetch('/api/admin/tag', {
895
1396
  method: 'POST',
896
1397
  headers: { 'Content-Type': 'application/json' },
@@ -898,7 +1399,7 @@ async function execAdminTagSet(skillId) {
898
1399
  });
899
1400
  const result = await response.json();
900
1401
  if (result.error) {
901
- showToast(`Error: ${result.error}`, 'error');
1402
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
902
1403
  } else {
903
1404
  showToast(`✅ ${result.message}`, 'success');
904
1405
  loadAdminTags(skillId);
@@ -906,7 +1407,7 @@ async function execAdminTagSet(skillId) {
906
1407
  document.getElementById('tag-set-version').value = '';
907
1408
  }
908
1409
  } catch (err) {
909
- showToast(`Error: ${err.message}`, 'error');
1410
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
910
1411
  }
911
1412
  }
912
1413
 
@@ -914,12 +1415,12 @@ async function execAdminTagRemove(skillId) {
914
1415
  const tag = document.getElementById('tag-rm-name').value;
915
1416
 
916
1417
  if (!tag) {
917
- showToast('Tag name is required', 'error');
1418
+ showToast(t('admin.tagNameRequired'), 'error');
918
1419
  return;
919
1420
  }
920
1421
 
921
1422
  try {
922
- showToast(`Removing tag ${tag}...`, 'info');
1423
+ showToast(t('admin.removingTag', { tag: tag }), 'info');
923
1424
  const response = await fetch('/api/admin/tag', {
924
1425
  method: 'POST',
925
1426
  headers: { 'Content-Type': 'application/json' },
@@ -927,14 +1428,14 @@ async function execAdminTagRemove(skillId) {
927
1428
  });
928
1429
  const result = await response.json();
929
1430
  if (result.error) {
930
- showToast(`Error: ${result.error}`, 'error');
1431
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
931
1432
  } else {
932
1433
  showToast(`✅ ${result.message}`, 'success');
933
1434
  loadAdminTags(skillId);
934
1435
  document.getElementById('tag-rm-name').value = '';
935
1436
  }
936
1437
  } catch (err) {
937
- showToast(`Error: ${err.message}`, 'error');
1438
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
938
1439
  }
939
1440
  }
940
1441
 
@@ -943,25 +1444,25 @@ function showAdminOwnerModal(skillId) {
943
1444
  const modalBody = document.getElementById('modal-body');
944
1445
 
945
1446
  modalBody.innerHTML = `
946
- <h2>Owners: ${skillId}</h2>
1447
+ <h2>${t('admin.ownersTitle', { skillId: skillId })}</h2>
947
1448
  <div class="admin-modal-section">
948
- <h3>Add Owner</h3>
1449
+ <h3>${t('admin.addOwner')}</h3>
949
1450
  <div class="admin-input-group">
950
- <label>npm User</label>
951
- <input type="text" id="owner-add-name" placeholder="npm username">
1451
+ <label>${t('admin.npmUser')}</label>
1452
+ <input type="text" id="owner-add-name" placeholder="${t('admin.npmUserPlaceholder')}">
952
1453
  </div>
953
- <button class="btn btn-success btn-sm" onclick="execAdminOwnerAdd('${skillId}')">Add Owner</button>
1454
+ <button class="btn btn-success btn-sm" onclick="execAdminOwnerAdd('${skillId}')">${t('admin.addOwner')}</button>
954
1455
  </div>
955
1456
  <div class="admin-modal-section">
956
- <h3>Remove Owner</h3>
1457
+ <h3>${t('admin.removeOwner')}</h3>
957
1458
  <div class="admin-input-group">
958
- <label>npm User</label>
959
- <input type="text" id="owner-rm-name" placeholder="npm username">
1459
+ <label>${t('admin.npmUser')}</label>
1460
+ <input type="text" id="owner-rm-name" placeholder="${t('admin.npmUserPlaceholder')}">
960
1461
  </div>
961
- <button class="btn btn-danger btn-sm" onclick="execAdminOwnerRemove('${skillId}')">Remove Owner</button>
1462
+ <button class="btn btn-danger btn-sm" onclick="execAdminOwnerRemove('${skillId}')">${t('admin.removeOwner')}</button>
962
1463
  </div>
963
1464
  <div class="actions">
964
- <button class="btn btn-secondary" onclick="closeModal()">Close</button>
1465
+ <button class="btn btn-secondary" onclick="closeModal()">${t('btn.cancel')}</button>
965
1466
  </div>
966
1467
  `;
967
1468
  modal.classList.remove('hidden');
@@ -969,10 +1470,10 @@ function showAdminOwnerModal(skillId) {
969
1470
 
970
1471
  async function execAdminOwnerAdd(skillId) {
971
1472
  const user = document.getElementById('owner-add-name').value;
972
- if (!user) { showToast('Username is required', 'error'); return; }
1473
+ if (!user) { showToast(t('admin.usernameRequired'), 'error'); return; }
973
1474
 
974
1475
  try {
975
- showToast(`Adding owner ${user}...`, 'info');
1476
+ showToast(t('admin.addingOwner', { user: user }), 'info');
976
1477
  const response = await fetch('/api/admin/owner', {
977
1478
  method: 'POST',
978
1479
  headers: { 'Content-Type': 'application/json' },
@@ -980,22 +1481,22 @@ async function execAdminOwnerAdd(skillId) {
980
1481
  });
981
1482
  const result = await response.json();
982
1483
  if (result.error) {
983
- showToast(`Error: ${result.error}`, 'error');
1484
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
984
1485
  } else {
985
1486
  showToast(`✅ ${result.message}`, 'success');
986
1487
  document.getElementById('owner-add-name').value = '';
987
1488
  }
988
1489
  } catch (err) {
989
- showToast(`Error: ${err.message}`, 'error');
1490
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
990
1491
  }
991
1492
  }
992
1493
 
993
1494
  async function execAdminOwnerRemove(skillId) {
994
1495
  const user = document.getElementById('owner-rm-name').value;
995
- if (!user) { showToast('Username is required', 'error'); return; }
1496
+ if (!user) { showToast(t('admin.usernameRequired'), 'error'); return; }
996
1497
 
997
1498
  try {
998
- showToast(`Removing owner ${user}...`, 'info');
1499
+ showToast(t('admin.removingOwner', { user: user }), 'info');
999
1500
  const response = await fetch('/api/admin/owner', {
1000
1501
  method: 'POST',
1001
1502
  headers: { 'Content-Type': 'application/json' },
@@ -1003,13 +1504,13 @@ async function execAdminOwnerRemove(skillId) {
1003
1504
  });
1004
1505
  const result = await response.json();
1005
1506
  if (result.error) {
1006
- showToast(`Error: ${result.error}`, 'error');
1507
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
1007
1508
  } else {
1008
1509
  showToast(`✅ ${result.message}`, 'success');
1009
1510
  document.getElementById('owner-rm-name').value = '';
1010
1511
  }
1011
1512
  } catch (err) {
1012
- showToast(`Error: ${err.message}`, 'error');
1513
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
1013
1514
  }
1014
1515
  }
1015
1516
 
@@ -1018,20 +1519,19 @@ function showAdminAccessModal(skillId) {
1018
1519
  const modalBody = document.getElementById('modal-body');
1019
1520
 
1020
1521
  modalBody.innerHTML = `
1021
- <h2>Access: ${skillId}</h2>
1522
+ <h2>${t('admin.accessTitle', { skillId: skillId })}</h2>
1022
1523
  <div class="admin-modal-section">
1023
1524
  <p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 12px;">
1024
- Set the package access level. Public packages are visible to everyone.
1025
- Restricted packages require authentication to install.
1525
+ ${t('admin.accessDesc')}
1026
1526
  </p>
1027
1527
  <div class="admin-radio-group">
1028
- <label><input type="radio" name="access-level" value="public" checked> Public</label>
1029
- <label><input type="radio" name="access-level" value="restricted"> Restricted</label>
1528
+ <label><input type="radio" name="access-level" value="public" checked> ${t('admin.accessPublic')}</label>
1529
+ <label><input type="radio" name="access-level" value="restricted"> ${t('admin.accessRestricted')}</label>
1030
1530
  </div>
1031
1531
  </div>
1032
1532
  <div class="actions">
1033
- <button class="btn btn-primary" onclick="execAdminAccess('${skillId}')">Set Access</button>
1034
- <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
1533
+ <button class="btn btn-primary" onclick="execAdminAccess('${skillId}')">${t('admin.setAccess')}</button>
1534
+ <button class="btn btn-secondary" onclick="closeModal()">${t('btn.cancel')}</button>
1035
1535
  </div>
1036
1536
  `;
1037
1537
  modal.classList.remove('hidden');
@@ -1041,7 +1541,7 @@ async function execAdminAccess(skillId) {
1041
1541
  const level = document.querySelector('input[name="access-level"]:checked').value;
1042
1542
 
1043
1543
  try {
1044
- showToast(`Setting access to ${level}...`, 'info');
1544
+ showToast(t('admin.settingAccess', { level: level }), 'info');
1045
1545
  const response = await fetch('/api/admin/access', {
1046
1546
  method: 'POST',
1047
1547
  headers: { 'Content-Type': 'application/json' },
@@ -1049,12 +1549,12 @@ async function execAdminAccess(skillId) {
1049
1549
  });
1050
1550
  const result = await response.json();
1051
1551
  if (result.error) {
1052
- showToast(`Error: ${result.error}`, 'error');
1552
+ showToast(`${t('error.generic')}: ${result.error}`, 'error');
1053
1553
  } else {
1054
1554
  showToast(`✅ ${result.message}`, 'success');
1055
1555
  closeModal();
1056
1556
  }
1057
1557
  } catch (err) {
1058
- showToast(`Error: ${err.message}`, 'error');
1558
+ showToast(`${t('error.generic')}: ${err.message}`, 'error');
1059
1559
  }
1060
1560
  }