dsh-oc-tui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -563
- package/bin/mcp-sse-bridge.mjs +499 -499
- package/docs/assets/conversation.png +0 -0
- package/docs/assets/cover.png +0 -0
- package/docs/assets/home.png +0 -0
- package/docs/assets/settings.png +0 -0
- package/docs/dsh-std-/346/216/245/345/205/245/350/257/264/346/230/216.md +566 -0
- package/docs/user-guide.md +616 -0
- package/docs//347/224/250/346/210/267/346/211/213/345/206/214.md +607 -593
- package/dsh-plugin.json +69 -69
- package/lib/boot-block.js +129 -0
- package/lib/diagnostics.js +162 -120
- package/lib/dsh-version.js +161 -0
- package/lib/git-branch.js +157 -157
- package/lib/i18n.js +283 -283
- package/lib/image.js +2 -1
- package/lib/index.js +5183 -4812
- package/lib/locales/ar.js +670 -635
- package/lib/locales/de.js +639 -604
- package/lib/locales/en.js +35 -0
- package/lib/locales/es.js +639 -604
- package/lib/locales/fr.js +639 -604
- package/lib/locales/id.js +641 -606
- package/lib/locales/it.js +639 -604
- package/lib/locales/ja.js +639 -604
- package/lib/locales/ko.js +639 -604
- package/lib/locales/nl.js +639 -604
- package/lib/locales/pl.js +653 -618
- package/lib/locales/pt-BR.js +639 -604
- package/lib/locales/ru.js +653 -618
- package/lib/locales/tr.js +639 -604
- package/lib/locales/vi.js +641 -606
- package/lib/locales/zh-CN.js +640 -605
- package/lib/locales/zh-TW.js +640 -605
- package/lib/markdown.js +101 -27
- package/lib/mcp-monitor.js +189 -0
- package/lib/mcp-skill.js +93 -93
- package/lib/mcp.js +865 -838
- package/lib/model-route.js +66 -0
- package/lib/providers.js +712 -712
- package/lib/screenshot.js +140 -0
- package/lib/splash-opencode.js +29 -26
- package/lib/startup.js +66 -48
- package/lib/term.js +53 -11
- package/lib/themes.js +101 -0
- package/lib/ui.js +685 -443
- package/lib/updates.js +5 -2
- package/lib/web-settings.js +52 -21
- package/package.json +160 -146
- package/skills/mcp-import/SKILL.md +216 -216
package/lib/i18n.js
CHANGED
|
@@ -1,283 +1,283 @@
|
|
|
1
|
-
// The TUI's message catalog and locale resolution. Every user-facing string the
|
|
2
|
-
// TUI paints or toasts comes from here: `lib/locales/<id>.js` holds the
|
|
3
|
-
// translations, English is the source catalog (and the fallback for any key a
|
|
4
|
-
// translation has not covered yet).
|
|
5
|
-
//
|
|
6
|
-
// A catalog is a flat `{ 'area.key': 'text' }` map. Interpolation uses `{name}`
|
|
7
|
-
// placeholders, and a plural entry is a family of keys suffixed with the CLDR
|
|
8
|
-
// plural categories: `key.one`, `key.other`, `key.few`, `key.many`, `key.zero`,
|
|
9
|
-
// `key.two`. `tn()` picks the category with Intl.PluralRules for the active
|
|
10
|
-
// locale, falling back to `key.other` and then to the base key.
|
|
11
|
-
import en from './locales/en.js'
|
|
12
|
-
import zhCN from './locales/zh-CN.js'
|
|
13
|
-
import zhTW from './locales/zh-TW.js'
|
|
14
|
-
import ja from './locales/ja.js'
|
|
15
|
-
import ko from './locales/ko.js'
|
|
16
|
-
import de from './locales/de.js'
|
|
17
|
-
import fr from './locales/fr.js'
|
|
18
|
-
import es from './locales/es.js'
|
|
19
|
-
import ptBR from './locales/pt-BR.js'
|
|
20
|
-
import ru from './locales/ru.js'
|
|
21
|
-
import it from './locales/it.js'
|
|
22
|
-
import tr from './locales/tr.js'
|
|
23
|
-
import vi from './locales/vi.js'
|
|
24
|
-
import id from './locales/id.js'
|
|
25
|
-
import pl from './locales/pl.js'
|
|
26
|
-
import nl from './locales/nl.js'
|
|
27
|
-
import ar from './locales/ar.js'
|
|
28
|
-
|
|
29
|
-
// The languages offered by Settings -> General -> Language, in the order the
|
|
30
|
-
// picker lists them. `native` is what the row shows (a user looking for their
|
|
31
|
-
// own language finds it in its own script); `english` is only used by the docs
|
|
32
|
-
// and the tests. `bcp47` feeds Intl (plural rules, number formatting).
|
|
33
|
-
export const LOCALES = Object.freeze([
|
|
34
|
-
{ id: 'en', native: 'English', english: 'English', bcp47: 'en' },
|
|
35
|
-
{ id: 'zh-CN', native: '简体中文', english: 'Simplified Chinese', bcp47: 'zh-Hans' },
|
|
36
|
-
{ id: 'zh-TW', native: '繁體中文', english: 'Traditional Chinese', bcp47: 'zh-Hant' },
|
|
37
|
-
{ id: 'ja', native: '日本語', english: 'Japanese', bcp47: 'ja' },
|
|
38
|
-
{ id: 'ko', native: '한국어', english: 'Korean', bcp47: 'ko' },
|
|
39
|
-
{ id: 'de', native: 'Deutsch', english: 'German', bcp47: 'de' },
|
|
40
|
-
{ id: 'fr', native: 'Français', english: 'French', bcp47: 'fr' },
|
|
41
|
-
{ id: 'es', native: 'Español', english: 'Spanish', bcp47: 'es' },
|
|
42
|
-
{ id: 'pt-BR', native: 'Português (Brasil)', english: 'Portuguese (Brazil)', bcp47: 'pt-BR' },
|
|
43
|
-
{ id: 'ru', native: 'Русский', english: 'Russian', bcp47: 'ru' },
|
|
44
|
-
{ id: 'it', native: 'Italiano', english: 'Italian', bcp47: 'it' },
|
|
45
|
-
{ id: 'tr', native: 'Türkçe', english: 'Turkish', bcp47: 'tr' },
|
|
46
|
-
{ id: 'vi', native: 'Tiếng Việt', english: 'Vietnamese', bcp47: 'vi' },
|
|
47
|
-
{ id: 'id', native: 'Bahasa Indonesia', english: 'Indonesian', bcp47: 'id' },
|
|
48
|
-
{ id: 'pl', native: 'Polski', english: 'Polish', bcp47: 'pl' },
|
|
49
|
-
{ id: 'nl', native: 'Nederlands', english: 'Dutch', bcp47: 'nl' },
|
|
50
|
-
{ id: 'ar', native: 'العربية', english: 'Arabic', bcp47: 'ar' },
|
|
51
|
-
])
|
|
52
|
-
|
|
53
|
-
const CATALOGS = {
|
|
54
|
-
en,
|
|
55
|
-
'zh-CN': zhCN,
|
|
56
|
-
'zh-TW': zhTW,
|
|
57
|
-
ja,
|
|
58
|
-
ko,
|
|
59
|
-
de,
|
|
60
|
-
fr,
|
|
61
|
-
es,
|
|
62
|
-
'pt-BR': ptBR,
|
|
63
|
-
ru,
|
|
64
|
-
it,
|
|
65
|
-
tr,
|
|
66
|
-
vi,
|
|
67
|
-
id,
|
|
68
|
-
pl,
|
|
69
|
-
nl,
|
|
70
|
-
ar,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const LOCALE_IDS = LOCALES.map((entry) => entry.id)
|
|
74
|
-
const LOCALE_BY_ID = new Map(LOCALES.map((entry) => [entry.id, entry]))
|
|
75
|
-
// Lookup is case-insensitive (`pt-br` from `pt_BR`, `zh-tw` from `zh-TW`), but
|
|
76
|
-
// the value handed back is always the catalog's own spelling.
|
|
77
|
-
const LOCALE_ID_BY_LOWER = new Map(LOCALES.map((entry) => [entry.id.toLowerCase(), entry.id]))
|
|
78
|
-
|
|
79
|
-
// Aliases a locale can arrive under: POSIX env values (zh_CN.UTF-8), the
|
|
80
|
-
// WebUI's two-letter ids (zh), and regional variants that have no catalog of
|
|
81
|
-
// their own (en-US, pt-PT, es-419...).
|
|
82
|
-
const ALIASES = {
|
|
83
|
-
zh: 'zh-CN',
|
|
84
|
-
'zh-hans': 'zh-CN',
|
|
85
|
-
'zh-sg': 'zh-CN',
|
|
86
|
-
'zh-hant': 'zh-TW',
|
|
87
|
-
'zh-hk': 'zh-TW',
|
|
88
|
-
'zh-mo': 'zh-TW',
|
|
89
|
-
pt: 'pt-BR',
|
|
90
|
-
in: 'id',
|
|
91
|
-
'pt-pt': 'pt-BR',
|
|
92
|
-
'en-us': 'en',
|
|
93
|
-
'en-gb': 'en',
|
|
94
|
-
'es-419': 'es',
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const PLURAL_CATEGORIES = ['zero', 'one', 'two', 'few', 'many', 'other']
|
|
98
|
-
|
|
99
|
-
// English until the plugin resolves the configured language at boot. Tests and
|
|
100
|
-
// modules that never boot the plugin therefore render English.
|
|
101
|
-
let currentLocale = 'en'
|
|
102
|
-
const pluralRules = new Map()
|
|
103
|
-
|
|
104
|
-
function normalizeId(raw) {
|
|
105
|
-
if (typeof raw !== 'string') return null
|
|
106
|
-
const value = raw.trim().replace(/_/g, '-').toLowerCase()
|
|
107
|
-
if (value === '' || value === 'system' || value === 'auto' || value === 'c' || value === 'posix') return null
|
|
108
|
-
const exact = LOCALE_ID_BY_LOWER.get(value)
|
|
109
|
-
if (exact) return exact
|
|
110
|
-
if (ALIASES[value]) return ALIASES[value]
|
|
111
|
-
// `ja-JP`, `de-DE`, `ru-RU`: the bare language id is the catalog.
|
|
112
|
-
const base = value.split('-')[0]
|
|
113
|
-
const byBase = LOCALE_ID_BY_LOWER.get(base)
|
|
114
|
-
if (byBase) return byBase
|
|
115
|
-
if (ALIASES[base]) return ALIASES[base]
|
|
116
|
-
return null
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// The POSIX/Windows language variables, most specific first. `DSH_TUI_LOCALE`
|
|
120
|
-
// wins so a single run can be pinned without touching the settings file.
|
|
121
|
-
export function detectSystemLocale(env = process.env) {
|
|
122
|
-
for (const name of ['DSH_TUI_LOCALE', 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG']) {
|
|
123
|
-
const value = env?.[name]
|
|
124
|
-
if (typeof value !== 'string' || value.trim() === '') continue
|
|
125
|
-
// LANGUAGE can be a colon-separated priority list.
|
|
126
|
-
for (const part of value.split(':')) {
|
|
127
|
-
const id = normalizeId(part)
|
|
128
|
-
if (id) return id
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
// Windows exports none of the above: its locale lives in the OS regional
|
|
132
|
-
// settings, which Node's ICU default reflects. Only the real environment is
|
|
133
|
-
// consulted — a caller that passes an explicit table is asking about that
|
|
134
|
-
// table, not about this machine.
|
|
135
|
-
if (env !== process.env) return null
|
|
136
|
-
try {
|
|
137
|
-
return normalizeId(Intl.DateTimeFormat().resolvedOptions().locale)
|
|
138
|
-
} catch {
|
|
139
|
-
return null
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Resolve a stored preference ('system' or a locale id) to a catalog id.
|
|
144
|
-
export function resolveLocale(preference, env = process.env) {
|
|
145
|
-
if (preference !== undefined && preference !== null) {
|
|
146
|
-
const direct = normalizeId(preference)
|
|
147
|
-
if (direct) return direct
|
|
148
|
-
}
|
|
149
|
-
return detectSystemLocale(env) ?? 'en'
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function isLocaleId(id) {
|
|
153
|
-
return typeof id === 'string' && LOCALE_BY_ID.has(id)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function localeInfo(id) {
|
|
157
|
-
return LOCALE_BY_ID.get(id) ?? LOCALE_BY_ID.get('en')
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function getLocale() {
|
|
161
|
-
return currentLocale
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Switch the active catalog. Returns the id actually in effect (an unknown id
|
|
165
|
-
// falls back to English instead of throwing — a stale settings value must not
|
|
166
|
-
// take the TUI down).
|
|
167
|
-
export function setLocale(id) {
|
|
168
|
-
const next = normalizeId(id) ?? 'en'
|
|
169
|
-
currentLocale = next
|
|
170
|
-
return currentLocale
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function interpolate(template, params) {
|
|
174
|
-
if (!params) return template
|
|
175
|
-
return template.replace(/\{(\w+)\}/g, (match, name) => {
|
|
176
|
-
const value = params[name]
|
|
177
|
-
return value === undefined || value === null ? match : String(value)
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function lookup(catalog, key) {
|
|
182
|
-
const value = catalog?.[key]
|
|
183
|
-
return typeof value === 'string' ? value : undefined
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// The message for `key` in the active locale. A missing translation falls back
|
|
187
|
-
// to English; a key missing from both returns the key itself, which makes an
|
|
188
|
-
// uncovered string visible in the UI instead of painting "undefined".
|
|
189
|
-
export function t(key, params) {
|
|
190
|
-
const catalog = CATALOGS[currentLocale] ?? en
|
|
191
|
-
const message = lookup(catalog, key) ?? lookup(en, key) ?? key
|
|
192
|
-
return interpolate(message, params)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function categoryFor(localeId, count) {
|
|
196
|
-
const info = localeInfo(localeId)
|
|
197
|
-
let rules = pluralRules.get(info.bcp47)
|
|
198
|
-
if (!rules) {
|
|
199
|
-
try {
|
|
200
|
-
rules = new Intl.PluralRules(info.bcp47)
|
|
201
|
-
} catch {
|
|
202
|
-
rules = new Intl.PluralRules('en')
|
|
203
|
-
}
|
|
204
|
-
pluralRules.set(info.bcp47, rules)
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
return rules.select(count)
|
|
208
|
-
} catch {
|
|
209
|
-
return 'other'
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// A pluralized message: `tn('transcript.turns', 3, { count: 3 })` selects
|
|
214
|
-
// `transcript.turns.other` (or `.one`, `.few`... per the locale's CLDR rules).
|
|
215
|
-
// The base key itself is the last fallback so a catalog may store a single
|
|
216
|
-
// form for languages that do not inflect.
|
|
217
|
-
export function tn(key, count, params) {
|
|
218
|
-
const catalog = CATALOGS[currentLocale] ?? en
|
|
219
|
-
const category = categoryFor(currentLocale, count)
|
|
220
|
-
const candidates = [key + '.' + category, key + '.other', key]
|
|
221
|
-
for (const candidate of candidates) {
|
|
222
|
-
const message = lookup(catalog, candidate) ?? lookup(en, candidate)
|
|
223
|
-
if (message !== undefined) return interpolate(message, { count, n: count, ...params })
|
|
224
|
-
}
|
|
225
|
-
return key
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Stored ids whose label key is not simply `value.<id>`.
|
|
229
|
-
const VALUE_KEY_ALIASES = { 'text only': 'value.textOnly', 'text + image': 'value.textImage' }
|
|
230
|
-
|
|
231
|
-
// The label of a stored choice value (`auto`, `queue`, `halfblock`...). A value
|
|
232
|
-
// with no catalog entry is shown verbatim, which is what keeps ids such as a
|
|
233
|
-
// preset name or a provider id readable — and what lets the technical values
|
|
234
|
-
// (`halfblock`, `chip`, `stdio`, `http`, `sse`) stay untranslated on purpose.
|
|
235
|
-
export function tv(value) {
|
|
236
|
-
const id = String(value)
|
|
237
|
-
const catalog = CATALOGS[currentLocale] ?? en
|
|
238
|
-
for (const key of [VALUE_KEY_ALIASES[id] ?? 'value.' + id, 'common.' + id]) {
|
|
239
|
-
const message = lookup(catalog, key) ?? lookup(en, key)
|
|
240
|
-
if (message !== undefined) return message
|
|
241
|
-
}
|
|
242
|
-
return id
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Choice rows for a settings field: translated labels, unchanged values.
|
|
246
|
-
export function valueChoices(values) {
|
|
247
|
-
return values.map((value) => {
|
|
248
|
-
const label = tv(value)
|
|
249
|
-
return label === String(value) ? value : { label, value }
|
|
250
|
-
})
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// The keys a catalog is missing relative to English (used by the i18n tests and
|
|
254
|
-
// by translators to check their work).
|
|
255
|
-
export function catalogGaps(localeId) {
|
|
256
|
-
const catalog = CATALOGS[localeId]
|
|
257
|
-
if (!catalog) return null
|
|
258
|
-
return Object.keys(en).filter((key) => typeof catalog[key] !== 'string')
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Extra keys a catalog carries that English does not know about (a typo in a
|
|
262
|
-
// translation file, or a stale key left behind by an English rename). A CLDR
|
|
263
|
-
// plural form English happens not to use (`ru` needs `.few`/`.many`, `ar` also
|
|
264
|
-
// `.zero`/`.two`) is not an extra: it is a form of a family English does define.
|
|
265
|
-
export function catalogExtras(localeId) {
|
|
266
|
-
const catalog = CATALOGS[localeId]
|
|
267
|
-
if (!catalog) return null
|
|
268
|
-
return Object.keys(catalog).filter((key) => {
|
|
269
|
-
if (typeof en[key] === 'string') return false
|
|
270
|
-
const dot = key.lastIndexOf('.')
|
|
271
|
-
if (dot < 0) return true
|
|
272
|
-
if (!PLURAL_CATEGORIES.includes(key.slice(dot + 1))) return true
|
|
273
|
-
return typeof en[key.slice(0, dot) + '.other'] !== 'string'
|
|
274
|
-
})
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function catalogKeys() {
|
|
278
|
-
return Object.keys(en)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export function hasLocale(id) {
|
|
282
|
-
return Object.hasOwn(CATALOGS, id)
|
|
283
|
-
}
|
|
1
|
+
// The TUI's message catalog and locale resolution. Every user-facing string the
|
|
2
|
+
// TUI paints or toasts comes from here: `lib/locales/<id>.js` holds the
|
|
3
|
+
// translations, English is the source catalog (and the fallback for any key a
|
|
4
|
+
// translation has not covered yet).
|
|
5
|
+
//
|
|
6
|
+
// A catalog is a flat `{ 'area.key': 'text' }` map. Interpolation uses `{name}`
|
|
7
|
+
// placeholders, and a plural entry is a family of keys suffixed with the CLDR
|
|
8
|
+
// plural categories: `key.one`, `key.other`, `key.few`, `key.many`, `key.zero`,
|
|
9
|
+
// `key.two`. `tn()` picks the category with Intl.PluralRules for the active
|
|
10
|
+
// locale, falling back to `key.other` and then to the base key.
|
|
11
|
+
import en from './locales/en.js'
|
|
12
|
+
import zhCN from './locales/zh-CN.js'
|
|
13
|
+
import zhTW from './locales/zh-TW.js'
|
|
14
|
+
import ja from './locales/ja.js'
|
|
15
|
+
import ko from './locales/ko.js'
|
|
16
|
+
import de from './locales/de.js'
|
|
17
|
+
import fr from './locales/fr.js'
|
|
18
|
+
import es from './locales/es.js'
|
|
19
|
+
import ptBR from './locales/pt-BR.js'
|
|
20
|
+
import ru from './locales/ru.js'
|
|
21
|
+
import it from './locales/it.js'
|
|
22
|
+
import tr from './locales/tr.js'
|
|
23
|
+
import vi from './locales/vi.js'
|
|
24
|
+
import id from './locales/id.js'
|
|
25
|
+
import pl from './locales/pl.js'
|
|
26
|
+
import nl from './locales/nl.js'
|
|
27
|
+
import ar from './locales/ar.js'
|
|
28
|
+
|
|
29
|
+
// The languages offered by Settings -> General -> Language, in the order the
|
|
30
|
+
// picker lists them. `native` is what the row shows (a user looking for their
|
|
31
|
+
// own language finds it in its own script); `english` is only used by the docs
|
|
32
|
+
// and the tests. `bcp47` feeds Intl (plural rules, number formatting).
|
|
33
|
+
export const LOCALES = Object.freeze([
|
|
34
|
+
{ id: 'en', native: 'English', english: 'English', bcp47: 'en' },
|
|
35
|
+
{ id: 'zh-CN', native: '简体中文', english: 'Simplified Chinese', bcp47: 'zh-Hans' },
|
|
36
|
+
{ id: 'zh-TW', native: '繁體中文', english: 'Traditional Chinese', bcp47: 'zh-Hant' },
|
|
37
|
+
{ id: 'ja', native: '日本語', english: 'Japanese', bcp47: 'ja' },
|
|
38
|
+
{ id: 'ko', native: '한국어', english: 'Korean', bcp47: 'ko' },
|
|
39
|
+
{ id: 'de', native: 'Deutsch', english: 'German', bcp47: 'de' },
|
|
40
|
+
{ id: 'fr', native: 'Français', english: 'French', bcp47: 'fr' },
|
|
41
|
+
{ id: 'es', native: 'Español', english: 'Spanish', bcp47: 'es' },
|
|
42
|
+
{ id: 'pt-BR', native: 'Português (Brasil)', english: 'Portuguese (Brazil)', bcp47: 'pt-BR' },
|
|
43
|
+
{ id: 'ru', native: 'Русский', english: 'Russian', bcp47: 'ru' },
|
|
44
|
+
{ id: 'it', native: 'Italiano', english: 'Italian', bcp47: 'it' },
|
|
45
|
+
{ id: 'tr', native: 'Türkçe', english: 'Turkish', bcp47: 'tr' },
|
|
46
|
+
{ id: 'vi', native: 'Tiếng Việt', english: 'Vietnamese', bcp47: 'vi' },
|
|
47
|
+
{ id: 'id', native: 'Bahasa Indonesia', english: 'Indonesian', bcp47: 'id' },
|
|
48
|
+
{ id: 'pl', native: 'Polski', english: 'Polish', bcp47: 'pl' },
|
|
49
|
+
{ id: 'nl', native: 'Nederlands', english: 'Dutch', bcp47: 'nl' },
|
|
50
|
+
{ id: 'ar', native: 'العربية', english: 'Arabic', bcp47: 'ar' },
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
const CATALOGS = {
|
|
54
|
+
en,
|
|
55
|
+
'zh-CN': zhCN,
|
|
56
|
+
'zh-TW': zhTW,
|
|
57
|
+
ja,
|
|
58
|
+
ko,
|
|
59
|
+
de,
|
|
60
|
+
fr,
|
|
61
|
+
es,
|
|
62
|
+
'pt-BR': ptBR,
|
|
63
|
+
ru,
|
|
64
|
+
it,
|
|
65
|
+
tr,
|
|
66
|
+
vi,
|
|
67
|
+
id,
|
|
68
|
+
pl,
|
|
69
|
+
nl,
|
|
70
|
+
ar,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const LOCALE_IDS = LOCALES.map((entry) => entry.id)
|
|
74
|
+
const LOCALE_BY_ID = new Map(LOCALES.map((entry) => [entry.id, entry]))
|
|
75
|
+
// Lookup is case-insensitive (`pt-br` from `pt_BR`, `zh-tw` from `zh-TW`), but
|
|
76
|
+
// the value handed back is always the catalog's own spelling.
|
|
77
|
+
const LOCALE_ID_BY_LOWER = new Map(LOCALES.map((entry) => [entry.id.toLowerCase(), entry.id]))
|
|
78
|
+
|
|
79
|
+
// Aliases a locale can arrive under: POSIX env values (zh_CN.UTF-8), the
|
|
80
|
+
// WebUI's two-letter ids (zh), and regional variants that have no catalog of
|
|
81
|
+
// their own (en-US, pt-PT, es-419...).
|
|
82
|
+
const ALIASES = {
|
|
83
|
+
zh: 'zh-CN',
|
|
84
|
+
'zh-hans': 'zh-CN',
|
|
85
|
+
'zh-sg': 'zh-CN',
|
|
86
|
+
'zh-hant': 'zh-TW',
|
|
87
|
+
'zh-hk': 'zh-TW',
|
|
88
|
+
'zh-mo': 'zh-TW',
|
|
89
|
+
pt: 'pt-BR',
|
|
90
|
+
in: 'id',
|
|
91
|
+
'pt-pt': 'pt-BR',
|
|
92
|
+
'en-us': 'en',
|
|
93
|
+
'en-gb': 'en',
|
|
94
|
+
'es-419': 'es',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const PLURAL_CATEGORIES = ['zero', 'one', 'two', 'few', 'many', 'other']
|
|
98
|
+
|
|
99
|
+
// English until the plugin resolves the configured language at boot. Tests and
|
|
100
|
+
// modules that never boot the plugin therefore render English.
|
|
101
|
+
let currentLocale = 'en'
|
|
102
|
+
const pluralRules = new Map()
|
|
103
|
+
|
|
104
|
+
function normalizeId(raw) {
|
|
105
|
+
if (typeof raw !== 'string') return null
|
|
106
|
+
const value = raw.trim().replace(/_/g, '-').toLowerCase()
|
|
107
|
+
if (value === '' || value === 'system' || value === 'auto' || value === 'c' || value === 'posix') return null
|
|
108
|
+
const exact = LOCALE_ID_BY_LOWER.get(value)
|
|
109
|
+
if (exact) return exact
|
|
110
|
+
if (ALIASES[value]) return ALIASES[value]
|
|
111
|
+
// `ja-JP`, `de-DE`, `ru-RU`: the bare language id is the catalog.
|
|
112
|
+
const base = value.split('-')[0]
|
|
113
|
+
const byBase = LOCALE_ID_BY_LOWER.get(base)
|
|
114
|
+
if (byBase) return byBase
|
|
115
|
+
if (ALIASES[base]) return ALIASES[base]
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The POSIX/Windows language variables, most specific first. `DSH_TUI_LOCALE`
|
|
120
|
+
// wins so a single run can be pinned without touching the settings file.
|
|
121
|
+
export function detectSystemLocale(env = process.env) {
|
|
122
|
+
for (const name of ['DSH_TUI_LOCALE', 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG']) {
|
|
123
|
+
const value = env?.[name]
|
|
124
|
+
if (typeof value !== 'string' || value.trim() === '') continue
|
|
125
|
+
// LANGUAGE can be a colon-separated priority list.
|
|
126
|
+
for (const part of value.split(':')) {
|
|
127
|
+
const id = normalizeId(part)
|
|
128
|
+
if (id) return id
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Windows exports none of the above: its locale lives in the OS regional
|
|
132
|
+
// settings, which Node's ICU default reflects. Only the real environment is
|
|
133
|
+
// consulted — a caller that passes an explicit table is asking about that
|
|
134
|
+
// table, not about this machine.
|
|
135
|
+
if (env !== process.env) return null
|
|
136
|
+
try {
|
|
137
|
+
return normalizeId(Intl.DateTimeFormat().resolvedOptions().locale)
|
|
138
|
+
} catch {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Resolve a stored preference ('system' or a locale id) to a catalog id.
|
|
144
|
+
export function resolveLocale(preference, env = process.env) {
|
|
145
|
+
if (preference !== undefined && preference !== null) {
|
|
146
|
+
const direct = normalizeId(preference)
|
|
147
|
+
if (direct) return direct
|
|
148
|
+
}
|
|
149
|
+
return detectSystemLocale(env) ?? 'en'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function isLocaleId(id) {
|
|
153
|
+
return typeof id === 'string' && LOCALE_BY_ID.has(id)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function localeInfo(id) {
|
|
157
|
+
return LOCALE_BY_ID.get(id) ?? LOCALE_BY_ID.get('en')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function getLocale() {
|
|
161
|
+
return currentLocale
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Switch the active catalog. Returns the id actually in effect (an unknown id
|
|
165
|
+
// falls back to English instead of throwing — a stale settings value must not
|
|
166
|
+
// take the TUI down).
|
|
167
|
+
export function setLocale(id) {
|
|
168
|
+
const next = normalizeId(id) ?? 'en'
|
|
169
|
+
currentLocale = next
|
|
170
|
+
return currentLocale
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function interpolate(template, params) {
|
|
174
|
+
if (!params) return template
|
|
175
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => {
|
|
176
|
+
const value = params[name]
|
|
177
|
+
return value === undefined || value === null ? match : String(value)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function lookup(catalog, key) {
|
|
182
|
+
const value = catalog?.[key]
|
|
183
|
+
return typeof value === 'string' ? value : undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The message for `key` in the active locale. A missing translation falls back
|
|
187
|
+
// to English; a key missing from both returns the key itself, which makes an
|
|
188
|
+
// uncovered string visible in the UI instead of painting "undefined".
|
|
189
|
+
export function t(key, params) {
|
|
190
|
+
const catalog = CATALOGS[currentLocale] ?? en
|
|
191
|
+
const message = lookup(catalog, key) ?? lookup(en, key) ?? key
|
|
192
|
+
return interpolate(message, params)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function categoryFor(localeId, count) {
|
|
196
|
+
const info = localeInfo(localeId)
|
|
197
|
+
let rules = pluralRules.get(info.bcp47)
|
|
198
|
+
if (!rules) {
|
|
199
|
+
try {
|
|
200
|
+
rules = new Intl.PluralRules(info.bcp47)
|
|
201
|
+
} catch {
|
|
202
|
+
rules = new Intl.PluralRules('en')
|
|
203
|
+
}
|
|
204
|
+
pluralRules.set(info.bcp47, rules)
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return rules.select(count)
|
|
208
|
+
} catch {
|
|
209
|
+
return 'other'
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// A pluralized message: `tn('transcript.turns', 3, { count: 3 })` selects
|
|
214
|
+
// `transcript.turns.other` (or `.one`, `.few`... per the locale's CLDR rules).
|
|
215
|
+
// The base key itself is the last fallback so a catalog may store a single
|
|
216
|
+
// form for languages that do not inflect.
|
|
217
|
+
export function tn(key, count, params) {
|
|
218
|
+
const catalog = CATALOGS[currentLocale] ?? en
|
|
219
|
+
const category = categoryFor(currentLocale, count)
|
|
220
|
+
const candidates = [key + '.' + category, key + '.other', key]
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
const message = lookup(catalog, candidate) ?? lookup(en, candidate)
|
|
223
|
+
if (message !== undefined) return interpolate(message, { count, n: count, ...params })
|
|
224
|
+
}
|
|
225
|
+
return key
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Stored ids whose label key is not simply `value.<id>`.
|
|
229
|
+
const VALUE_KEY_ALIASES = { 'text only': 'value.textOnly', 'text + image': 'value.textImage' }
|
|
230
|
+
|
|
231
|
+
// The label of a stored choice value (`auto`, `queue`, `halfblock`...). A value
|
|
232
|
+
// with no catalog entry is shown verbatim, which is what keeps ids such as a
|
|
233
|
+
// preset name or a provider id readable — and what lets the technical values
|
|
234
|
+
// (`halfblock`, `chip`, `stdio`, `http`, `sse`) stay untranslated on purpose.
|
|
235
|
+
export function tv(value) {
|
|
236
|
+
const id = String(value)
|
|
237
|
+
const catalog = CATALOGS[currentLocale] ?? en
|
|
238
|
+
for (const key of [VALUE_KEY_ALIASES[id] ?? 'value.' + id, 'common.' + id]) {
|
|
239
|
+
const message = lookup(catalog, key) ?? lookup(en, key)
|
|
240
|
+
if (message !== undefined) return message
|
|
241
|
+
}
|
|
242
|
+
return id
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Choice rows for a settings field: translated labels, unchanged values.
|
|
246
|
+
export function valueChoices(values) {
|
|
247
|
+
return values.map((value) => {
|
|
248
|
+
const label = tv(value)
|
|
249
|
+
return label === String(value) ? value : { label, value }
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// The keys a catalog is missing relative to English (used by the i18n tests and
|
|
254
|
+
// by translators to check their work).
|
|
255
|
+
export function catalogGaps(localeId) {
|
|
256
|
+
const catalog = CATALOGS[localeId]
|
|
257
|
+
if (!catalog) return null
|
|
258
|
+
return Object.keys(en).filter((key) => typeof catalog[key] !== 'string')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Extra keys a catalog carries that English does not know about (a typo in a
|
|
262
|
+
// translation file, or a stale key left behind by an English rename). A CLDR
|
|
263
|
+
// plural form English happens not to use (`ru` needs `.few`/`.many`, `ar` also
|
|
264
|
+
// `.zero`/`.two`) is not an extra: it is a form of a family English does define.
|
|
265
|
+
export function catalogExtras(localeId) {
|
|
266
|
+
const catalog = CATALOGS[localeId]
|
|
267
|
+
if (!catalog) return null
|
|
268
|
+
return Object.keys(catalog).filter((key) => {
|
|
269
|
+
if (typeof en[key] === 'string') return false
|
|
270
|
+
const dot = key.lastIndexOf('.')
|
|
271
|
+
if (dot < 0) return true
|
|
272
|
+
if (!PLURAL_CATEGORIES.includes(key.slice(dot + 1))) return true
|
|
273
|
+
return typeof en[key.slice(0, dot) + '.other'] !== 'string'
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function catalogKeys() {
|
|
278
|
+
return Object.keys(en)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function hasLocale(id) {
|
|
282
|
+
return Object.hasOwn(CATALOGS, id)
|
|
283
|
+
}
|
package/lib/image.js
CHANGED
|
@@ -320,7 +320,8 @@ export async function renderImage(source, { protocol, maxCellsW, maxCellsH, cell
|
|
|
320
320
|
return { protocol, cellsW, cellsH: rows, hash, payload: { kind: 'iterm2', b64, bytes: png.length, pxW, pxH: bandH } }
|
|
321
321
|
}
|
|
322
322
|
if (protocol === 'sixel') {
|
|
323
|
-
|
|
323
|
+
const png = await sharp(data, { raw: { width: pxW, height: bandH, channels: 4 } }).png().toBuffer()
|
|
324
|
+
return { protocol, cellsW, cellsH: rows, hash, payload: { kind: 'sixel', s: encodeSixel(data, pxW, bandH), png } }
|
|
324
325
|
}
|
|
325
326
|
const segLines = renderHalfblock(data, pxW, bandH, cellsW, rows, { bgHex })
|
|
326
327
|
return { protocol: 'halfblock', cellsW, cellsH: rows, hash, segLines }
|