@goodandready/dsh-russian-lang 0.2.16 → 0.2.19

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/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { registerPluginUpdater } from './plugin-updater.js'
1
2
  // dsh-russian-lang — серверная половина.
2
3
  //
3
4
  // Словари и переключатель живут в браузере (lib/client.js). Хосту переводить
@@ -49,4 +50,23 @@ export function apply(ctx) {
49
50
  ctx.on('settings/updated', (ns) => { if (ns === NS) sync() })
50
51
  sync()
51
52
  })
53
+
54
+ // One-click plugin updater per DSH standard
55
+ if (typeof ctx.inject === 'function') {
56
+ ctx.inject(['webServer'], (wctx) => {
57
+ try {
58
+ const mount = () => registerPluginUpdater(wctx, {
59
+ endpoint: '/api/dsh-russian-lang/update',
60
+ packageName: '@goodandready/dsh-russian-lang',
61
+ manifestUrl: new URL('../package.json', import.meta.url),
62
+ })
63
+ if (typeof wctx.effect === 'function') wctx.effect(mount, 'russian-lang: plugin updater')
64
+ else mount()
65
+ } catch (err) {
66
+ if (typeof wctx.logger?.warn === 'function') {
67
+ wctx.logger.warn('Failed to mount plugin updater route:', err)
68
+ }
69
+ }
70
+ })
71
+ }
52
72
  }
@@ -0,0 +1,333 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { homedir } from 'node:os'
5
+ import { basename, dirname, isAbsolute, resolve } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ /**
9
+ * Host-side one-click updater for @goodandready/dsh-russian-lang.
10
+ * Implements the unified DSH plugin updater standard.
11
+ */
12
+
13
+ const UPDATE_HEADER = 'x-dsh-plugin-update'
14
+ const UPDATE_TIMEOUT_MS = 10 * 60_000
15
+ const VERSION_CACHE_MS = 5 * 60_000
16
+ let latestCache = undefined
17
+
18
+ function header(request, name) {
19
+ const value = request.headers?.[name]
20
+ return Array.isArray(value) ? value[0] : value
21
+ }
22
+
23
+ function isLoopback(value) {
24
+ const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
25
+ return (
26
+ address === 'localhost' ||
27
+ address === 'localhost.' ||
28
+ address === '::1' ||
29
+ address?.startsWith('127.') === true ||
30
+ address?.startsWith('::ffff:127.') === true
31
+ )
32
+ }
33
+
34
+ export function isTrustedUpdateRequest(request) {
35
+ if (header(request, UPDATE_HEADER) !== '1') return false
36
+ if (!isLoopback(request.socket?.remoteAddress)) return false
37
+ const site = header(request, 'sec-fetch-site')
38
+ if (site !== undefined && site !== 'same-origin') return false
39
+ const origin = header(request, 'origin')
40
+ const host = header(request, 'host')
41
+ if (origin === undefined || host === undefined) return false
42
+ try {
43
+ const url = new URL(origin)
44
+ return (
45
+ (url.protocol === 'http:' || url.protocol === 'https:') &&
46
+ isLoopback(url.hostname) &&
47
+ url.host === host
48
+ )
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ function validProfileName(value) {
55
+ return (
56
+ typeof value === 'string' &&
57
+ value !== '' &&
58
+ value !== '.' &&
59
+ value !== '..' &&
60
+ !value.includes('/') &&
61
+ !value.includes('\\') &&
62
+ !/[\0-\x1f\x7f]/.test(value)
63
+ )
64
+ }
65
+
66
+ function profileNameFromArgv(argv) {
67
+ for (let index = 2; index < argv.length; index += 1) {
68
+ if (argv[index] === '--profile') return argv[index + 1]
69
+ if (argv[index]?.startsWith('--profile=')) return argv[index].slice('--profile='.length)
70
+ }
71
+ return argv[2] === 'web' ? 'web' : undefined
72
+ }
73
+
74
+ function findDshCliEntry() {
75
+ const value = process.argv[1]
76
+ if (value === undefined || value === '') return undefined
77
+ const entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value)
78
+ if (!existsSync(entry)) return undefined
79
+ for (let directory = dirname(entry); ; directory = dirname(directory)) {
80
+ const manifestPath = resolve(directory, 'package.json')
81
+ if (existsSync(manifestPath)) {
82
+ try {
83
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
84
+ const bin =
85
+ typeof manifest.bin === 'string'
86
+ ? manifest.bin
87
+ : typeof manifest.bin === 'object' && manifest.bin !== null
88
+ ? manifest.bin.dsh
89
+ : undefined
90
+ if (
91
+ manifest.name === '@deepseek-ai/dsh' &&
92
+ typeof bin === 'string' &&
93
+ !isAbsolute(bin) &&
94
+ resolve(directory, bin) === resolve(entry)
95
+ ) {
96
+ return entry
97
+ }
98
+ } catch {
99
+ // Continue searching parent directories
100
+ }
101
+ }
102
+ const parent = dirname(directory)
103
+ if (parent === directory) return undefined
104
+ }
105
+ }
106
+
107
+ function runtime() {
108
+ const profileDir = resolve(
109
+ process.env.DSH_PROFILE_DIR ?? resolve(homedir(), '.dsh', 'profiles', 'web')
110
+ )
111
+ const selected = profileNameFromArgv(process.argv)
112
+ const profileName = validProfileName(selected)
113
+ ? selected
114
+ : validProfileName(basename(profileDir))
115
+ ? basename(profileDir)
116
+ : 'web'
117
+ const cliEntry = findDshCliEntry()
118
+ return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry }
119
+ }
120
+
121
+ function parseSemver(value) {
122
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value)
123
+ if (match === null) return undefined
124
+ return {
125
+ core: [Number(match[1]), Number(match[2]), Number(match[3])],
126
+ prerelease: match[4]?.split('.') ?? [],
127
+ }
128
+ }
129
+
130
+ function comparePrerelease(left, right) {
131
+ if (left.length === 0 || right.length === 0) {
132
+ return left.length === right.length ? 0 : left.length === 0 ? 1 : -1
133
+ }
134
+ const length = Math.max(left.length, right.length)
135
+ for (let index = 0; index < length; index += 1) {
136
+ const a = left[index]
137
+ const b = right[index]
138
+ if (a === undefined || b === undefined) return a === b ? 0 : a === undefined ? -1 : 1
139
+ if (a === b) continue
140
+ const aNumeric = /^\d+$/.test(a)
141
+ const bNumeric = /^\d+$/.test(b)
142
+ if (aNumeric && bNumeric) {
143
+ const aNumber = BigInt(a)
144
+ const bNumber = BigInt(b)
145
+ if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1
146
+ continue
147
+ }
148
+ if (aNumeric !== bNumeric) return aNumeric ? -1 : 1
149
+ return a > b ? 1 : -1
150
+ }
151
+ return 0
152
+ }
153
+
154
+ export function isNewerVersion(currentValue, candidateValue) {
155
+ const current = parseSemver(currentValue)
156
+ const candidate = parseSemver(candidateValue)
157
+ if (current === undefined || candidate === undefined) return false
158
+ for (let index = 0; index < 3; index += 1) {
159
+ if (candidate.core[index] !== current.core[index]) {
160
+ return candidate.core[index] > current.core[index]
161
+ }
162
+ }
163
+ return comparePrerelease(candidate.prerelease, current.prerelease) > 0
164
+ }
165
+
166
+ async function latestVersion(packageName, registry) {
167
+ if (
168
+ latestCache?.packageName === packageName &&
169
+ latestCache.registry === registry &&
170
+ Date.now() < latestCache.expiresAt
171
+ ) {
172
+ return latestCache.version
173
+ }
174
+ try {
175
+ const response = await fetch(
176
+ `${registry.replace(/\/$/, '')}/${encodeURIComponent(packageName)}/latest`,
177
+ { signal: AbortSignal.timeout(8_000) }
178
+ )
179
+ if (!response.ok) return undefined
180
+ const value = await response.json()
181
+ if (typeof value.version !== 'string' || value.version === '') return undefined
182
+ latestCache = {
183
+ packageName,
184
+ registry,
185
+ version: value.version,
186
+ expiresAt: Date.now() + VERSION_CACHE_MS,
187
+ }
188
+ return value.version
189
+ } catch {
190
+ return undefined
191
+ }
192
+ }
193
+
194
+ async function currentVersion(manifestUrl) {
195
+ const value = JSON.parse(await readFile(manifestUrl, 'utf8'))
196
+ if (typeof value.version !== 'string' || value.version === '') {
197
+ throw new Error('Cannot read current plugin version.')
198
+ }
199
+ return value.version
200
+ }
201
+
202
+ async function status(options, target) {
203
+ const current = await currentVersion(options.manifestUrl)
204
+ const latest = await latestVersion(options.packageName, options.registry ?? 'https://registry.npmjs.org')
205
+ return {
206
+ packageName: options.packageName,
207
+ currentVersion: current,
208
+ ...(latest === undefined ? {} : { latestVersion: latest }),
209
+ latestCheckFailed: latest === undefined,
210
+ updateAvailable: latest !== undefined && isNewerVersion(current, latest),
211
+ profileName: target.profileName,
212
+ canAutoUpdate: target.cliEntry !== undefined,
213
+ }
214
+ }
215
+
216
+ async function installExact(target, packageSpec, options) {
217
+ if (target.cliEntry === undefined) {
218
+ throw new Error('Automatic update is unavailable in this runtime.')
219
+ }
220
+ await new Promise((resolvePromise, reject) => {
221
+ const child = spawn(
222
+ process.execPath,
223
+ [
224
+ target.cliEntry,
225
+ 'plugin',
226
+ '--profile',
227
+ target.profileName,
228
+ 'add',
229
+ '--config.minimumReleaseAge=0',
230
+ packageSpec,
231
+ `--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
232
+ ],
233
+ {
234
+ cwd: target.profileDir,
235
+ windowsHide: true,
236
+ stdio: ['ignore', 'pipe', 'pipe'],
237
+ env: { ...process.env, NO_COLOR: '1' },
238
+ }
239
+ )
240
+ let detail = ''
241
+ child.stdout?.on('data', (chunk) => {
242
+ detail = (detail + String(chunk)).slice(-4_000)
243
+ })
244
+ child.stderr?.on('data', (chunk) => {
245
+ detail = (detail + String(chunk)).slice(-4_000)
246
+ })
247
+ const timer = setTimeout(() => {
248
+ child.kill()
249
+ reject(new Error('Update timed out; use the normal DSH update flow.'))
250
+ }, UPDATE_TIMEOUT_MS)
251
+ child.once('error', (error) => {
252
+ clearTimeout(timer)
253
+ reject(error)
254
+ })
255
+ child.once('exit', (code) => {
256
+ clearTimeout(timer)
257
+ if (code === 0) resolvePromise()
258
+ else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
259
+ })
260
+ })
261
+ }
262
+
263
+ function json(response, statusCode, value) {
264
+ response.writeHead(statusCode, {
265
+ 'content-type': 'application/json; charset=utf-8',
266
+ 'cache-control': 'no-store',
267
+ })
268
+ response.end(JSON.stringify(value))
269
+ }
270
+
271
+ export function registerPluginUpdater(ctx, options) {
272
+ const host = ctx
273
+ if (!host.webServer || typeof host.webServer.register !== 'function') {
274
+ return () => {}
275
+ }
276
+ let installing = false
277
+ return host.webServer.register({
278
+ kind: 'exact',
279
+ path: options.endpoint,
280
+ handler: async (request, response) => {
281
+ try {
282
+ const target = runtime()
283
+ if (request.method === 'GET' || request.method === 'HEAD') {
284
+ const payload = await status(options, target)
285
+ response.writeHead(200, {
286
+ 'content-type': 'application/json; charset=utf-8',
287
+ 'cache-control': 'no-store',
288
+ })
289
+ response.end(request.method === 'HEAD' ? undefined : JSON.stringify(payload))
290
+ return
291
+ }
292
+ if (request.method !== 'POST') {
293
+ response.writeHead(405, { allow: 'GET, HEAD, POST' })
294
+ response.end()
295
+ return
296
+ }
297
+ if (!isTrustedUpdateRequest(request)) {
298
+ json(response, 403, { error: 'Rejected non-local or cross-origin update request.' })
299
+ return
300
+ }
301
+ if (installing) {
302
+ json(response, 409, { error: 'This plugin is already updating.' })
303
+ return
304
+ }
305
+ installing = true
306
+ try {
307
+ const before = await status(options, target)
308
+ if (before.latestVersion === undefined) {
309
+ json(response, 503, { error: 'The latest version is temporarily unavailable.' })
310
+ return
311
+ }
312
+ if (!before.updateAvailable) {
313
+ json(response, 200, before)
314
+ return
315
+ }
316
+ await installExact(target, `${options.packageName}@${before.latestVersion}`, options)
317
+ json(response, 200, {
318
+ ...before,
319
+ updatedVersion: before.latestVersion,
320
+ restartRequired: true,
321
+ })
322
+ } finally {
323
+ installing = false
324
+ }
325
+ } catch (error) {
326
+ if (ctx.logger && typeof ctx.logger.warn === 'function') {
327
+ ctx.logger.warn(`plugin updater failed: ${String(error)}`)
328
+ }
329
+ json(response, 503, { error: 'Plugin update failed; see server logs.' })
330
+ }
331
+ },
332
+ })
333
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-russian-lang",
3
- "version": "0.2.16",
4
- "description": "Russian localization for DeepSeek Harness (DSH v0.1.5-rc.2+) web UI: adds Русский as the native language option in Settings - General - Language. 100.0% coverage across 6,808 keys (1,225 core UI keys + 5,583 keys across 40+ ecosystem plugins) with zero machine drafts. Features unified dsh-clinebot visual design standard for Settings Card, full translations for updated ecosystem plugins (dsh-model-sync, dsh-issue-reporter, dsh-model-search, dsh-session-control, dsh-grok-xsearch, dsh-cron, Live Canvas, dsh-goal, Skills Manager, Shadow Auditor, Kanban Epics, Key Rotation, Loop Guard, Remote Workspace, Cost Meter, Vision Bridge, Context, Time Machine), Smart Russian UX (localized Markdown export, turn translation, live typography, slash command aliases, Alt+L layout fix), expanded error humanizer (POSIX, HTTP 4xx/5xx), Skin Center & Wallpaper Engine support, Workshop market integration, sidebar previewers, and runtime snapshot extension without modifying core files.",
3
+ "version": "0.2.19",
4
+ "description": "Russian localization for DeepSeek Harness (DSH v0.1.6-alpha.1+) web UI: adds Русский as native language option. 100.0% coverage across 11,295 keys (127 namespaces). Features full DSH v0.1.6-alpha.1 core support (terminal, archive, command palette, Auto review, trajectory, PTC/Creator presets) and complete GoodAndReady plugin catalog (subscriptions, tts, image-gen, voice, key-limits, notify, commandcode, plugin-console).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -68,4 +68,4 @@
68
68
  "bugs": {
69
69
  "url": "https://github.com/GooDAnDReaDY/dsh-russian-lang/issues"
70
70
  }
71
- }
71
+ }
@@ -150,7 +150,21 @@
150
150
  "description.goal": "Установить или просмотреть цель для длительной задачи",
151
151
  "description.permission": "Переключить пресет разрешений (режим песочницы и политики подтверждений)",
152
152
  "description.plan": "Войти или выйти из режима планирования",
153
- "notice.attachmentsUnsupported": "/{command} не принимает вложения; сначала удалите их"
153
+ "notice.attachmentsUnsupported": "/{command} не принимает вложения; сначала удалите их",
154
+ "section.add": "Добавить",
155
+ "section.commands": "Команды",
156
+ "label.goal": "Цель",
157
+ "label.plan": "План",
158
+ "label.feedback": "Обратная связь",
159
+ "label.compact": "Сжатие",
160
+ "label.permission": "Режим доступа",
161
+ "label.export": "Экспорт",
162
+ "token.goal": "цель",
163
+ "token.plan": "план",
164
+ "token.feedback": "отзыв",
165
+ "token.compact": "сжатие",
166
+ "token.permission": "доступ",
167
+ "token.export": "экспорт"
154
168
  },
155
169
  "session-log-download": {
156
170
  "dialog.close": "Закрыть",
@@ -177,12 +191,22 @@
177
191
  "model": {
178
192
  "trigger.loading": "Загрузка моделей…",
179
193
  "option.deepseekV4Flash.description": "Быстрая, эффективная и экономичная; подходит для типовых, повседневных или параллельных задач.",
180
- "option.deepseekV4Pro.description": "Улучшенное агентное написание кода, глубокие знания и сложное мышление; подходит для комплексных задач с высокими требованиями к качеству при более высокой стоимости."
194
+ "option.deepseekV4Pro.description": "Улучшенное агентное написание кода, глубокие знания и сложное мышление; подходит для комплексных задач с высокими требованиями к качеству при более высокой стоимости.",
195
+ "command.label": "Модель"
181
196
  },
182
197
  "permission.access": {
183
198
  "preset.fullAccess": "Полный доступ",
184
199
  "preset.readOnly": "Только чтение",
185
- "preset.workspaceWrite": "Запись в рабочую область"
200
+ "preset.workspaceWrite": "Запись в рабочую область",
201
+ "mode": "Режим доступа, текущий: {name}",
202
+ "close": "Закрыть",
203
+ "auto.label": "Автопроверка",
204
+ "auto.badge": "ЭКСП",
205
+ "auto.description": "Запуск без песочницы с экспериментальной проверкой каждого вызова инструментов и внутренних вызовов PTC той же моделью.",
206
+ "auto.confirm.title": "Включить автопроверку (экспериментально)?",
207
+ "auto.confirm.description": "Автопроверка работает без песочницы. Перед каждым вызовом нативного инструмента и внутренним вызовом PTC та же модель, что и текущий агент, проверяет, разрешить ли его. Эта функция экспериментальная, может ошибочно разрешать или блокировать действия и расходует дополнительные токены.",
208
+ "auto.confirm.acknowledge": "Я понимаю эти риски и хочу продолжить",
209
+ "auto.confirm.enable": "Включить автопроверку"
186
210
  },
187
211
  "reference": {
188
212
  "crumb.root": "Рабочая область",
@@ -260,7 +260,11 @@
260
260
  "input.send.steer": "Направить сообщение",
261
261
  "queue.file": "Файл в очереди: {name}",
262
262
  "queue.sending": "Отправка…",
263
- "tool.title.readImage": "Просмотр изображения"
263
+ "tool.title.readImage": "Просмотр изображения",
264
+ "input.file": "Файл",
265
+ "tool.autoReviewRejected": "Отклонено автопроверкой",
266
+ "tool.autoReviewNotExecuted": "Инструмент не был выполнен. Причина: {reason}",
267
+ "tool.autoReviewReasonFallback": "Автопроверка не разрешила это действие"
264
268
  },
265
269
  "chat": {
266
270
  "chat.deepDiving": "Глубокий анализ…",
@@ -379,7 +383,8 @@
379
383
  "stats.dialog.title": "Статистика сессии",
380
384
  "stats.dialog.toolTime": "Время инструментов",
381
385
  "stats.dialog.ttft": "Среднее время до первого токена (TTFT)",
382
- "stats.dialog.usageTitle": "Использование токенов"
386
+ "stats.dialog.usageTitle": "Использование токенов",
387
+ "duration.hours": "{hours} ч {minutes} мин {seconds} с"
383
388
  },
384
389
  "trajectory": {
385
390
  "block.label": "Блок #{index} {type}",
@@ -823,7 +828,14 @@
823
828
  "usage.thisRequest": "This request",
824
829
  "usage.tokens": "Токены",
825
830
  "view.trajectory": "Trajectory",
826
- "layout.fileAttachments": "Файлы ×{count}"
831
+ "layout.fileAttachments": "Файлы ×{count}",
832
+ "code.copyOutput": "Скопировать вывод",
833
+ "code.copySource": "Скопировать код",
834
+ "code.originalJson": "Оригинальный JSON",
835
+ "code.output": "Вывод",
836
+ "code.running": "Выполняется…",
837
+ "code.source": "Код",
838
+ "record.wrapLines": "Переносить строки"
827
839
  },
828
840
  "slash.menu": {
829
841
  "crumbs.aria": "Навигация по папкам",
@@ -831,4 +843,4 @@
831
843
  "drill.hint": "Открыть папку",
832
844
  "drill.key": "Tab"
833
845
  }
834
- }
846
+ }
@@ -99,7 +99,10 @@
99
99
  "welcomeBody": "DeepSeek Harness 0.1 находится на стадии тестирования для разработчиков Harness. Многие компоненты требуют дальнейшего улучшения, и мы приветствуем обратную связь от сообщества. Базовые плагины и системные API DeepSeek Harness продолжат активно развиваться в ближайшие месяцы.\n\nМы с нетерпением ждём возможности исследовать границы искусственного интеллекта вместе с разработчиками по всему миру на базе открытой, модульной и повторно используемой инфраструктуры. Приглашаем разработчиков присоединиться к экосистеме плагинов DSH.",
100
100
  "welcomeContinue": "Продолжить",
101
101
  "welcomeTitle": "Уведомление о внутреннем тестировании",
102
- "customBaseUrlInvalid": "Введите корректный URL (HTTP или HTTPS)."
102
+ "customBaseUrlInvalid": "Введите корректный URL (HTTP или HTTPS).",
103
+ "deepSeekChatBaseUrl": "https://api.deepseek.com",
104
+ "deepSeekMessagesBaseUrl": "https://api.deepseek.com/anthropic",
105
+ "deepSeekEndpointHint": "Используйте эндпоинт, совместимый с настроенным подключением."
103
106
  },
104
107
  "workspace": {
105
108
  "actions.newSession.aria": "Новая сессия в {name}",
@@ -55,7 +55,13 @@
55
55
  "view": "Посмотреть",
56
56
  "presetPtcDescription": "Полноценный агент разработки без инструмента рабочих процессов; остальные инструменты доступны через SDK режима PTC, что позволяет модели объединять многоэтапные операции в одну программу TypeScript.",
57
57
  "presetPtcName": "Режим PTC",
58
- "switchRefused": "Не удалось переключиться на {name}: {reason}"
58
+ "switchRefused": "Не удалось переключиться на {name}: {reason}",
59
+ "selectionOffDefault": "По умолчанию",
60
+ "showPicker": "Разрешить выбор режимов агента",
61
+ "showPickerBeta": "Бета",
62
+ "showPickerDescription": "Если включено, для новых задач можно выбирать режимы: Стандартный, PTC, Creator, Минимальный и пользовательские. Если выключено, все новые задачи используют режим по умолчанию (по умолчанию Стандартный; настраивается). Влияет только на новые задачи.",
63
+ "enablePickerToSetDefault": "Включите выбор режимов агента, чтобы задать режим по умолчанию",
64
+ "enablePickerToCreate": "Включите выбор режимов агента, чтобы запустить режим Creator"
59
65
  },
60
66
  "cordis": {
61
67
  "a11y.defining": "Определение плагина",
@@ -133,4 +139,4 @@
133
139
  "preset.readOnly": "Только чтение",
134
140
  "preset.workspaceWrite": "Запись в рабочую область"
135
141
  }
136
- }
142
+ }
@@ -52,6 +52,7 @@
52
52
  "viewer.text": "Простой текст",
53
53
  "wrap.aria": "Перенос строк",
54
54
  "wrap.disable": "Отключить перенос строк",
55
- "wrap.enable": "Включить перенос строк"
55
+ "wrap.enable": "Включить перенос строк",
56
+ "unsupportedFile": "Предпросмотр для этого типа файлов пока недоступен."
56
57
  }
57
58
  }
@@ -0,0 +1,48 @@
1
+ {
2
+ "settings.archivedSessions": {
3
+ "nav": "Архив сессий",
4
+ "search": "Поиск по архивным сессиям",
5
+ "loading": "Чтение сессий…",
6
+ "empty": "Нет архивных сессий.",
7
+ "unavailable": "Здесь нет архивных сессий, доступных для восстановления.",
8
+ "emptySearch": "Нет подходящих сессий.",
9
+ "unarchive": "Разархивировать",
10
+ "unarchiveNamed": "Разархивировать «{title}»",
11
+ "ungrouped": "Без группы",
12
+ "time.now": "сейчас",
13
+ "time.minutes": "{n} мин",
14
+ "time.hours": "{n} ч",
15
+ "time.days": "{n} дн",
16
+ "time.months": "{n} мес",
17
+ "time.years": "{n} г"
18
+ },
19
+ "sidebarTerminal": {
20
+ "attachmentEnded": "Подключение к терминалу завершено. Переподключитесь для продолжения.",
21
+ "cleanupFailed": "Не удалось завершить терминал «{title}»: {message}",
22
+ "closed": "Терминал закрыт.",
23
+ "connecting": "Подключение…",
24
+ "control": "Перехватить управление",
25
+ "creating": "Запуск…",
26
+ "description": "Выполнение команд в рабочей области сессии",
27
+ "disconnected": "Отключено.",
28
+ "exited": "Процесс завершился с кодом {code}",
29
+ "failed": "Ошибка терминала: {message}",
30
+ "inputFull": "Буфер ввода переполнен. Переподключитесь и повторите попытку.",
31
+ "invalidOutput": "Не удалось получить экран терминала. Переподключитесь для восстановления.",
32
+ "loading": "Чтение окружения терминала…",
33
+ "missingTerminal": "Этот терминал больше не существует. Откройте новый терминал.",
34
+ "new": "Новый терминал",
35
+ "readonly": "Этот вид доступен только для чтения.",
36
+ "reconnect": "Переподключиться",
37
+ "recoveryFailed": "Не удалось восстановить терминал: {message}",
38
+ "rename": "Название терминала",
39
+ "retry": "Повторить",
40
+ "retryRecovery": "Повторить восстановление терминала",
41
+ "shell": "Выбрать командную оболочку",
42
+ "shellEmpty": "Нет доступных оболочек",
43
+ "shellLoading": "Загрузка оболочек…",
44
+ "terminalLimit": "Достигнут лимит терминалов. Закройте неиспользуемые терминалы и повторите попытку. Завершённые терминалы также учитываются в лимите.",
45
+ "title": "Терминал",
46
+ "unavailable": "Недоступно"
47
+ }
48
+ }
@@ -531,6 +531,9 @@
531
531
  "hostIncompatibleBody": "Плагин {plugin} требует DSH версии {requirement}, а на этом сервере запущен {host}. Установка, скорее всего, нарушит работу плагина, и единственным выходом будет ручной откат версии.\nНичего не было изменено — плагин остаётся на текущей версии.\nЕсли вы уверены, что номер версии определён неверно, вы можете продолжить обновление.",
532
532
  "hostIncompatibleKeep": "Оставить текущую версию",
533
533
  "hostIncompatibleTitle": "Для этого релиза требуется более новая версия DSH",
534
- "hostIncompatibleUnknown": "неизвестная версия"
534
+ "hostIncompatibleUnknown": "неизвестная версия",
535
+ "hostUpdateHint": "Установлено и обновляется десктоп-клиентом; маркет только отображает статус",
536
+ "hostUpdateReady": "Новая версия {0}",
537
+ "setSelfHostManagedHint": "Эта копия установлена десктоп-клиентом; обновляйте её через десктопное приложение."
535
538
  }
536
- }
539
+ }
@@ -163,7 +163,9 @@
163
163
  "visibleModelsHideInactive": "Скрыть неактивные",
164
164
  "visibleModelsReset": "Сбросить выбор моделей",
165
165
  "visibleModelsStaleHint": "{count} выбранных моделей больше нет в каталоге (возможно, выведены из эксплуатации); на остальные модели это не влияет. Удалите их или сохраните.",
166
- "visibleModelsTitle": "Видимые модели"
166
+ "visibleModelsTitle": "Видимые модели",
167
+ "showSidebarQuota": "Показывать карточку квоты в боковой панели",
168
+ "showSidebarQuotaHint": "Когда включено, карточка тарифа и квоты Command Code располагается внизу боковой панели (над Настройками) и открывает полный дашборд. По умолчанию выключено: ничего не отображается слева и фоновый опрос не выполняется. Применяется при сохранении."
167
169
  },
168
170
  "commandcode": {
169
171
  "title": "📊 Использование Command Code{account}",
@@ -196,5 +198,56 @@
196
198
  "noData": "(нет данных — проверьте API-ключ)",
197
199
  "errorText": "Не удалось получить статистику Command Code: {message}",
198
200
  "imageGate": "В сессии уже есть изображения, а модель {model} не поддерживает изображения; выберите модель с поддержкой зрения или удалите изображения из сессии."
201
+ },
202
+ "panel.commandcode": {
203
+ "active": "Активен",
204
+ "close": "Закрыть",
205
+ "closeHint": "Вернуться к диалогу",
206
+ "coolingDown": "Охлаждение",
207
+ "credits": "Кредиты",
208
+ "errorGeneric": "Не удалось получить данные об использовании аккаунта",
209
+ "errorInvalidKey": "API-ключ недействителен или истёк",
210
+ "errorInvalidKeyHint": "Сервер отклонил все запросы (401). Проверьте ключ для этого аккаунта или сгенерируйте новый в консоли commandcode.ai.",
211
+ "errorNetwork": "Не удалось связаться с сервисом Command Code",
212
+ "errorNetworkHint": "Запрос не достиг сервера. Проверьте сетевое подключение или настройку базового URL API.",
213
+ "errorServiceUnavailable": "Сервис Command Code временно недоступен",
214
+ "errorServiceUnavailableHint": "Сервер возвращает ошибки (5xx). Повторите попытку через минуту.",
215
+ "exceeded": "Превышен",
216
+ "exhausted": "Исчерпан",
217
+ "failed": "ошибок",
218
+ "fiveHour": "5-часовое окно",
219
+ "fiveHourShort": "5 ч",
220
+ "free": "Бесплатно",
221
+ "invalidKey": "Неверный ключ",
222
+ "limits": "Лимиты",
223
+ "loading": "Загрузка…",
224
+ "monthly": "Месячный лимит",
225
+ "monthlyLimit": "Месячный лимит",
226
+ "monthlyUsed": "Использовано за месяц",
227
+ "nav": "Command Code",
228
+ "noKey": "Ключ не настроен",
229
+ "noKeyHint": "Добавьте API-ключ Command Code в Настройках, чтобы отслеживать квоту и кредиты.",
230
+ "partial": "Частично",
231
+ "periodEnds": "Окончание периода",
232
+ "plan": "Тариф",
233
+ "purchased": "Приобретено",
234
+ "refresh": "Обновить",
235
+ "refreshing": "Обновление…",
236
+ "remaining": "Осталось",
237
+ "requests": "Запросы",
238
+ "resets": "Сброс через",
239
+ "spend": "Расходы",
240
+ "subtitle": "Тарифы, кредиты и плавающие окна квот",
241
+ "successRate": "Успешность",
242
+ "tokens": "Токены",
243
+ "tokensIn": "Входящие токены",
244
+ "tokensOut": "Исходящие токены",
245
+ "unavailable": "Недоступно",
246
+ "unconfigured": "Не настроено",
247
+ "updated": "Обновлено",
248
+ "usage": "Использование",
249
+ "weekly": "Недельное окно",
250
+ "weeklyShort": "7 дн",
251
+ "windowUnlimited": "Без ограничений"
199
252
  }
200
- }
253
+ }