mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 14:30:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70b5ab2bd6 | ||
|
|
753d6b7aa4 | ||
|
|
a4b5204c74 | ||
|
|
64ff394847 | ||
|
|
ae52ef461e | ||
|
|
b6cdd602f1 | ||
|
|
4735e9ace0 | ||
|
|
ca0c59ff0c | ||
|
|
2f3ba0e581 | ||
|
|
cf34c14f3a | ||
|
|
f696163674 | ||
|
|
b18c3e8c4f | ||
|
|
e658d008dc | ||
|
|
a82b006428 | ||
|
|
78554e6d37 | ||
|
|
0c7f69dc8f | ||
|
|
45a38237ad | ||
|
|
74eeecb190 | ||
|
|
584ccf9482 | ||
|
|
21350d64db | ||
|
|
04810ae15e | ||
|
|
729958ea90 | ||
|
|
c89e3d9574 | ||
|
|
fa445a8466 | ||
|
|
b2d4883006 | ||
|
|
9848107592 | ||
|
|
9a45005406 | ||
|
|
e06b47625f | ||
|
|
609c664eed | ||
|
|
37a136aeb5 | ||
|
|
1ce4797bdb | ||
|
|
d8aacbe4e9 | ||
|
|
9c66e7f309 | ||
|
|
8020d45808 | ||
|
|
48157f56a8 | ||
|
|
dbe3e693e7 | ||
|
|
fc6a686ad9 | ||
|
|
da1e9547ec | ||
|
|
f615fc4b89 | ||
|
|
5c0e5f5538 | ||
|
|
c0d90178ab | ||
|
|
f291ba5ebf | ||
|
|
5e16c72c03 | ||
|
|
c02848bb08 | ||
|
|
f67a29ecd7 | ||
|
|
793f86129c | ||
|
|
9811e1ad24 | ||
|
|
1840fa53f8 | ||
|
|
416ea21424 | ||
|
|
82af11f26e | ||
|
|
b04e8f58bf | ||
|
|
54e4d8dfbb | ||
|
|
45d7046b16 | ||
|
|
ae8babcf5a | ||
|
|
9b2b8be2b8 | ||
|
|
b6dec0dba3 | ||
|
|
5ba34b647e | ||
|
|
73bc62948f | ||
|
|
524db08a8e | ||
|
|
7d8f46d00d | ||
|
|
387866cb71 | ||
|
|
4d72c80189 | ||
|
|
317c05f9f4 | ||
|
|
a6141ebb33 | ||
|
|
8ccf46c8f8 | ||
|
|
07c7a0fae5 | ||
|
|
a90a681cf0 | ||
|
|
26a923c498 | ||
|
|
b688fc815b | ||
|
|
93421abdc4 | ||
|
|
a5e13009f9 | ||
|
|
626b5a2c04 | ||
|
|
26a550c596 | ||
|
|
6dd816e346 | ||
|
|
3ff093f576 | ||
|
|
f18be6e8a9 | ||
|
|
882b37c6e6 | ||
|
|
52f92330ad | ||
|
|
1428e9754c | ||
|
|
c41d11989b | ||
|
|
17b33c192e | ||
|
|
9837f4fed9 | ||
|
|
30104338d2 | ||
|
|
86d4897a37 | ||
|
|
4180a770e5 | ||
|
|
ef70cad34d | ||
|
|
2b7f4b47d7 | ||
|
|
885ab7b379 | ||
|
|
90e7149517 | ||
|
|
fd2d34f780 | ||
|
|
317d5647e0 | ||
|
|
f1d2585279 | ||
|
|
a558c16485 | ||
|
|
b52d474819 | ||
|
|
18f00ec1af | ||
|
|
b6f548283b | ||
|
|
b9e8a45509 | ||
|
|
3b3145358d | ||
|
|
45c67d0a3c | ||
|
|
ea74a4cbf8 | ||
|
|
f4fb0a5717 | ||
|
|
606478435e | ||
|
|
181c1d637e | ||
|
|
d00a966984 | ||
|
|
cd4eb06cb1 | ||
|
|
d60645365a | ||
|
|
bfe8baf39a | ||
|
|
749021ea85 | ||
|
|
aba07c3e6f | ||
|
|
4e2bd5f783 | ||
|
|
b18f2bc447 | ||
|
|
69c735a908 | ||
|
|
bb74a84eff | ||
|
|
49604ba432 | ||
|
|
72c133cc57 | ||
|
|
cef4c2ee30 | ||
|
|
816e494b27 | ||
|
|
33df2cad52 | ||
|
|
e27a626b8d | ||
|
|
75977a7e15 | ||
|
|
1d91c8fbf4 | ||
|
|
1eb877e2aa | ||
|
|
205f266441 | ||
|
|
6469336ba9 | ||
|
|
c9b9a4f5d1 | ||
|
|
f08ca1c08a | ||
|
|
c6baebd514 | ||
|
|
34c3c5beca | ||
|
|
34eb3cd8fc | ||
|
|
3d939480c9 | ||
|
|
6400ce1e75 | ||
|
|
6cda26fad7 | ||
|
|
3deeb74ca7 | ||
|
|
476e441057 | ||
|
|
965c1c581a | ||
|
|
99e1a8d6d8 | ||
|
|
0558dd6f6d | ||
|
|
86e2336370 | ||
|
|
6f7d18ffba | ||
|
|
6cb68bf6fc | ||
|
|
bccc085029 | ||
|
|
2d95c71b85 | ||
|
|
effafd9c2e | ||
|
|
aebdcedda9 | ||
|
|
74b05c83d0 | ||
|
|
94e359f6d7 | ||
|
|
101ff24a1f | ||
|
|
9fe1fc7aa4 | ||
|
|
1dbf46e32f | ||
|
|
6e3401f9dc | ||
|
|
6944b77ec7 | ||
|
|
3f2e33f6e0 | ||
|
|
2b9846d1d2 | ||
|
|
b6a7f40175 | ||
|
|
aa4b7f46e5 | ||
|
|
c79d51858a | ||
|
|
e430a42dbe | ||
|
|
5dec129088 | ||
|
|
f299271b1b | ||
|
|
49787215d2 | ||
|
|
5e34538c55 | ||
|
|
862c3f6fb5 | ||
|
|
2edbeae02c | ||
|
|
02ada0c61d | ||
|
|
98e95c36dc | ||
|
|
a1b1c37f36 | ||
|
|
45905f8f34 | ||
|
|
6d83b32815 | ||
|
|
a366cf81d9 | ||
|
|
3a8bd7eea7 | ||
|
|
a46c3321db | ||
|
|
eedb123b3a | ||
|
|
1c7dc33852 | ||
|
|
49a3c31de8 | ||
|
|
82e318c17f | ||
|
|
920e3cf8ec | ||
|
|
2fe7de6cd7 | ||
|
|
f3ac99e587 | ||
|
|
fe7a867f38 | ||
|
|
7d70d85d23 | ||
|
|
5a70e9d6b8 | ||
|
|
65b00ae12f | ||
|
|
c36ff6bd49 | ||
|
|
269b4d7a7a | ||
|
|
3c499de5fa | ||
|
|
c2ce086027 | ||
|
|
fc16da92d1 | ||
|
|
49bd0ad1ae | ||
|
|
6462fe8db4 | ||
|
|
b118230218 | ||
|
|
cc5f2bdd7f | ||
|
|
ab309c0db2 | ||
|
|
6346754ad4 | ||
|
|
3c72bbad39 | ||
|
|
9ae973b87a | ||
|
|
49f79f0897 | ||
|
|
21373fc67f | ||
|
|
fd3552d8ef | ||
|
|
2f7059fbbf | ||
|
|
73429565c5 | ||
|
|
a7d7039317 | ||
|
|
e389cce5a2 | ||
|
|
debae7f89e | ||
|
|
172466a2c7 | ||
|
|
da81caa72c | ||
|
|
91610bbbaa | ||
|
|
9562f909e1 | ||
|
|
878f9bfbb7 | ||
|
|
5df248b5ea | ||
|
|
d019ded7bf | ||
|
|
354a7000b4 | ||
|
|
5271f64402 | ||
|
|
5665578b2e | ||
|
|
b0b88bb3a2 | ||
|
|
8d324a2afb | ||
|
|
433927fcfe | ||
|
|
fb78596c85 | ||
|
|
546eb9129a | ||
|
|
24c978cf4d | ||
|
|
74a8f4d32d | ||
|
|
b4151c61d6 | ||
|
|
0b94a82579 | ||
|
|
fce196599a | ||
|
|
c43af71572 | ||
|
|
cf6ebadb08 | ||
|
|
d027207bed | ||
|
|
109c8075c2 | ||
|
|
2fcda46801 | ||
|
|
12fdd1c724 | ||
|
|
1a52a0b846 | ||
|
|
26e3c7c2b7 | ||
|
|
b88f7a0bfd | ||
|
|
c1635a0f55 | ||
|
|
986551f146 | ||
|
|
2796975526 | ||
|
|
0cbb377385 | ||
|
|
ee3dce4cc5 | ||
|
|
854ae0d6f2 | ||
|
|
a20bb21af8 | ||
|
|
2fbc78fdc0 | ||
|
|
32d9824eb9 | ||
|
|
98ead6af1f | ||
|
|
595f42c84d | ||
|
|
025d0cec08 | ||
|
|
6ba594800f | ||
|
|
16caa48a2a | ||
|
|
cdcacaef28 | ||
|
|
90b6e514ef | ||
|
|
c10debea35 | ||
|
|
d6f98948e0 | ||
|
|
7770e86743 | ||
|
|
1f7d2f91c9 | ||
|
|
05231d3fba | ||
|
|
d534b80532 | ||
|
|
acd39df54b | ||
|
|
657aa5a637 | ||
|
|
1386357d23 | ||
|
|
0749847fa3 | ||
|
|
10c9318e96 | ||
|
|
136be6cdc6 | ||
|
|
28b111dae8 | ||
|
|
98a2cdf22b | ||
|
|
50a8c57023 | ||
|
|
569b3d1426 | ||
|
|
bd9dbf30f8 | ||
|
|
281948ee17 | ||
|
|
c47a116403 | ||
|
|
e70b501990 | ||
|
|
8c54a3b036 | ||
|
|
3f0ff88107 | ||
|
|
071ff09d8e | ||
|
|
45c9b9e86d | ||
|
|
ae7798f997 | ||
|
|
94224d41a8 | ||
|
|
324b7b47fd | ||
|
|
b1db158b09 | ||
|
|
331edcbcc5 | ||
|
|
c1563e3862 | ||
|
|
922c372cb8 | ||
|
|
4fdf892b6a | ||
|
|
c2361461f7 | ||
|
|
1e9e4a10e7 | ||
|
|
45d51d204c | ||
|
|
9f3603d4ae | ||
|
|
4ab29424b7 | ||
|
|
6c04cecbba | ||
|
|
1337eb355b | ||
|
|
584abffa74 | ||
|
|
6fd8d711e9 | ||
|
|
506bc849ee | ||
|
|
ff639063b0 | ||
|
|
4c00ec42f9 | ||
|
|
4e5ad952f8 | ||
|
|
9f86ae2c54 | ||
|
|
4683cbc299 | ||
|
|
4eebd091bf | ||
|
|
70e06201d3 | ||
|
|
2c953cd5cb | ||
|
|
8f1167ff34 | ||
|
|
f5f3b1bb20 | ||
|
|
98438de1c4 | ||
|
|
61f89e7f7d | ||
|
|
30d78ebc5a | ||
|
|
f23c0fa0e0 | ||
|
|
e2e23c29da | ||
|
|
2e9c021e68 | ||
|
|
b7491fd05a | ||
|
|
003f1e1cb9 | ||
|
|
fc30fd4b3c | ||
|
|
377cd2ae6f | ||
|
|
a50be64266 | ||
|
|
9a81b11317 | ||
|
|
2f22765133 | ||
|
|
98f260b047 | ||
|
|
42211fb432 | ||
|
|
52974c5e77 | ||
|
|
41f4dd0781 | ||
|
|
1d18ca9761 | ||
|
|
b310ec7a25 | ||
|
|
85524afc0d | ||
|
|
6b85fbbafc | ||
|
|
12971e1ca4 | ||
|
|
997bd2b3d7 | ||
|
|
db4cff0309 | ||
|
|
8709a5e354 | ||
|
|
e9587de1ea | ||
|
|
bd12dc663e | ||
|
|
24667b7163 | ||
|
|
108cea1e5d | ||
|
|
b2e08e3963 | ||
|
|
b1f199c4d7 | ||
|
|
ee4b09059e | ||
|
|
6b9b71ac8a | ||
|
|
9d98853091 | ||
|
|
7f90896dbc | ||
|
|
8c4575c0d1 | ||
|
|
232cbb9b67 | ||
|
|
d1ef4c4e33 | ||
|
|
ff298476f1 | ||
|
|
59ea442f5c | ||
|
|
124bab4761 | ||
|
|
526187f635 | ||
|
|
99095e2f43 | ||
|
|
8db0b40d74 | ||
|
|
71687491f0 | ||
|
|
c998b69bd8 | ||
|
|
d207a1fb44 | ||
|
|
19844e2831 | ||
|
|
13061fda9a | ||
|
|
5f403232ab | ||
|
|
6a9ed0fc9d | ||
|
|
12e1ec7dcd | ||
|
|
efa223d9db | ||
|
|
f726c6b4f6 | ||
|
|
eee7a8846c | ||
|
|
b09e468f04 | ||
|
|
686543b90a | ||
|
|
c89d3a780e | ||
|
|
fa72bb28ff | ||
|
|
3c96452513 | ||
|
|
cc511a6b43 | ||
|
|
1678404081 | ||
|
|
b2ea89a1a0 | ||
|
|
ffd6cb588c | ||
|
|
1263eaa31f | ||
|
|
cb2189a2e3 | ||
|
|
235eea1e80 | ||
|
|
0f85eaba8e | ||
|
|
bb680703e7 | ||
|
|
cfd778e7d5 | ||
|
|
5080d62a8b | ||
|
|
5883362f62 | ||
|
|
3b774ca9a1 | ||
|
|
6c1ff4e4ca | ||
|
|
6c93016460 | ||
|
|
8e4d6fe752 | ||
|
|
daaa51e090 | ||
|
|
81d88aed13 | ||
|
|
5d555c9eb9 | ||
|
|
7dc5103833 | ||
|
|
0020181b3e | ||
|
|
579daf8852 | ||
|
|
a3eae4d2ca | ||
|
|
7163e0e144 | ||
|
|
8050e0dab4 | ||
|
|
a759e11066 | ||
|
|
7071741315 | ||
|
|
52b5cbc300 | ||
|
|
490f033cfd | ||
|
|
c4333ae87a | ||
|
|
1cd4f1827f | ||
|
|
b3bf566db4 | ||
|
|
5dad3d0e65 | ||
|
|
4e2a9ef4d1 | ||
|
|
d6b926899d | ||
|
|
367c73afb9 | ||
|
|
25faca7f3f | ||
|
|
7f0500ec04 | ||
|
|
f03fcffc80 | ||
|
|
b299d34a6c | ||
|
|
7cba6bae2a | ||
|
|
1679e2984f | ||
|
|
0df189b83a | ||
|
|
1c5723ddc7 | ||
|
|
0699a8a25b | ||
|
|
7d321a4f03 | ||
|
|
99f4d4b5c0 | ||
|
|
32b46b1ef3 | ||
|
|
0e80c874d6 | ||
|
|
89ed8735e7 | ||
|
|
2d4c27b12c | ||
|
|
f575317de8 | ||
|
|
6e8abe2a19 | ||
|
|
20f0ef77ae | ||
|
|
983574ce8c | ||
|
|
cff6aa2906 | ||
|
|
c8adc97cef | ||
|
|
2dac87f56e | ||
|
|
f95cc754e7 | ||
|
|
270a02a372 | ||
|
|
23719e955e | ||
|
|
36b7c828e9 | ||
|
|
8539747e29 | ||
|
|
b88ba552f4 | ||
|
|
9ea1900f30 | ||
|
|
7366467831 | ||
|
|
5fdb56c243 | ||
|
|
e64df373a7 | ||
|
|
1c4be78060 | ||
|
|
fd3de5c6c1 | ||
|
|
c9a160556a | ||
|
|
54a85d8e99 | ||
|
|
ace0cbe578 | ||
|
|
ced31bbf0e | ||
|
|
c1d0a43a29 | ||
|
|
0ac753e298 | ||
|
|
fa3f17fa32 | ||
|
|
a35319f0ad | ||
|
|
33dec754d5 | ||
|
|
2029b21e0b | ||
|
|
97b1a2a336 | ||
|
|
f4ca635c1a | ||
|
|
d5de40218c | ||
|
|
c9db5d5429 | ||
|
|
4bc3882d58 | ||
|
|
ecbc967644 | ||
|
|
fb34595170 | ||
|
|
59ae79837d | ||
|
|
31ba02e74b | ||
|
|
c93f58bd27 | ||
|
|
5a993f200c | ||
|
|
6b65d055fd | ||
|
|
79944017d2 | ||
|
|
6e04d0e766 | ||
|
|
b73c9a90ae | ||
|
|
538595cff9 | ||
|
|
92da189360 | ||
|
|
09268f9b97 | ||
|
|
22ff5d2e16 | ||
|
|
0b025fe40e | ||
|
|
2c485e9806 | ||
|
|
de16715ae4 | ||
|
|
ce24b83db6 | ||
|
|
d010065da1 | ||
|
|
cb4a84b439 | ||
|
|
a3052ae429 | ||
|
|
59502d629a | ||
|
|
6979a6b165 | ||
|
|
46d2a6e50a | ||
|
|
0c8370477c | ||
|
|
9aed2c41d4 | ||
|
|
a0f7be4f2f | ||
|
|
9aa5606ed6 | ||
|
|
c8670435d3 | ||
|
|
84fed10878 | ||
|
|
766dd36d76 | ||
|
|
659c193769 | ||
|
|
57da0ad4de | ||
|
|
8dc4dbf009 | ||
|
|
7a62acb828 | ||
|
|
2f8523a0a1 | ||
|
|
a455577b21 | ||
|
|
4881485fa6 | ||
|
|
55ab3cc3f2 | ||
|
|
156fcbebed | ||
|
|
f0d8b51c44 | ||
|
|
0d8d68d954 | ||
|
|
f191a9b449 | ||
|
|
1b965ac6ad | ||
|
|
7b474f6b9e | ||
|
|
033b904982 | ||
|
|
b0b9d60438 | ||
|
|
d8304f3eec | ||
|
|
e661169b60 | ||
|
|
95f39f11b7 | ||
|
|
642905043c | ||
|
|
3943b4869f | ||
|
|
ee8687af15 | ||
|
|
bf723e7348 | ||
|
|
15baefaa57 | ||
|
|
f0f858447e | ||
|
|
83bbf9c73d | ||
|
|
7ee5888795 | ||
|
|
1349df9c0c | ||
|
|
4259be6f57 | ||
|
|
24665eb532 | ||
|
|
7faa0db866 | ||
|
|
56a86b48bf | ||
|
|
53328ca50d | ||
|
|
a0abcc3131 | ||
|
|
8083aece66 | ||
|
|
6893b986ca | ||
|
|
599c8ff200 | ||
|
|
cd6578ba25 | ||
|
|
12835ab0e5 | ||
|
|
0913c6188f | ||
|
|
9ef868f7ed | ||
|
|
20b613d1bb | ||
|
|
12d7d6c730 | ||
|
|
07e837b21c | ||
|
|
825bd722d1 | ||
|
|
51f5fd9088 | ||
|
|
d9ed716b01 | ||
|
|
c1ebedcc1f | ||
|
|
7c707bf3b0 | ||
|
|
59f70af0c3 | ||
|
|
a15f677727 | ||
|
|
2cfbf235f7 | ||
|
|
3446749573 | ||
|
|
b709417e23 | ||
|
|
d79353942b | ||
|
|
0ceec3d45c | ||
|
|
fc287f9038 | ||
|
|
d071827a1e | ||
|
|
6b114f55eb | ||
|
|
3235f11870 | ||
|
|
6fad3f0b76 | ||
|
|
278f528deb | ||
|
|
b6da5e60c9 | ||
|
|
355488f4d0 | ||
|
|
61a2473a7c | ||
|
|
5e6fc5f721 | ||
|
|
b17835bf58 | ||
|
|
977c5c56e1 | ||
|
|
06ec9d6b1d | ||
|
|
a7c35f2813 | ||
|
|
0c01238a39 | ||
|
|
46d730f2fa | ||
|
|
c7de55007b | ||
|
|
4caa37c10d | ||
|
|
6feef21841 | ||
|
|
99003f9716 | ||
|
|
9180e6abb6 | ||
|
|
8b5135b008 | ||
|
|
306a91d1e3 | ||
|
|
4fa3eff976 | ||
|
|
db89462e00 | ||
|
|
f26c057128 | ||
|
|
7ca3974bf8 | ||
|
|
58bef9344a | ||
|
|
12a513606b | ||
|
|
89da4674cd | ||
|
|
3a540a5730 | ||
|
|
bd3491a3e4 | ||
|
|
8322a2aeb8 | ||
|
|
7ac21c3c33 | ||
|
|
5cd6149981 | ||
|
|
947cca5199 | ||
|
|
4108538dbb | ||
|
|
3beb909a09 | ||
|
|
4b874410f9 | ||
|
|
f237cfb4ab | ||
|
|
aa37b353f7 | ||
|
|
3d134abe94 | ||
|
|
218bf2183a | ||
|
|
a7cc2cbee3 | ||
|
|
c79f485270 | ||
|
|
8424471ff6 | ||
|
|
46262d18f3 | ||
|
|
6554f7bc8f | ||
|
|
010e2f356e | ||
|
|
d184819b61 | ||
|
|
f53d359d38 | ||
|
|
c6bdcffb89 | ||
|
|
369251099f | ||
|
|
65c3df2ff4 | ||
|
|
44a84b5046 | ||
|
|
0ea8269a78 | ||
|
|
d6b00df333 | ||
|
|
8e165a7101 | ||
|
|
8deb52842b | ||
|
|
a82e69865f | ||
|
|
6e4a1cf2a2 | ||
|
|
194691038c | ||
|
|
28762b3a73 | ||
|
|
a493fbfea8 | ||
|
|
6304608114 | ||
|
|
e4c7484181 | ||
|
|
11d5c269d1 | ||
|
|
8741d683ba | ||
|
|
affe197f26 | ||
|
|
40dd3d1a6a | ||
|
|
9b397053ca | ||
|
|
0380eef924 | ||
|
|
475e896cf0 | ||
|
|
256dbca4ba | ||
|
|
541e627702 | ||
|
|
96f0a9e559 | ||
|
|
dea182c38a | ||
|
|
77856252cf | ||
|
|
cff74b6e49 | ||
|
|
94464da004 | ||
|
|
055fa4bb97 | ||
|
|
d8ac9b081b | ||
|
|
b74b5a442a | ||
|
|
6f074d620e | ||
|
|
e2f8ca9c00 | ||
|
|
d1cb3e100b | ||
|
|
1e939c9b31 | ||
|
|
9bf846978d | ||
|
|
e897a3a30a | ||
|
|
a238a5ac2f | ||
|
|
b2c02559f1 | ||
|
|
f95cf73ca4 | ||
|
|
018067032e | ||
|
|
1d07879c11 | ||
|
|
eb1a39dbc2 | ||
|
|
982569532c | ||
|
|
554d1991df | ||
|
|
feb6f60db2 | ||
|
|
96101c0c73 | ||
|
|
fe297dbc55 | ||
|
|
e3580a45e5 | ||
|
|
df359f6a9a | ||
|
|
7bdad5e701 | ||
|
|
39f8bf194a | ||
|
|
36295e63ed | ||
|
|
257cda56e3 | ||
|
|
7587090bcd | ||
|
|
eba27d94b6 | ||
|
|
3289c0deca | ||
|
|
d1de0d39e8 | ||
|
|
25a2141bb3 | ||
|
|
b9c164aeb6 | ||
|
|
5afdbdf8fc | ||
|
|
14da682578 | ||
|
|
7008262762 | ||
|
|
7c2e1ef238 | ||
|
|
8eb3ee44b4 | ||
|
|
0b5c48282a | ||
|
|
45a757c0e0 | ||
|
|
b480eabb3c | ||
|
|
7477f5c47a | ||
|
|
4364522bf1 | ||
|
|
4d3fa2d225 | ||
|
|
cde2fa716c | ||
|
|
43063e1744 | ||
|
|
5b83b47845 | ||
|
|
a815e7d2e5 | ||
|
|
9674cd8ae6 | ||
|
|
d9334a8e73 | ||
|
|
ba9d9c1e58 | ||
|
|
73e615cb64 | ||
|
|
93079bb576 | ||
|
|
52bc48f73a | ||
|
|
46755465f9 | ||
|
|
04906a80ff | ||
|
|
193b2b73c9 | ||
|
|
b32310b073 | ||
|
|
5a27dbe955 | ||
|
|
2cbcebf45a | ||
|
|
644d55a0c0 | ||
|
|
1bb20026a2 | ||
|
|
932dd4f191 | ||
|
|
ecadc1fc2d | ||
|
|
79ccf47655 | ||
|
|
6ef0a4ad6b | ||
|
|
88142d2a92 | ||
|
|
f5c3302329 | ||
|
|
bb02f8e162 | ||
|
|
d57dd664a2 | ||
|
|
74ec6678bb | ||
|
|
b9e88cd99d | ||
|
|
32afade4f9 | ||
|
|
66de2db912 | ||
|
|
b08ddc73b4 | ||
|
|
dccd872122 | ||
|
|
1e568011f8 | ||
|
|
0a38da8867 | ||
|
|
5e739f8293 | ||
|
|
6f64245d10 | ||
|
|
2318eca54e | ||
|
|
ed8a04c84e | ||
|
|
d48ca65a1e | ||
|
|
285fcd55a9 | ||
|
|
05b713ab18 | ||
|
|
293b15f67a | ||
|
|
83aec35f2f | ||
|
|
910ef72205 | ||
|
|
550a37b379 | ||
|
|
2b396c14e3 | ||
|
|
36724a3abd | ||
|
|
4459aa4ef3 | ||
|
|
64a6986d01 | ||
|
|
a301ecb2ca | ||
|
|
f16429e30f | ||
|
|
46b9bf6ccb | ||
|
|
17c8f11194 | ||
|
|
4d1a7ea55a | ||
|
|
babe06a944 | ||
|
|
9e31d53bdd | ||
|
|
ea24841939 | ||
|
|
bf9f557e42 | ||
|
|
106e748a9b | ||
|
|
3d669fffed | ||
|
|
94fff62f9b | ||
|
|
324253f23a | ||
|
|
e9a2e44a91 | ||
|
|
7b4f046001 | ||
|
|
1a3560a19f | ||
|
|
3b525300e0 | ||
|
|
08ff49d3f5 | ||
|
|
f5c4271a07 | ||
|
|
74d41b43b6 | ||
|
|
3408bba303 | ||
|
|
5e00e998a8 | ||
|
|
3847f0cda0 | ||
|
|
1ebcd017bd | ||
|
|
9013a7e312 | ||
|
|
afefbd953f | ||
|
|
535b141b23 | ||
|
|
b21e44b65f | ||
|
|
b42be379e3 | ||
|
|
b2f0a3bea3 | ||
|
|
df3745d185 | ||
|
|
f85bb3f9b2 | ||
|
|
566f3e3c32 | ||
|
|
58eb91fb23 | ||
|
|
36267717ac | ||
|
|
5e323f1f8f | ||
|
|
c0efc9d5c1 | ||
|
|
61188ab8e2 | ||
|
|
ae209d37c1 | ||
|
|
a5b0efba75 | ||
|
|
5adb64e40e | ||
|
|
41fea1028d | ||
|
|
5a90a4331b | ||
|
|
881f3b1a34 | ||
|
|
8be5865b76 | ||
|
|
685d1cb41a | ||
|
|
14fe1e3ecb | ||
|
|
636f4d7037 | ||
|
|
c92ad2f601 | ||
|
|
8a876fd67d | ||
|
|
d39cd60863 | ||
|
|
602ca92476 | ||
|
|
f413035295 | ||
|
|
bfd3fb4dad | ||
|
|
733e19a6f6 | ||
|
|
85b552e1a6 | ||
|
|
068730c53c | ||
|
|
c9d84c7ce3 | ||
|
|
d558aea7de | ||
|
|
e211eec693 | ||
|
|
6b1277d3e1 | ||
|
|
35bf38be70 | ||
|
|
555c00406e | ||
|
|
e67012654a | ||
|
|
ecdb1d17cd | ||
|
|
a5578b5e60 | ||
|
|
fb4641878f | ||
|
|
7d6f30f51f | ||
|
|
9869b645b1 | ||
|
|
037b85bd66 | ||
|
|
ba784b8b35 | ||
|
|
eae760db3f | ||
|
|
4b5993cad6 | ||
|
|
6af62aa093 | ||
|
|
61e8de4270 | ||
|
|
27dce4e427 | ||
|
|
8b53fb1c7b | ||
|
|
6c1661dc3c | ||
|
|
3662b45121 | ||
|
|
437253179e | ||
|
|
d85f4edbbb | ||
|
|
96c9ccaaa0 | ||
|
|
3203ed7a19 | ||
|
|
517cbb6cee | ||
|
|
3bc373dbec | ||
|
|
273fe10296 | ||
|
|
2a10a28cc8 | ||
|
|
f74645e1a4 | ||
|
|
15ec02dcae | ||
|
|
e75c654a1a | ||
|
|
29b1eca1fd | ||
|
|
e2d036e710 | ||
|
|
094f0abe4a | ||
|
|
8ab2003dae | ||
|
|
b1b0c5648c | ||
|
|
36e5779d94 | ||
|
|
846d8246a3 | ||
|
|
26a04b22d3 | ||
|
|
53aef452cc | ||
|
|
f5f55ffc2e | ||
|
|
3ef5a64b94 | ||
|
|
c28db932a4 | ||
|
|
0792ce1415 | ||
|
|
f2c2501fa5 | ||
|
|
b1f930a995 | ||
|
|
eca23a2691 | ||
|
|
de60b616cd | ||
|
|
6e6a0240a7 | ||
|
|
2e2360a9fc | ||
|
|
8011f4e2e8 | ||
|
|
970037682c | ||
|
|
42b58efc5c | ||
|
|
b20163d762 | ||
|
|
e0a56cbb14 | ||
|
|
8dae851ea3 | ||
|
|
03ba9595c0 | ||
|
|
4b07b4826a | ||
|
|
80d9b33c59 | ||
|
|
3be3c14912 | ||
|
|
4171f85c73 | ||
|
|
5a78ebcf7c | ||
|
|
9294a7130f | ||
|
|
9ce3abc2b4 | ||
|
|
327594a598 | ||
|
|
31cccdec03 | ||
|
|
29a6172120 | ||
|
|
06486e06dd | ||
|
|
ada55ab461 | ||
|
|
a9e4de65a9 | ||
|
|
2867262e4d | ||
|
|
779c09186c | ||
|
|
6a0408b942 | ||
|
|
43e094c345 | ||
|
|
7d30b19421 | ||
|
|
e9e8c35178 | ||
|
|
646e7ce001 | ||
|
|
21da34187e | ||
|
|
d2fa4f1cd9 | ||
|
|
72a6fc14f9 | ||
|
|
97c2cb1f86 | ||
|
|
73fd091b80 | ||
|
|
5bb4052f3d | ||
|
|
36e7e3cb7f | ||
|
|
25b73187f5 | ||
|
|
75a9600089 | ||
|
|
c9216b32ab | ||
|
|
70e374ef11 | ||
|
|
24840c539c | ||
|
|
461be76821 | ||
|
|
28a7184cc4 | ||
|
|
13f1453276 | ||
|
|
ff25c36ede | ||
|
|
092aa45fd9 | ||
|
|
64acf80024 | ||
|
|
099beb8438 | ||
|
|
8bee13c3f9 | ||
|
|
65cd8aba79 | ||
|
|
37856e5608 | ||
|
|
5b1deaa08a | ||
|
|
a41bced1d7 | ||
|
|
8c207a1dff | ||
|
|
99e1974a69 | ||
|
|
132bf288ac | ||
|
|
0a9f9848b7 | ||
|
|
11da55abf7 | ||
|
|
e751c0f23e | ||
|
|
f4b5beec01 | ||
|
|
9e2b8093fb | ||
|
|
7b2f66000c | ||
|
|
6e7593dee2 | ||
|
|
2098b2b09d | ||
|
|
aa1781577b | ||
|
|
409d293faa | ||
|
|
8181fe71cf | ||
|
|
d06009684e | ||
|
|
55236ce34a | ||
|
|
b89f06b7f0 | ||
|
|
a01d1f770f | ||
|
|
f1fdb61195 | ||
|
|
39fea86f13 | ||
|
|
ce5d1d0e5a | ||
|
|
7ac29366ae | ||
|
|
9d0f6a9cea | ||
|
|
e0403412e7 | ||
|
|
bb67aa77f5 | ||
|
|
e948a7a869 | ||
|
|
3fc56df111 | ||
|
|
4860581525 | ||
|
|
008890a688 | ||
|
|
178f56455e | ||
|
|
8376e35022 | ||
|
|
0b8206aecb | ||
|
|
203505bc25 | ||
|
|
c5ac85ae5b | ||
|
|
c6552ddc75 | ||
|
|
7972b19bdd | ||
|
|
33918a2433 | ||
|
|
9e7f6d98fd | ||
|
|
ceda20510f | ||
|
|
82f3250b5b | ||
|
|
c37e087332 | ||
|
|
f282c58edc | ||
|
|
45e208f1d8 | ||
|
|
132c597d1e | ||
|
|
85f486e6cd | ||
|
|
2c96773679 | ||
|
|
a8f9fd7a56 | ||
|
|
aae4ad4da8 | ||
|
|
014d7b4d39 | ||
|
|
5912339813 | ||
|
|
20bfa0e3bd | ||
|
|
41ebe0fa64 | ||
|
|
66cf610cc0 | ||
|
|
62ec391523 | ||
|
|
7927da2085 | ||
|
|
d45dea4bff | ||
|
|
816e274dfc | ||
|
|
1a20a6a4a8 | ||
|
|
910049b0ea | ||
|
|
173a83aafa | ||
|
|
b7093f88b1 | ||
|
|
2e66bcf254 | ||
|
|
95208294b0 | ||
|
|
a4bf2234cd | ||
|
|
e527e7233f | ||
|
|
afe959835d | ||
|
|
3b2b05064b | ||
|
|
1e94fe983f | ||
|
|
274ac4e0e1 | ||
|
|
1ad4443e3b | ||
|
|
031bf0ee45 | ||
|
|
0efe80b06d | ||
|
|
3fb7c6dd21 | ||
|
|
c7e4ac82ca | ||
|
|
d5e29598d3 | ||
|
|
fca7782634 | ||
|
|
42b23a9faa | ||
|
|
06011d01d6 | ||
|
|
4bf4e65df8 | ||
|
|
45e62ed43e | ||
|
|
d9156349e1 | ||
|
|
983b0b2f1d | ||
|
|
368c31e48d | ||
|
|
a552c14cbd | ||
|
|
3f5787ceb1 | ||
|
|
e4ec2363d0 | ||
|
|
0fa926de26 | ||
|
|
84b71910ee | ||
|
|
371217832b | ||
|
|
b9b7db2a4e | ||
|
|
e3f68e1a3f | ||
|
|
3c4746aea0 | ||
|
|
afb514b472 | ||
|
|
e14dc22bba | ||
|
|
463dd4464f | ||
|
|
6b7c12c23c | ||
|
|
63b95bb68e | ||
|
|
ea41389842 | ||
|
|
429cb8d6e9 | ||
|
|
55faae767a | ||
|
|
94b8f298ae | ||
|
|
1ef3f9f979 | ||
|
|
e88313eb84 | ||
|
|
03cd9bc968 | ||
|
|
4d7c56e537 | ||
|
|
4769668ff9 | ||
|
|
8ca36a695b | ||
|
|
222b3869dd | ||
|
|
56af2d3840 | ||
|
|
053a976d37 | ||
|
|
1695470089 | ||
|
|
d4b5f799cb | ||
|
|
40fb5b62a9 | ||
|
|
1fec5925eb | ||
|
|
23d4b342b9 | ||
|
|
2c716cd74c | ||
|
|
6c23514d84 | ||
|
|
456ddcfe68 | ||
|
|
2a283a4f83 | ||
|
|
b29533259b | ||
|
|
0f8aa08994 | ||
|
|
fb522c5016 | ||
|
|
7272f2564d | ||
|
|
07a2f3a899 | ||
|
|
399e6a6f2d | ||
|
|
46d1cf1696 | ||
|
|
5be9bb58df | ||
|
|
cab4fc36ab | ||
|
|
53d3e05bb4 | ||
|
|
0c4de74c84 | ||
|
|
2a4feea40f | ||
|
|
faa90e1aa5 | ||
|
|
1aa96c3490 | ||
|
|
0e80955e96 | ||
|
|
7771592cf2 | ||
|
|
6e9e8fc40d | ||
|
|
67448cea65 | ||
|
|
770b06a9ee | ||
|
|
1d50b2c4a1 | ||
|
|
453202df8f | ||
|
|
a78c052d86 | ||
|
|
e6b0a551e8 | ||
|
|
38775245d2 | ||
|
|
fcb699ffb9 | ||
|
|
e889d8fc20 | ||
|
|
bf1c95500a | ||
|
|
f9d00c9d23 | ||
|
|
8fd7ff6475 | ||
|
|
02c80ae7d2 | ||
|
|
e5d3d02b17 | ||
|
|
78186d8d46 | ||
|
|
c899653621 | ||
|
|
a91fbcdd68 | ||
|
|
74b315e285 | ||
|
|
60eeafe7a9 | ||
|
|
ee2c21e712 | ||
|
|
e678ad3546 | ||
|
|
c47c780b48 | ||
|
|
88074ac9b3 | ||
|
|
59cb0c4b65 | ||
|
|
bf0bd193eb | ||
|
|
7661375925 | ||
|
|
308fb45985 | ||
|
|
f4aa6ddb46 | ||
|
|
f6cb73fdd6 | ||
|
|
3c100b0ae2 | ||
|
|
168e42b5fa | ||
|
|
2ce6bd5ed1 | ||
|
|
7bd5d6465a | ||
|
|
65387d4c61 | ||
|
|
6084e8e94f | ||
|
|
3ccc5c9fc6 | ||
|
|
d07859f604 | ||
|
|
88a322a03b | ||
|
|
0e02bbc2fb | ||
|
|
affd9217e2 | ||
|
|
7b4a349e3f | ||
|
|
7dc5ab5035 | ||
|
|
3e8965f9a9 | ||
|
|
23a27bf544 |
No files matched your search
@@ -0,0 +1,118 @@
|
||||
name: Bug Report
|
||||
description: Report a reproducible problem in Netcatty
|
||||
title: "[Bug] "
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report a bug. Incomplete reports may be closed automatically.
|
||||
Please search [existing issues](https://github.com/binaricat/Netcatty/issues) first.
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Operating system
|
||||
options:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Netcatty version
|
||||
description: Find it in Settings > Application, or on the [latest release](https://github.com/binaricat/Netcatty/releases/latest) page.
|
||||
placeholder: "e.g. 1.2.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: install_source
|
||||
attributes:
|
||||
label: How did you install Netcatty?
|
||||
options:
|
||||
- GitHub Release (.dmg / .exe / .AppImage / .deb / .rpm / .pacman)
|
||||
- Homebrew
|
||||
- Built from source (npm run dev / pack)
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Affected area
|
||||
multiple: true
|
||||
options:
|
||||
- SSH connection / terminal
|
||||
- SFTP / file browser
|
||||
- Host vault / keychain
|
||||
- Port forwarding
|
||||
- Snippets
|
||||
- AI assistant
|
||||
- Settings / sync
|
||||
- UI / layout
|
||||
- Crash / app won't start
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: reproducibility
|
||||
attributes:
|
||||
label: Can you reproduce it?
|
||||
options:
|
||||
- Always (100%)
|
||||
- Often (>50%)
|
||||
- Sometimes
|
||||
- Once / not sure
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Numbered steps so we can follow exactly.
|
||||
placeholder: |
|
||||
1. Open Netcatty and connect to host X
|
||||
2. Click SFTP tab
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs / screenshots
|
||||
description: |
|
||||
Optional but helpful. Crash logs: Settings > System > Crash Logs > Open folder.
|
||||
For SSH errors, include redacted connection details (no passwords / private keys).
|
||||
placeholder: Paste relevant log lines or attach screenshots.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I searched existing issues and did not find a duplicate
|
||||
required: true
|
||||
- label: I removed passwords, private keys, and other secrets from this report
|
||||
required: true
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & general help
|
||||
url: https://github.com/binaricat/Netcatty/discussions
|
||||
about: Not sure if it is a bug? Ask in Discussions first.
|
||||
- name: Latest release
|
||||
url: https://github.com/binaricat/Netcatty/releases/latest
|
||||
about: Check your Netcatty version before reporting.
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Feature Request
|
||||
description: Suggest an improvement or new capability
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Describe the problem you are trying to solve and the change you want.
|
||||
Vague requests like "make it better" may be closed.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem / pain point
|
||||
description: What is hard, missing, or frustrating today?
|
||||
placeholder: When I manage 50+ hosts, I cannot ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: What would you like Netcatty to do?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Other tools, workarounds, or designs you thought about.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Related area
|
||||
multiple: true
|
||||
options:
|
||||
- SSH / terminal
|
||||
- SFTP
|
||||
- Host vault / keychain
|
||||
- Port forwarding
|
||||
- Snippets
|
||||
- AI assistant
|
||||
- Settings / sync
|
||||
- UI / UX
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: priority
|
||||
attributes:
|
||||
label: How important is this to you?
|
||||
options:
|
||||
- Nice to have
|
||||
- Would improve my daily workflow
|
||||
- Blocking / critical for my use case
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I searched existing issues and discussions for similar requests
|
||||
required: true
|
||||
@@ -0,0 +1,40 @@
|
||||
## Summary
|
||||
|
||||
<!-- Briefly describe what this PR does and why. -->
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Refactor / code cleanup
|
||||
- [ ] Documentation update
|
||||
- [ ] Build / CI change
|
||||
- [ ] Other (please describe):
|
||||
|
||||
## Related Issue (optional)
|
||||
|
||||
N/A
|
||||
|
||||
<!-- If this PR has a related issue, replace N/A with "Closes #123" or "Related to #123". -->
|
||||
|
||||
## Changes Made
|
||||
|
||||
<!-- List the key changes in this PR. -->
|
||||
|
||||
## Screenshots / Demo
|
||||
|
||||
<!-- If this PR affects UI or behavior, include screenshots or a short description. -->
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] I have tested these changes locally (`npm run dev`)
|
||||
- [ ] Linting passes (`npm run lint`)
|
||||
- [ ] Tests pass (`npm test`)
|
||||
- [ ] Generated capability tool specs are updated when applicable (`npm run generate:capability-tools`)
|
||||
- [ ] No new console errors or warnings, if this affects app behavior
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the existing project style
|
||||
- [ ] I have added or updated relevant documentation
|
||||
- [ ] I have not introduced any breaking changes (or I have described them above)
|
||||
@@ -0,0 +1,45 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We actively maintain the latest stable release of Netcatty. Security fixes are applied to the current release only.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| Latest | :white_check_mark: |
|
||||
| Older | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in Netcatty, **please do not open a public GitHub Issue**.
|
||||
|
||||
Instead, report it privately via one of the following methods:
|
||||
|
||||
- **GitHub Private Vulnerability Reporting**: Use the [Security tab](https://github.com/binaricat/Netcatty/security/advisories/new) to submit a private advisory.
|
||||
- **Email**: Send details to support@netcatty.com.
|
||||
- **GitHub Issues** (for non-sensitive security concerns only): https://github.com/binaricat/Netcatty/issues
|
||||
|
||||
Please include the following details in your report:
|
||||
|
||||
- A description of the vulnerability
|
||||
- Steps to reproduce the issue
|
||||
- The potential impact
|
||||
- Any suggested mitigation or fix
|
||||
|
||||
## Response Timeline
|
||||
|
||||
We aim to:
|
||||
|
||||
- Acknowledge receipt within **3 business days**
|
||||
- Provide a status update within **7 business days**
|
||||
- Release a patch as soon as possible depending on severity
|
||||
|
||||
## Scope
|
||||
|
||||
This policy applies to the Netcatty desktop application (`netcatty.app`) and its source code in this repository.
|
||||
|
||||
Vulnerabilities in third-party dependencies should be reported directly to the respective upstream projects.
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
We follow responsible disclosure. Once a fix is available, we will publish a security advisory and credit the reporter (unless they prefer to remain anonymous).
|
||||
@@ -50,6 +50,7 @@ const baseUrl = `https://github.com/${repo}/releases/download/${tag}`;
|
||||
// - AppImage: x64 -> x86_64, arm64 -> arm64
|
||||
// - deb: x64 -> amd64, arm64 -> arm64
|
||||
// - rpm: x64 -> x86_64, arm64 -> aarch64
|
||||
// - pacman: x64 -> x64, arm64 -> aarch64
|
||||
const files = {
|
||||
mac: {
|
||||
arm64: `Netcatty-${version}-mac-arm64.dmg`,
|
||||
@@ -70,6 +71,10 @@ const files = {
|
||||
rpm: {
|
||||
x64: `Netcatty-${version}-linux-x86_64.rpm`,
|
||||
arm64: `Netcatty-${version}-linux-aarch64.rpm`
|
||||
},
|
||||
pacman: {
|
||||
x64: `Netcatty-${version}-linux-x64.pacman`,
|
||||
arm64: `Netcatty-${version}-linux-aarch64.pacman`
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -88,7 +93,9 @@ const badges = {
|
||||
deb_x64: `[](${baseUrl}/${files.linux.deb.x64})`,
|
||||
deb_arm64: `[](${baseUrl}/${files.linux.deb.arm64})`,
|
||||
rpm_x64: `[](${baseUrl}/${files.linux.rpm.x64})`,
|
||||
rpm_arm64: `[](${baseUrl}/${files.linux.rpm.arm64})`
|
||||
rpm_arm64: `[](${baseUrl}/${files.linux.rpm.arm64})`,
|
||||
pacman_x64: `[](${baseUrl}/${files.linux.pacman.x64})`,
|
||||
pacman_arm64: `[](${baseUrl}/${files.linux.pacman.arm64})`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,7 +106,7 @@ const content = `
|
||||
| :--- | :--- |
|
||||
| **Windows** | ${badges.win.setup_x64} |
|
||||
| **macOS** | ${badges.mac.apple_silicon} ${badges.mac.intel} |
|
||||
| **Linux** | ${badges.linux.appimage_x64} ${badges.linux.deb_x64} ${badges.linux.rpm_x64} <br> ${badges.linux.appimage_arm64} ${badges.linux.deb_arm64} ${badges.linux.rpm_arm64} |
|
||||
| **Linux** | ${badges.linux.appimage_x64} ${badges.linux.deb_x64} ${badges.linux.rpm_x64} ${badges.linux.pacman_x64} <br> ${badges.linux.appimage_arm64} ${badges.linux.deb_arm64} ${badges.linux.rpm_arm64} ${badges.linux.pacman_arm64} |
|
||||
`;
|
||||
|
||||
fs.writeFileSync('release_notes.md', content);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
function usage() {
|
||||
console.error('Usage: node .github/scripts/update-nix-release.js --artifacts <dir> --version <semver>');
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let index = 2; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === '--artifacts' || arg === '--version') {
|
||||
args[arg.slice(2)] = argv[index + 1];
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
usage();
|
||||
process.exit(2);
|
||||
}
|
||||
if (!args.artifacts || !args.version) {
|
||||
usage();
|
||||
process.exit(2);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function sriSha256(filePath) {
|
||||
const digest = crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('base64');
|
||||
return `sha256-${digest}`;
|
||||
}
|
||||
|
||||
function findArtifact(artifactsDir, fileName) {
|
||||
const filePath = path.join(artifactsDir, fileName);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`Missing release artifact: ${filePath}`);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function renderReleaseNix({ version, x64Hash, arm64Hash }) {
|
||||
return `{
|
||||
version = "${version}";
|
||||
|
||||
sources = {
|
||||
x86_64-linux = {
|
||||
appImageArch = "x86_64";
|
||||
hash = "${x64Hash}";
|
||||
};
|
||||
aarch64-linux = {
|
||||
appImageArch = "arm64";
|
||||
hash = "${arm64Hash}";
|
||||
};
|
||||
};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv);
|
||||
const version = args.version.replace(/^v/, '');
|
||||
|
||||
if (!/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-.+)?$/.test(version)) {
|
||||
throw new Error(`Expected semver version, got: ${args.version}`);
|
||||
}
|
||||
|
||||
const x64AppImage = findArtifact(args.artifacts, `Netcatty-${version}-linux-x86_64.AppImage`);
|
||||
const arm64AppImage = findArtifact(args.artifacts, `Netcatty-${version}-linux-arm64.AppImage`);
|
||||
|
||||
const releaseNix = renderReleaseNix({
|
||||
version,
|
||||
x64Hash: sriSha256(x64AppImage),
|
||||
arm64Hash: sriSha256(arm64AppImage),
|
||||
});
|
||||
|
||||
fs.writeFileSync('nix/release.nix', releaseNix);
|
||||
console.log(`Updated nix/release.nix for Netcatty ${version}`);
|
||||
@@ -0,0 +1,222 @@
|
||||
name: build-et-binaries
|
||||
|
||||
# Trigger philosophy (mirrors build-mosh-binaries.yml):
|
||||
# - Pushes that touch the et build pipeline + PRs run the matrix so we can
|
||||
# validate workflow / script changes without tagging. Artifacts upload as
|
||||
# workflow artifacts only; *no* release.
|
||||
# - Manual `workflow_dispatch` with `release_tag` publishes the binaries +
|
||||
# SHA256SUMS to the dedicated binary repository
|
||||
# (`binaricat/Netcatty-et-bin` by default).
|
||||
#
|
||||
# `paths` keeps unrelated commits (UI, bridges, etc) from rebuilding the et
|
||||
# binaries on every push.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
et_ref:
|
||||
description: "EternalTerminal git ref (tag/branch/commit) — see https://github.com/MisterTea/EternalTerminal"
|
||||
type: string
|
||||
default: "et-v6.2.10"
|
||||
release_tag:
|
||||
description: "Optional release tag to attach binaries to (e.g. et-bin-6.2.10-1). Empty = artifacts only."
|
||||
type: string
|
||||
default: ""
|
||||
release_repo:
|
||||
description: "Repository that stores et binary releases."
|
||||
type: string
|
||||
default: "binaricat/Netcatty-et-bin"
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths:
|
||||
- ".github/workflows/build-et-binaries.yml"
|
||||
- "electron-builder.config.cjs"
|
||||
- "package.json"
|
||||
- "scripts/build-et/**"
|
||||
- "scripts/fetch-et-binaries.cjs"
|
||||
- "scripts/et-extra-resources.cjs"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/build-et-binaries.yml"
|
||||
- "electron-builder.config.cjs"
|
||||
- "package.json"
|
||||
- "scripts/build-et/**"
|
||||
- "scripts/fetch-et-binaries.cjs"
|
||||
- "scripts/et-extra-resources.cjs"
|
||||
|
||||
concurrency:
|
||||
group: build-et-binaries-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
ET_REF: ${{ inputs.et_ref || 'et-v6.2.10' }}
|
||||
|
||||
jobs:
|
||||
# ------------------------------------------------------------------
|
||||
# Linux x64 (manylinux2014 / glibc 2.17, broad distro compatibility).
|
||||
# ------------------------------------------------------------------
|
||||
build-linux-x64:
|
||||
name: build-linux-x64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build et (linux-x64)
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e ET_REF="${ET_REF}" \
|
||||
-e OUT_DIR=/work/out \
|
||||
-e ARCH=x64 \
|
||||
-v "${GITHUB_WORKSPACE}:/work" \
|
||||
-w /work \
|
||||
quay.io/pypa/manylinux2014_x86_64 \
|
||||
bash scripts/build-et/build-linux.sh
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: et-linux-x64
|
||||
path: out/
|
||||
|
||||
build-linux-arm64:
|
||||
name: build-linux-arm64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build et (linux-arm64)
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e ET_REF="${ET_REF}" \
|
||||
-e OUT_DIR=/work/out \
|
||||
-e ARCH=arm64 \
|
||||
-v "${GITHUB_WORKSPACE}:/work" \
|
||||
-w /work \
|
||||
quay.io/pypa/manylinux2014_aarch64 \
|
||||
bash scripts/build-et/build-linux.sh
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: et-linux-arm64
|
||||
path: out/
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# macOS universal2 (arm64 + x86_64 lipo). Min deployment target macOS 11.
|
||||
# ------------------------------------------------------------------
|
||||
build-macos-universal:
|
||||
name: build-macos-universal
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build et (darwin-universal)
|
||||
env:
|
||||
ET_REF: ${{ env.ET_REF }}
|
||||
OUT_DIR: ${{ github.workspace }}/out
|
||||
MACOSX_DEPLOYMENT_TARGET: "11.0"
|
||||
run: bash scripts/build-et/build-macos.sh
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: et-darwin-universal
|
||||
path: out/
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Windows x64 — static MSVC build (no DLL bundle).
|
||||
# ------------------------------------------------------------------
|
||||
build-windows-x64:
|
||||
name: build-windows-x64
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install ninja
|
||||
run: choco install -y ninja
|
||||
- name: Set up MSVC developer command prompt
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
with:
|
||||
arch: x64
|
||||
- name: Build et (win32-x64)
|
||||
env:
|
||||
ET_REF: ${{ env.ET_REF }}
|
||||
OUT_DIR: ${{ github.workspace }}\out
|
||||
shell: pwsh
|
||||
run: pwsh -File scripts/build-et/build-windows.ps1
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: et-win32-x64
|
||||
path: out/
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Windows arm64 — intentionally not built until a tested client exists.
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Aggregate + optional release to the dedicated binary repository.
|
||||
# ------------------------------------------------------------------
|
||||
release:
|
||||
name: release
|
||||
needs:
|
||||
- build-linux-x64
|
||||
- build-linux-arm64
|
||||
- build-macos-universal
|
||||
- build-windows-x64
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Stage release files
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p release
|
||||
for d in artifacts/*/; do
|
||||
find "$d" -maxdepth 1 -type f -exec cp {} release/ \;
|
||||
done
|
||||
(cd release && find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%P\n' | sort | xargs sha256sum > SHA256SUMS)
|
||||
ls -la release
|
||||
cat release/SHA256SUMS
|
||||
- name: Determine tag
|
||||
id: tag
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
tag="${RELEASE_TAG}"
|
||||
if [[ ! "$tag" =~ ^et-bin-[A-Za-z0-9._-]+$ ]]; then
|
||||
echo "Invalid et binary release tag: $tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'name=%s\n' "$tag" >> "$GITHUB_OUTPUT"
|
||||
- name: Create / update release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ET_BIN_RELEASE_TOKEN }}
|
||||
RELEASE_REPO: ${{ inputs.release_repo }}
|
||||
RELEASE_TAG: ${{ steps.tag.outputs.name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z "${GH_TOKEN:-}" ]]; then
|
||||
echo "::error::ET_BIN_RELEASE_TOKEN is required to publish into ${RELEASE_REPO}."
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
printf '%s\n' 'Pre-built EternalTerminal `et` client binaries consumed by `scripts/fetch-et-binaries.cjs` during `npm run pack`.'
|
||||
printf 'Built from `MisterTea/EternalTerminal` upstream ref `%s`.\n\n' "${ET_REF}"
|
||||
printf 'Source workflow: %s/%s/actions/runs/%s\n' "${GITHUB_SERVER_URL}" "${GITHUB_REPOSITORY}" "${GITHUB_RUN_ID}"
|
||||
printf 'Source commit: `%s`\n\n' "${GITHUB_SHA}"
|
||||
printf '%s\n' 'All artifacts are Apache-2.0; see `resources/et/README.md` for source provenance.'
|
||||
} > release-notes.md
|
||||
if gh release view "${RELEASE_TAG}" --repo "${RELEASE_REPO}" >/dev/null 2>&1; then
|
||||
gh release edit "${RELEASE_TAG}" \
|
||||
--repo "${RELEASE_REPO}" \
|
||||
--title "${RELEASE_TAG}" \
|
||||
--notes-file release-notes.md
|
||||
gh release upload "${RELEASE_TAG}" release/* \
|
||||
--repo "${RELEASE_REPO}" \
|
||||
--clobber
|
||||
else
|
||||
gh release create "${RELEASE_TAG}" release/* \
|
||||
--repo "${RELEASE_REPO}" \
|
||||
--title "${RELEASE_TAG}" \
|
||||
--notes-file release-notes.md
|
||||
fi
|
||||
@@ -129,17 +129,18 @@ jobs:
|
||||
path: out/
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Windows x64 pinned standalone client.
|
||||
# Do not compile this in CI: the upstream Cygwin build can clear the
|
||||
# terminal and never render output on Windows. Ship the SHA256-pinned
|
||||
# FluentTerminal standalone binary verified by fetch-windows.sh.
|
||||
# Windows x64 pinned runtime bundle.
|
||||
# Do not compile this in CI: rebuilding the upstream Cygwin client here
|
||||
# previously regressed Windows startup. Ship the SHA256-pinned Netcatty
|
||||
# bundle with DLLs + terminfo, plus the FluentTerminal standalone fallback,
|
||||
# verified by fetch-windows.sh.
|
||||
# ------------------------------------------------------------------
|
||||
fetch-windows-x64:
|
||||
name: fetch-windows-x64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Fetch pinned mosh-client.exe (win32-x64)
|
||||
- name: Fetch pinned mosh-client bundle (win32-x64)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export OUT_DIR="${GITHUB_WORKSPACE}/out"
|
||||
@@ -212,7 +213,7 @@ jobs:
|
||||
{
|
||||
printf '%s\n' 'Pre-built `mosh-client` binaries consumed by `scripts/fetch-mosh-binaries.cjs` during `npm run pack`.'
|
||||
printf 'Linux/macOS artifacts are built from `mobile-shell/mosh` upstream ref `%s`.\n' "${MOSH_REF}"
|
||||
printf '%s\n\n' 'Windows x64 is the SHA256-pinned FluentTerminal standalone `mosh-client.exe` fallback.'
|
||||
printf '%s\n\n' 'Windows x64 is the SHA256-pinned Netcatty runtime bundle, with the FluentTerminal standalone `mosh-client.exe` kept as a fallback asset.'
|
||||
printf 'Source workflow: %s/%s/actions/runs/%s\n' "${GITHUB_SERVER_URL}" "${GITHUB_REPOSITORY}" "${GITHUB_RUN_ID}"
|
||||
printf 'Source commit: `%s`\n\n' "${GITHUB_SHA}"
|
||||
printf '%s\n' 'All artifacts are GPL-3.0; see `resources/mosh/README.md` for source provenance.'
|
||||
|
||||
+325
-32
@@ -29,6 +29,10 @@ on:
|
||||
description: "Release tag containing bundled mosh-client binaries"
|
||||
type: string
|
||||
default: ""
|
||||
et_bin_release:
|
||||
description: "Release tag containing bundled et (EternalTerminal) binaries"
|
||||
type: string
|
||||
default: ""
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
@@ -54,6 +58,8 @@ permissions:
|
||||
env:
|
||||
MOSH_BIN_RELEASE: ${{ github.event.inputs.mosh_bin_release || vars.MOSH_BIN_RELEASE || '' }}
|
||||
BUNDLE_MOSH: ${{ (startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release))) || (github.event_name == 'workflow_dispatch' && inputs.mosh_bin_release != '') }}
|
||||
ET_BIN_RELEASE: ${{ github.event.inputs.et_bin_release || vars.ET_BIN_RELEASE || '' }}
|
||||
BUNDLE_ET: ${{ (startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release))) || (github.event_name == 'workflow_dispatch' && inputs.et_bin_release != '') }}
|
||||
STRICT_VERSION_REF_RE: '^refs/tags/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[A-Za-z][0-9A-Za-z-]*|[0-9A-Za-z][0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[A-Za-z][0-9A-Za-z-]*|[0-9A-Za-z][0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?$'
|
||||
|
||||
jobs:
|
||||
@@ -191,9 +197,38 @@ jobs:
|
||||
fi
|
||||
echo "mosh_bin_release=${release}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
resolve-et:
|
||||
name: resolve bundled et-client
|
||||
needs: dedupe
|
||||
if: |
|
||||
needs.dedupe.outputs.skip_heavy_ci != 'true'
|
||||
&& (
|
||||
(startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release)))
|
||||
|| (github.event_name == 'workflow_dispatch' && inputs.et_bin_release != '')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
et_bin_release: ${{ steps.resolve.outputs.et_bin_release }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve bundled et-client release
|
||||
id: resolve
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
node scripts/resolve-et-bin-release.cjs
|
||||
release="$(grep '^ET_BIN_RELEASE=' "$GITHUB_ENV" | tail -n 1 | cut -d= -f2-)"
|
||||
if [[ -z "$release" ]]; then
|
||||
echo "::error::ET_BIN_RELEASE was not resolved."
|
||||
exit 1
|
||||
fi
|
||||
echo "et_bin_release=${release}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && format('deduped build-{0}', matrix.name) || format('build-{0}', matrix.name) }}
|
||||
needs: [dedupe, resolve-mosh]
|
||||
needs: [dedupe, resolve-mosh, resolve-et]
|
||||
if: |
|
||||
always()
|
||||
&& needs.dedupe.result == 'success'
|
||||
@@ -214,6 +249,7 @@ jobs:
|
||||
pack_script: pack:win-x64
|
||||
env:
|
||||
MOSH_BIN_RELEASE: ${{ needs.resolve-mosh.outputs.mosh_bin_release }}
|
||||
ET_BIN_RELEASE: ${{ needs.resolve-et.outputs.et_bin_release }}
|
||||
VITE_SYNC_GITHUB_CLIENT_ID: ${{ secrets.VITE_SYNC_GITHUB_CLIENT_ID }}
|
||||
VITE_SYNC_GOOGLE_CLIENT_ID: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_ID }}
|
||||
VITE_SYNC_GOOGLE_CLIENT_SECRET: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_SECRET }}
|
||||
@@ -230,6 +266,17 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Validate bundled et-client release
|
||||
if: env.BUNDLE_ET == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
RESOLVE_ET_RESULT: ${{ needs.resolve-et.result }}
|
||||
run: |
|
||||
if [[ "$RESOLVE_ET_RESULT" != "success" || -z "$ET_BIN_RELEASE" ]]; then
|
||||
echo "::error::Bundled et-client release was not resolved for this package build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -242,21 +289,6 @@ jobs:
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
|
||||
- name: Install cross-platform native binaries
|
||||
shell: bash
|
||||
run: |
|
||||
# npm ci only installs optional deps for the host platform.
|
||||
# macOS packages still cover both arm64 and x64, so we need
|
||||
# codex-acp for both architectures there.
|
||||
# Platform-specific codex-acp packages declare cpu/os constraints,
|
||||
# so --force is needed to install the non-host-arch binary.
|
||||
CODEX_VER=$(node -e "console.log(require('./node_modules/@zed-industries/codex-acp/package.json').version)")
|
||||
if [[ "${{ matrix.name }}" == "macos" ]]; then
|
||||
npm install "@zed-industries/codex-acp-darwin-x64@${CODEX_VER}" "@zed-industries/codex-acp-darwin-arm64@${CODEX_VER}" --no-save --force
|
||||
elif [[ "${{ matrix.name }}" == "windows" ]]; then
|
||||
npm install "@zed-industries/codex-acp-win32-x64@${CODEX_VER}" --no-save --force
|
||||
fi
|
||||
|
||||
- name: Fetch bundled mosh-client
|
||||
if: env.BUNDLE_MOSH == 'true'
|
||||
shell: bash
|
||||
@@ -267,6 +299,16 @@ jobs:
|
||||
npm run fetch:mosh -- --platform=win32 --arch=x64
|
||||
fi
|
||||
|
||||
- name: Fetch bundled et-client
|
||||
if: env.BUNDLE_ET == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ matrix.name }}" == "macos" ]]; then
|
||||
npm run fetch:et -- --platform=darwin --arch=universal
|
||||
elif [[ "${{ matrix.name }}" == "windows" ]]; then
|
||||
npm run fetch:et -- --platform=win32 --arch=x64
|
||||
fi
|
||||
|
||||
- name: Set version
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -306,28 +348,40 @@ jobs:
|
||||
release/*.AppImage
|
||||
release/*.deb
|
||||
release/*.rpm
|
||||
release/*.pacman
|
||||
release/*.tar.gz
|
||||
release/*.yml
|
||||
release/*.blockmap
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Linux x64 — pin to ubuntu-22.04 for broader glibc compatibility.
|
||||
# ubuntu-latest (24.04) links native modules against glibc 2.39 which
|
||||
# can cause dlopen failures on some distros. 22.04 uses glibc 2.35,
|
||||
# compatible with most current Linux distributions including Arch.
|
||||
# See #264.
|
||||
# Dedicated job for Linux x64 — builds inside AlmaLinux 8 (GLIBC 2.28)
|
||||
# so native modules (node-pty, serialport) stay compatible with RHEL 8 /
|
||||
# CentOS 8 / UOS / Deepin. Ubuntu 22.04 links against GLIBC 2.32–2.35,
|
||||
# which crashes on those older hosts (see #2062).
|
||||
#
|
||||
# We previously used debian:buster for the same glibc floor, but Buster's
|
||||
# g++ 8 cannot compile Electron 42 natives (`-std=gnu++20`). AlmaLinux 8
|
||||
# keeps glibc 2.28 while gcc-toolset-13 provides a real C++20 toolchain.
|
||||
# Link natives with -static-libstdc++ so the result does not require a
|
||||
# newer system libstdc++ on RHEL 8 derivatives.
|
||||
# Key: GLIBC < 2.34 also avoids the libpthread-merge symbol requirement.
|
||||
build-linux-x64:
|
||||
name: ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-x64' || 'build-linux-x64' }}
|
||||
needs: [dedupe, resolve-mosh]
|
||||
needs: [dedupe, resolve-mosh, resolve-et]
|
||||
if: |
|
||||
always()
|
||||
&& needs.dedupe.result == 'success'
|
||||
&& needs.dedupe.outputs.skip_heavy_ci != 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: almalinux:8
|
||||
env:
|
||||
MOSH_BIN_RELEASE: ${{ needs.resolve-mosh.outputs.mosh_bin_release }}
|
||||
ET_BIN_RELEASE: ${{ needs.resolve-et.outputs.et_bin_release }}
|
||||
npm_config_arch: x64
|
||||
npm_config_target_arch: x64
|
||||
# Keep rebuilt natives portable to stock RHEL 8 libstdc++.
|
||||
LDFLAGS: "-static-libstdc++ -static-libgcc"
|
||||
VITE_SYNC_GITHUB_CLIENT_ID: ${{ secrets.VITE_SYNC_GITHUB_CLIENT_ID }}
|
||||
VITE_SYNC_GOOGLE_CLIENT_ID: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_ID }}
|
||||
VITE_SYNC_GOOGLE_CLIENT_SECRET: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_SECRET }}
|
||||
@@ -344,17 +398,170 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Validate bundled et-client release
|
||||
if: env.BUNDLE_ET == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
RESOLVE_ET_RESULT: ${{ needs.resolve-et.result }}
|
||||
run: |
|
||||
if [[ "$RESOLVE_ET_RESULT" != "success" || -z "$ET_BIN_RELEASE" ]]; then
|
||||
echo "::error::Bundled et-client release was not resolved for this package build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
dnf -y install epel-release
|
||||
dnf -y install \
|
||||
curl ca-certificates \
|
||||
gcc-toolset-13-gcc gcc-toolset-13-gcc-c++ make \
|
||||
python3.11 python3.11-pip \
|
||||
git file rpm-build binutils cpio elfutils \
|
||||
libarchive gzip bzip2 xz zstd tar which findutils \
|
||||
dpkg fakeroot \
|
||||
fuse fuse-libs gtk3 nss libXScrnSaver libXtst alsa-lib \
|
||||
at-spi2-atk libdrm mesa-libgbm libX11 libX11-xcb libxcb \
|
||||
mesa-libGL pango cairo
|
||||
# gcc-toolset-13: C++20 for Electron 42 natives; still links glibc 2.28.
|
||||
GCC_TOOLSET_ROOT="/opt/rh/gcc-toolset-13/root"
|
||||
echo "PATH=${GCC_TOOLSET_ROOT}/usr/bin:${PATH}" >> "${GITHUB_ENV}"
|
||||
echo "CC=${GCC_TOOLSET_ROOT}/usr/bin/gcc" >> "${GITHUB_ENV}"
|
||||
echo "CXX=${GCC_TOOLSET_ROOT}/usr/bin/g++" >> "${GITHUB_ENV}"
|
||||
echo "DEVTOOLSET_ROOTPATH=${GCC_TOOLSET_ROOT}" >> "${GITHUB_ENV}"
|
||||
export PATH="${GCC_TOOLSET_ROOT}/usr/bin:${PATH}"
|
||||
gcc --version
|
||||
g++ --version
|
||||
# AlmaLinux 8's libarchive package is often library-only in minimal
|
||||
# images (no /usr/bin/bsdtar). electron-builder pacman packaging needs
|
||||
# bsdtar; pull a Debian tools package that runs on glibc 2.28.
|
||||
# Prefer Buster (published as +deb10u1 on archive.debian.org).
|
||||
if [[ ! -x /usr/bin/bsdtar ]]; then
|
||||
echo "bsdtar missing; installing CLI from Debian libarchive-tools"
|
||||
mkdir -p /tmp/bsdtar-deb
|
||||
BSDTAR_DEB_URLS=(
|
||||
"http://archive.debian.org/debian/pool/main/liba/libarchive/libarchive-tools_3.3.3-4+deb10u1_amd64.deb"
|
||||
"http://archive.debian.org/debian/pool/main/liba/libarchive/libarchive-tools_3.2.2-2+deb9u2_amd64.deb"
|
||||
)
|
||||
BSDTAR_DEB_OK=0
|
||||
for url in "${BSDTAR_DEB_URLS[@]}"; do
|
||||
echo "trying ${url}"
|
||||
if curl -fsSL -o /tmp/libarchive-tools.deb "${url}"; then
|
||||
BSDTAR_DEB_OK=1
|
||||
break
|
||||
fi
|
||||
echo "download failed for ${url}"
|
||||
done
|
||||
if [[ "${BSDTAR_DEB_OK}" -ne 1 ]]; then
|
||||
echo "::error::Could not download Debian libarchive-tools for bsdtar"
|
||||
exit 1
|
||||
fi
|
||||
dpkg-deb -x /tmp/libarchive-tools.deb /tmp/bsdtar-deb
|
||||
install -m 0755 /tmp/bsdtar-deb/usr/bin/bsdtar /usr/local/bin/bsdtar-bin
|
||||
# Prefer system libarchive.so if present; fall back to the deb's.
|
||||
if [[ ! -e /usr/lib64/libarchive.so.13 && -d /tmp/bsdtar-deb/usr/lib/x86_64-linux-gnu ]]; then
|
||||
mkdir -p /usr/local/lib/bsdtar-libs
|
||||
cp -a /tmp/bsdtar-deb/usr/lib/x86_64-linux-gnu/libarchive.so* /usr/local/lib/bsdtar-libs/ 2>/dev/null || true
|
||||
fi
|
||||
printf '%s\n' \
|
||||
'#!/bin/bash' \
|
||||
'unset LD_LIBRARY_PATH' \
|
||||
'if [[ -d /usr/local/lib/bsdtar-libs ]]; then' \
|
||||
' export LD_LIBRARY_PATH=/usr/local/lib/bsdtar-libs' \
|
||||
'fi' \
|
||||
'exec /usr/local/bin/bsdtar-bin "$@"' \
|
||||
> /usr/local/bin/bsdtar
|
||||
chmod +x /usr/local/bin/bsdtar
|
||||
fi
|
||||
# Some fpm paths still look for xzmt even when payload compression
|
||||
# is gzip; provide a multi-thread xz shim.
|
||||
printf '%s\n' '#!/bin/sh' 'exec /usr/bin/xz -T0 "$@"' > /usr/local/bin/xzmt
|
||||
chmod +x /usr/local/bin/xzmt
|
||||
# electron-builder's portable fpm injects its own LD_LIBRARY_PATH.
|
||||
# System tools (rpmbuild, bsdtar, ...) then exit 127 under that path.
|
||||
# Wrap every packaging helper so child processes see a clean env.
|
||||
# Use printf (not a bare heredoc): unindented heredoc body breaks GHA YAML.
|
||||
wrap_clean_env() {
|
||||
local name="$1"
|
||||
local real=""
|
||||
if [[ -x "/usr/bin/${name}" ]]; then
|
||||
real="/usr/bin/${name}"
|
||||
elif [[ -x "/usr/local/bin/${name}-bin" ]]; then
|
||||
real="/usr/local/bin/${name}-bin"
|
||||
elif command -v "${name}" >/dev/null 2>&1; then
|
||||
real="$(command -v "${name}")"
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
# Don't wrap our own wrapper recursively.
|
||||
if [[ "${real}" == /usr/local/bin/* && "${name}" != "bsdtar" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "${name}" == "bsdtar" && -x /usr/local/bin/bsdtar-bin ]]; then
|
||||
real="/usr/local/bin/bsdtar-bin"
|
||||
fi
|
||||
printf '%s\n' \
|
||||
'#!/bin/bash' \
|
||||
'unset LD_LIBRARY_PATH' \
|
||||
"exec ${real} \"\$@\"" \
|
||||
> "/usr/local/bin/${name}"
|
||||
chmod +x "/usr/local/bin/${name}"
|
||||
}
|
||||
for cmd in rpmbuild bsdtar bsdcpio tar gzip xz strip cpio; do
|
||||
wrap_clean_env "${cmd}"
|
||||
done
|
||||
# Re-apply bsdtar wrapper with optional lib path for the deb-extracted binary.
|
||||
if [[ -x /usr/local/bin/bsdtar-bin ]]; then
|
||||
printf '%s\n' \
|
||||
'#!/bin/bash' \
|
||||
'unset LD_LIBRARY_PATH' \
|
||||
'if [[ -d /usr/local/lib/bsdtar-libs ]]; then' \
|
||||
' export LD_LIBRARY_PATH=/usr/local/lib/bsdtar-libs' \
|
||||
'fi' \
|
||||
'exec /usr/local/bin/bsdtar-bin "$@"' \
|
||||
> /usr/local/bin/bsdtar
|
||||
chmod +x /usr/local/bin/bsdtar
|
||||
fi
|
||||
# Fail early with diagnostics if packaging tools are missing.
|
||||
for tool in rpmbuild bsdtar strip gzip xz xzmt; do
|
||||
if ! command -v "${tool}" >/dev/null 2>&1; then
|
||||
echo "::error::Required packaging tool not found: ${tool}"
|
||||
ls -la /usr/bin/bsd* /usr/local/bin/bsd* 2>/dev/null || true
|
||||
rpm -ql libarchive 2>/dev/null | head -n 40 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "found ${tool}: $(command -v "${tool}")"
|
||||
done
|
||||
rpmbuild --version
|
||||
bsdtar --version | head -n 1
|
||||
# Official Node 22 linux-x64 tarball (same major as other package jobs).
|
||||
NODE_VERSION="$(curl -fsSL https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt \
|
||||
| awk '/node-v[0-9.]+-linux-x64\.tar\.xz$/{print $2; exit}' \
|
||||
| sed -E 's/^node-(v[0-9.]+)-linux-x64\.tar\.xz$/\1/')"
|
||||
if [[ -z "${NODE_VERSION}" ]]; then
|
||||
echo "::error::Could not resolve latest Node 22 linux-x64 tarball version."
|
||||
exit 1
|
||||
fi
|
||||
echo "Installing Node ${NODE_VERSION} from nodejs.org"
|
||||
curl -fsSL "https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.xz" \
|
||||
| tar -xJ -C /usr/local --strip-components=1
|
||||
node -v
|
||||
npm -v
|
||||
# node-gyp 12 needs Python >=3.8; AlmaLinux 8 default is older.
|
||||
alternatives --set python3 /usr/bin/python3.11 || ln -sfn /usr/bin/python3.11 /usr/local/bin/python3
|
||||
python3 --version
|
||||
echo "PYTHON=/usr/bin/python3.11" >> "${GITHUB_ENV}"
|
||||
echo "npm_config_python=/usr/bin/python3.11" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
run: |
|
||||
python3 --version
|
||||
g++ --version
|
||||
npm ci
|
||||
|
||||
- name: Set version
|
||||
shell: bash
|
||||
@@ -379,11 +586,31 @@ jobs:
|
||||
if: env.BUNDLE_MOSH == 'true'
|
||||
run: npm run fetch:mosh -- --platform=linux --arch=x64
|
||||
|
||||
- name: Fetch bundled et-client
|
||||
if: env.BUNDLE_ET == 'true'
|
||||
run: npm run fetch:et -- --platform=linux --arch=x64
|
||||
|
||||
- name: Build package
|
||||
env:
|
||||
npm_config_arch: x64
|
||||
ELECTRON_BUILDER_PUBLISH: "never"
|
||||
run: npm run pack:linux-x64
|
||||
# Natives are already rebuilt. Prefer system packaging tools so
|
||||
# rpmbuild brp scripts do not pick up a toolset-only binary, and so
|
||||
# /usr/local/bin/rpmbuild (clean-env wrapper) wins over /usr/bin.
|
||||
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
run: |
|
||||
set -euo pipefail
|
||||
command -v node
|
||||
command -v rpmbuild
|
||||
command -v bsdtar
|
||||
command -v gzip
|
||||
command -v xzmt
|
||||
for cmd in rpmbuild bsdtar; do
|
||||
if ! head -n 2 "$(command -v "${cmd}")" | grep -q 'unset LD_LIBRARY_PATH'; then
|
||||
echo "::warning::${cmd} on PATH is not the clean-env wrapper; packaging may fail with exit 127"
|
||||
fi
|
||||
done
|
||||
npm run pack:linux-x64
|
||||
|
||||
- name: Verify packaged node-pty Linux runtime
|
||||
run: bash scripts/ensure-node-pty-linux.sh verify x64
|
||||
@@ -391,6 +618,9 @@ jobs:
|
||||
- name: Verify packaged deb artifact
|
||||
run: bash scripts/verify-linux-deb-artifact.sh amd64
|
||||
|
||||
- name: Verify packaged rpm artifact
|
||||
run: bash scripts/verify-linux-rpm-artifact.sh x86_64
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -399,6 +629,7 @@ jobs:
|
||||
release/*.AppImage
|
||||
release/*.deb
|
||||
release/*.rpm
|
||||
release/*.pacman
|
||||
release/*.yml
|
||||
release/*.blockmap
|
||||
if-no-files-found: ignore
|
||||
@@ -408,7 +639,7 @@ jobs:
|
||||
# Key: GLIBC < 2.34 avoids the libpthread-merge symbol requirement.
|
||||
build-linux-arm64:
|
||||
name: ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-arm64' || 'build-linux-arm64' }}
|
||||
needs: [dedupe, resolve-mosh]
|
||||
needs: [dedupe, resolve-mosh, resolve-et]
|
||||
if: |
|
||||
always()
|
||||
&& needs.dedupe.result == 'success'
|
||||
@@ -418,6 +649,7 @@ jobs:
|
||||
image: debian:bullseye
|
||||
env:
|
||||
MOSH_BIN_RELEASE: ${{ needs.resolve-mosh.outputs.mosh_bin_release }}
|
||||
ET_BIN_RELEASE: ${{ needs.resolve-et.outputs.et_bin_release }}
|
||||
npm_config_arch: arm64
|
||||
npm_config_target_arch: arm64
|
||||
VITE_SYNC_GITHUB_CLIENT_ID: ${{ secrets.VITE_SYNC_GITHUB_CLIENT_ID }}
|
||||
@@ -436,10 +668,22 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Validate bundled et-client release
|
||||
if: env.BUNDLE_ET == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
RESOLVE_ET_RESULT: ${{ needs.resolve-et.result }}
|
||||
run: |
|
||||
if [[ "$RESOLVE_ET_RESULT" != "success" || -z "$ET_BIN_RELEASE" ]]; then
|
||||
echo "::error::Bundled et-client release was not resolved for this package build."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y curl build-essential python3 git libfuse2 file rpm \
|
||||
libarchive-tools \
|
||||
libglib2.0-0 libgtk-3-0 libnss3 libxss1 libxtst6 libasound2 \
|
||||
libatk-bridge2.0-0 libdrm2 libgbm1 libx11-xcb1 libxcb-dri3-0
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
@@ -474,6 +718,10 @@ jobs:
|
||||
if: env.BUNDLE_MOSH == 'true'
|
||||
run: npm run fetch:mosh -- --platform=linux --arch=arm64
|
||||
|
||||
- name: Fetch bundled et-client
|
||||
if: env.BUNDLE_ET == 'true'
|
||||
run: npm run fetch:et -- --platform=linux --arch=arm64
|
||||
|
||||
- name: Build package
|
||||
env:
|
||||
npm_config_arch: arm64
|
||||
@@ -486,6 +734,9 @@ jobs:
|
||||
- name: Verify packaged deb artifact
|
||||
run: bash scripts/verify-linux-deb-artifact.sh arm64
|
||||
|
||||
- name: Verify packaged rpm artifact
|
||||
run: bash scripts/verify-linux-rpm-artifact.sh aarch64
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -494,6 +745,7 @@ jobs:
|
||||
release/*.AppImage
|
||||
release/*.deb
|
||||
release/*.rpm
|
||||
release/*.pacman
|
||||
release/*.yml
|
||||
release/*.blockmap
|
||||
if-no-files-found: ignore
|
||||
@@ -599,12 +851,53 @@ jobs:
|
||||
artifacts/*.AppImage
|
||||
artifacts/*.deb
|
||||
artifacts/*.rpm
|
||||
artifacts/*.pacman
|
||||
artifacts/*.yml
|
||||
artifacts/*.blockmap
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: false
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
update-nix-release:
|
||||
name: update Nix release metadata
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
&& !contains(github.ref_name, '-')
|
||||
&& (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release))
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout default branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Download Linux artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Update Nix release metadata
|
||||
run: node .github/scripts/update-nix-release.js --artifacts artifacts --version "${GITHUB_REF_NAME#v}"
|
||||
|
||||
- name: Commit Nix release metadata
|
||||
shell: bash
|
||||
run: |
|
||||
if git diff --quiet -- nix/release.nix; then
|
||||
echo "Nix release metadata is already current."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add nix/release.nix
|
||||
git commit -m "Update Nix release metadata for ${GITHUB_REF_NAME}"
|
||||
git push origin HEAD:${{ github.event.repository.default_branch }}
|
||||
|
||||
homebrew-tap:
|
||||
name: bump homebrew tap
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
name: issue-format
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
# Skip issues opened by bots (e.g. dependabot) and maintainers fixing format
|
||||
if: >-
|
||||
github.event.issue.user.type != 'Bot' &&
|
||||
!contains(github.event.issue.labels.*.name, 'format-exempt')
|
||||
steps:
|
||||
- name: Validate title and body
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title.trim();
|
||||
const body = (issue.body || '').trim();
|
||||
const errors = [];
|
||||
|
||||
const modernTitle = /^\[(Bug|Feature)\] .{8,}/.test(title);
|
||||
const legacyAppTitle = /^Bug:\s*.{5,}/i.test(title);
|
||||
if (!modernTitle && !legacyAppTitle) {
|
||||
errors.push(
|
||||
'Title must start with `[Bug]` or `[Feature]` followed by a short summary (at least 8 characters after the prefix). Legacy app links using `Bug: ...` are also accepted. Example: `[Bug] SFTP upload fails on Windows`'
|
||||
);
|
||||
}
|
||||
|
||||
if (body.length < 120) {
|
||||
errors.push(
|
||||
'Body is too short. Please use the Bug Report or Feature Request template and fill in all required fields.'
|
||||
);
|
||||
}
|
||||
|
||||
const templateMarkers = [
|
||||
'Steps to reproduce',
|
||||
'Expected behavior',
|
||||
'Actual behavior',
|
||||
'Describe the problem',
|
||||
'Problem / pain point',
|
||||
'Proposed solution',
|
||||
'Operating system',
|
||||
];
|
||||
const hasTemplateStructure = templateMarkers.some((marker) =>
|
||||
body.includes(marker)
|
||||
);
|
||||
if (!hasTemplateStructure) {
|
||||
errors.push(
|
||||
'Body does not look like it came from an issue template. Choose **Bug Report** or **Feature Request** when opening an issue.'
|
||||
);
|
||||
}
|
||||
|
||||
const labels = new Set(
|
||||
(issue.labels || []).map((label) =>
|
||||
typeof label === 'string' ? label : label.name
|
||||
)
|
||||
);
|
||||
|
||||
if (errors.length === 0) {
|
||||
if (
|
||||
issue.state === 'closed' &&
|
||||
labels.has('invalid-format')
|
||||
) {
|
||||
labels.delete('invalid-format');
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: 'open',
|
||||
labels: [...labels],
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: '<!-- issue-format-bot --> Format looks good now. Reopening this issue.',
|
||||
});
|
||||
}
|
||||
core.info('Issue format OK');
|
||||
return;
|
||||
}
|
||||
|
||||
const issueNumber = issue.number;
|
||||
const marker = '<!-- issue-format-bot -->';
|
||||
const bodyText = [
|
||||
marker,
|
||||
'## Issue format check failed',
|
||||
'',
|
||||
'This issue was closed automatically because it does not follow the required format.',
|
||||
'',
|
||||
...errors.map((e) => `- ${e}`),
|
||||
'',
|
||||
'### How to resubmit',
|
||||
'',
|
||||
'1. Go to [New Issue](https://github.com/binaricat/Netcatty/issues/new/choose)',
|
||||
'2. Pick **Bug Report** or **Feature Request**',
|
||||
'3. Fill in every required field',
|
||||
'4. Keep the `[Bug]` or `[Feature]` prefix in the title and add a clear summary after it (older app versions may use `Bug: ...`)',
|
||||
'',
|
||||
'For questions and open-ended discussion, use [GitHub Discussions](https://github.com/binaricat/Netcatty/discussions) instead.',
|
||||
'',
|
||||
'If you believe this was a mistake, reply here after fixing the title/body and a maintainer can reopen.',
|
||||
].join('\n');
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
const alreadyNotified = comments.some((c) =>
|
||||
(c.body || '').includes(marker)
|
||||
);
|
||||
|
||||
if (!alreadyNotified) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: bodyText,
|
||||
});
|
||||
}
|
||||
|
||||
labels.add('invalid-format');
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned',
|
||||
labels: [...labels],
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
name: spam-comments
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
delete-spam:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout filter rules
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Delete suspected malware spam
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const path = require('node:path');
|
||||
const { detectSpamComment } = require(
|
||||
path.join(process.env.GITHUB_WORKSPACE, 'scripts/spam-comment-filter.cjs')
|
||||
);
|
||||
|
||||
const comment = context.payload.comment;
|
||||
const result = detectSpamComment({
|
||||
body: comment.body || '',
|
||||
authorAssociation: comment.author_association,
|
||||
userType: comment.user && comment.user.type,
|
||||
});
|
||||
|
||||
core.info(JSON.stringify(result, null, 2));
|
||||
|
||||
if (!result.spam) {
|
||||
core.info('Comment did not match the zip/dangerous-file spam filter.');
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
|
||||
core.notice(
|
||||
`Deleted comment with zip/dangerous attachment by @${comment.user.login} on #${context.payload.issue.number}: ${result.reasons.join('; ')}`
|
||||
);
|
||||
@@ -27,11 +27,21 @@ jobs:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install shell test dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fish
|
||||
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Verify generated Catty tool specs
|
||||
run: |
|
||||
npm run generate:capability-tools
|
||||
git diff --exit-code infrastructure/ai/harness/generated/cattyToolSpecs.json
|
||||
|
||||
- name: Test
|
||||
run: npm test
|
||||
+23
-1
@@ -8,6 +8,7 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
@@ -40,10 +41,22 @@ coverage
|
||||
|
||||
# Codex
|
||||
/.codex/
|
||||
/CLAUDE.md
|
||||
|
||||
# OMO / local agent run-continuation state
|
||||
/.omo/
|
||||
|
||||
# Qoder
|
||||
.qoder
|
||||
|
||||
# Workbuddy
|
||||
.workbuddy
|
||||
|
||||
# Codebuddy
|
||||
.codebuddy
|
||||
|
||||
# AI / Superpowers generated docs (local only)
|
||||
/docs/superpowers/
|
||||
/.superpowers/
|
||||
|
||||
# Dev-only electron-updater test config (not for production)
|
||||
/dev-app-update.yml
|
||||
@@ -74,3 +87,12 @@ build_with_vs2022.bat
|
||||
/resources/mosh/*/mosh-client-*-dlls/
|
||||
/resources/mosh/*/*.dll
|
||||
/resources/mosh/*/terminfo/
|
||||
|
||||
# Bundled EternalTerminal `et` client binaries fetched at pack time by
|
||||
# scripts/fetch-et-binaries.cjs. resources/et/README.md is committed; the
|
||||
# actual binaries (and any DLL bundle for dynamically-linked Windows builds)
|
||||
# are pulled from the dedicated et binary repository, never committed.
|
||||
/resources/et/*/et
|
||||
/resources/et/*/et.exe
|
||||
/resources/et/*/et-*-dlls/
|
||||
/resources/et/*/*.dll
|
||||
@@ -22,6 +22,68 @@ This project is wired around three layers: domain (pure logic), application stat
|
||||
- `App.tsx` wires hooks to components; no business logic should live in components beyond view glue.
|
||||
- Local storage keys are centralized in `infrastructure/config/storageKeys.ts`; avoid ad-hoc `localStorage` calls elsewhere.
|
||||
|
||||
## AI Agent Harness (`infrastructure/ai/harness/`)
|
||||
|
||||
Turn orchestration is centralized in **AgentRuntime**; the React hook `useAIChatStreaming` only manages UI state and delegates `runTurn` / `stopTurn`.
|
||||
|
||||
| Layer | Module | Role |
|
||||
|-------|--------|------|
|
||||
| Runtime | `agentRuntime.ts`, `globalAgentRuntime.ts` | Turn lifecycle, trace fan-out, per-turn ToolOutputStore / ToolResultDedup |
|
||||
| Drivers | `turnDrivers/cattyTurnDriver.ts`, `turnDrivers/externalSdkTurnDriver.ts` | Catty `streamText` + External SDK IPC; emit unified `AgentEvent`s |
|
||||
| Context | `contextManager.ts`, `contextBudget.ts`, `tokenEstimator.ts`, `sessionState.ts`, `staleContextPruner.ts`, `compactionPruner.ts`, `cattyRuntime.ts` | Pre-turn / step / 413 compaction, dynamic thresholds, SessionState reinjection, stale tool pruning |
|
||||
| Tools | `capabilityTools.ts`, `toolOutputStore.ts`, `toolResultDedup.ts` | Catalog tools, truncated output handles (`tool_output_read`), duplicate-read notices |
|
||||
| Trace | `traceStore.ts`, `agentEventAdapter.ts` | Session event log incl. `usage`, `performance`, and `CompactionTrace` |
|
||||
|
||||
**Stop** always goes through `stopAgentTurn()` (UI, `/stop`, MCP). Do not add parallel abort paths in hooks.
|
||||
|
||||
### AI SDK v7 (Catty path)
|
||||
|
||||
Catty sidebar turns use **Vercel AI SDK 7** via `streamText` in `turnDrivers/cattyStreamProcessor.ts`. Key conventions:
|
||||
|
||||
| Concern | Module | Notes |
|
||||
|---------|--------|-------|
|
||||
| `runtimeContext` | `cattyRuntimeContext.ts` | Per-turn orchestration state (`chatSessionId`, `turnId`, `agentKind`, `permissionMode`, scope, `lastCompaction`). Passed to `prepareStep` and lifecycle callbacks. **Does not replace** pre-turn compaction or 413 handling. |
|
||||
| `toolsContext` | `capabilityTools.ts` | Per-tool keyed context (`bridge`, `getExecutorContext`, `toolOutputStore`, …). Tools read deps from `{ context }` in `execute`, not closure capture. |
|
||||
| `toolApproval` | `cattyToolApproval.ts` | Write-tool gating for Catty `streamText` only. Calls `requestApproval()` in confirm mode; observer auto-denies writes. **External MCP agents** still approve via main-process `mcpServerBridge` → `setupMcpApprovalBridge()` — unchanged. |
|
||||
| `timeout` | `streamTimeouts.ts` | `totalMs` / `stepMs` / `chunkMs` / `toolMs` on `streamText`; compaction `generateText` uses a shorter 90s timeout. |
|
||||
| Lifecycle | `cattyStreamProcessor.ts` | `onStart` → `model_call_start`; `onStepEnd` → per-step `step_end` usage; `onEnd` → turn-total `usage`; `finalStep.performance` → `performance` event. Distinct from `AgentRuntime` turn_start/turn_end. |
|
||||
|
||||
Compaction remains **`prepareTurnContext` / `compactCattyMessages`** (pre-turn + 413-retry). Step-level pruning is **`prepareStepContext`** only (typed compression + handle notices, no LLM summarize).
|
||||
|
||||
### Capability exposure (Round 2 + gap fill)
|
||||
|
||||
Single source of truth: `electron/capabilities/catalog/` + `electron/capabilities/codegen/toolSurfaces.cjs`.
|
||||
|
||||
**Agent kinds** (where an in-app agent runs — orthogonal to MCP/CLI/RPC surfaces):
|
||||
|
||||
| Kind | UI | Tool list | Notes |
|
||||
|------|-----|-----------|--------|
|
||||
| `sidebar` | Chat side panel (Catty) | `listAgentToolSpecs('sidebar')` → `cattyToolSpecs.json` | Includes `harness.*` renderer-local tools (`surfaces.catty`) |
|
||||
| `global` | Future app-wide agent | `listAgentToolSpecs('global')` → `globalAgentToolSpecs.json` | Shared RPC tools (terminal, SFTP, vault, …); **no** sidebar-only harness tools unless opted in |
|
||||
|
||||
Placement rules (`resolveAgentKinds` in `toolSurfaces.cjs`):
|
||||
|
||||
- Explicit `agentKinds` on a catalog entry overrides inference.
|
||||
- `surfaces.globalAgent` only → global agent (future global-only local tools).
|
||||
- `surfaces.catty` only (harness) → sidebar only.
|
||||
- RPC/MCP-backed tools → both agents unless restricted via `agentKinds`.
|
||||
|
||||
| Surface | Codegen / consumer | Notes |
|
||||
|---------|-------------------|--------|
|
||||
| Catty (sidebar) tools | `npm run generate:capability-tools` → `infrastructure/ai/harness/generated/cattyToolSpecs.json` | Sidebar agent tool set. CI verifies JSON drift. |
|
||||
| Global agent tools | same script → `globalAgentToolSpecs.json` | Prepared for future global agent runtime; shared RPC tools only today. |
|
||||
| MCP stdio | `electron/capabilities/codegen/mcpToolRegistry.cjs` → `electron/mcp/netcatty-mcp-server.cjs` | Registry-driven; external agents. Harness tools are **not** on MCP. |
|
||||
| CLI | `electron/cli/netcatty-tool-cli.cjs` + `electron/capabilities/adapters/cliAdapter.cjs` | **30** catalog commands; exec/sftp/session remain special-case; vault/portforward/snippets use catalog fallback dispatch |
|
||||
| RPC dispatch | `electron/bridges/mcpServerBridge.cjs` + `capabilityRpcDispatch.cjs` | `netcatty/*` builtin handlers via `buildBuiltinRpcHandlerRegistry` (catalog-aligned); `public/*`, `vault/*`, `portforward/*` → services |
|
||||
| Vault bridge | `electron/bridges/aiBridge/vaultAgentBridge.cjs` + `infrastructure/ai/vaultAgentBridgeClient.ts` | Renderer vault state; **never** returns password/privateKey |
|
||||
| AI context | `buildAITerminalSessionInfo` + `useTerminalAiContexts` | Per-session `hostChain` + `activePortForwards`; mirrored in `getContext` and Catty system prompt |
|
||||
|
||||
**Policy:** SFTP writes/transfers, `portforward_start`, and `host_notes_set` require confirm-mode approval. Observer mode blocks writes.
|
||||
|
||||
**Handles:** `ToolOutputStore` persists across turns per chat session; cleared on chat session delete. Large `sftp.read` results spill to `tool_output_read`.
|
||||
|
||||
**Harness domain (`catalog/harness.cjs`):** Catty-only surface (`surfaces.catty.toolName`). Registered in the capability catalog but executed locally in `capabilityTools.executeLocalCattyCapability` (not MCP/CLI). `harness.web.search` is omitted when web search is not configured.
|
||||
|
||||
## Extending the System
|
||||
1) **New domain logic**: Add pure functions/types under `domain/`; avoid side effects.
|
||||
2) **New stateful behavior**: Wrap it in a hook under `application/state/`; keep external I/O behind adapters.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start dev server (runs lint first, then Vite + Electron concurrently)
|
||||
npm run dev
|
||||
|
||||
# Lint
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run a single test file
|
||||
node --test --import tsx path/to/file.test.ts
|
||||
|
||||
# Build renderer
|
||||
npm run build
|
||||
|
||||
# Package for current platform
|
||||
npm run pack
|
||||
|
||||
# Package for specific platforms
|
||||
npm run pack:mac
|
||||
npm run pack:win
|
||||
npm run pack:linux
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
Netcatty is an Electron + React desktop app (SSH manager, terminal, SFTP browser). It has two runtimes:
|
||||
|
||||
### Electron Main Process (`electron/`)
|
||||
- **`main.cjs`** — entry point; wires crash logging, process error guards, and delegates to `main/registerBridges.cjs`
|
||||
- **`bridges/`** — one `.cjs` file per capability domain (sshBridge, sftpBridge, terminalBridge, portForwardingBridge, aiBridge, etc.). Each bridge exposes IPC handlers via `ipcMain`. Tests live alongside the bridge file (`*.test.cjs`).
|
||||
- **`preload.cjs`** — exposes a typed `window.electron` API to the renderer via `contextBridge`. Uses `preload/api.cjs` for the generated API surface.
|
||||
- **`cli/`** — `netcatty-tool-cli.cjs` is a separate internal binary for tool/MCP integration; treat as internal surface only.
|
||||
|
||||
### Renderer Process (React + Vite)
|
||||
Three-layer architecture (see `AGENTS.md` for full detail):
|
||||
|
||||
- **`domain/`** — pure TypeScript logic, no side effects. Models (`models.ts`), host helpers, workspace tree operations.
|
||||
- **`application/state/`** — React hooks that own state and persistence boundaries. Key hooks: `useVaultState` (hosts/keys/snippets), `useSessionState` (terminal sessions/workspace), `useSettingsState` (theme/config).
|
||||
- **`infrastructure/`** — external edges: `persistence/localStorageAdapter.ts` for storage, `services/` for network calls (Gemini AI, GitHub Gist sync), `config/` for defaults, storage keys, and terminal themes.
|
||||
- **`components/`** — presentation only. `App.tsx` wires hooks to components; no business logic in components.
|
||||
|
||||
### IPC Pattern
|
||||
UI calls `window.electron.*` (preload API) → IPC → bridge handler in main process. Never call `ipcRenderer` directly from components.
|
||||
|
||||
### Key Conventions
|
||||
- All storage reads/writes go through `localStorageAdapter`; storage keys are in `infrastructure/config/storageKeys.ts`.
|
||||
- Temporary files must use `tempDirBridge.getTempFilePath(fileName)` — never `os.tmpdir()` directly.
|
||||
- Aside panels (VaultView subpages) use the shared design system in `components/ui/aside-panel.tsx` — see `AGENTS.md` for usage patterns.
|
||||
- Renderer code is TypeScript/ESM; Electron main/bridges are CommonJS (`.cjs`).
|
||||
- Path alias `@/` resolves to the repo root (configured in `vite.config.ts` and `tsconfig.json`).
|
||||
@@ -0,0 +1,129 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported privately to the community leaders responsible for enforcement at
|
||||
support@netcatty.com. Please do not report Code of Conduct incidents through
|
||||
public GitHub issues.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Contributing to Netcatty
|
||||
|
||||
Thank you for your interest in contributing to Netcatty — an AI-powered SSH client, SFTP browser, and terminal manager built with Electron, React, and xterm.js.
|
||||
|
||||
Please read this guide before submitting issues or pull requests.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project follows the [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you agree to uphold this standard.
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
- Report bugs via [GitHub Issues](https://github.com/binaricat/Netcatty/issues)
|
||||
- Suggest features or improvements
|
||||
- Fix bugs or implement features listed in Issues
|
||||
- Improve documentation
|
||||
|
||||
## Development Setup
|
||||
|
||||
**Prerequisites:** Node.js 22+ and npm.
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/binaricat/Netcatty.git
|
||||
cd Netcatty
|
||||
|
||||
# Install dependencies
|
||||
npm ci
|
||||
|
||||
# Start in development mode (Vite + Electron)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Build & Package
|
||||
|
||||
```bash
|
||||
npm run build # Build for production
|
||||
npm run pack # Package for current platform
|
||||
npm run pack:mac # macOS (DMG + ZIP)
|
||||
npm run pack:win # Windows (NSIS, portable, ZIP)
|
||||
npm run pack:linux # Linux (AppImage, DEB, RPM, pacman)
|
||||
```
|
||||
|
||||
## Linting and Tests
|
||||
|
||||
```bash
|
||||
npm run lint # Run ESLint
|
||||
npm test # Run the test suite
|
||||
```
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a feature branch:
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
3. Make your changes, following the existing project style.
|
||||
4. Commit with a clear message:
|
||||
```bash
|
||||
git commit -m 'feat: add amazing feature'
|
||||
```
|
||||
5. Push the branch and open a Pull Request against `main`.
|
||||
6. Run `npm run lint` and `npm test` before requesting review.
|
||||
7. If you changed the capability catalog, run `npm run generate:capability-tools`
|
||||
and commit any generated updates.
|
||||
8. Ensure all CI checks pass before requesting review.
|
||||
|
||||
## Commit Message Convention
|
||||
|
||||
We recommend following [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
- `feat:` — new feature
|
||||
- `fix:` — bug fix
|
||||
- `docs:` — documentation only
|
||||
- `refactor:` — code change without feature or fix
|
||||
- `chore:` — build process or tooling changes
|
||||
|
||||
## Architecture & Coding Guidelines
|
||||
|
||||
Netcatty is organized around three layers:
|
||||
|
||||
- Domain logic in `domain/`
|
||||
- Application state in `application/state/`
|
||||
- UI components in `components/`
|
||||
|
||||
Keep side effects in application or infrastructure code, avoid direct
|
||||
`localStorage` or network calls from components, and update relevant
|
||||
documentation when behavior changes.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the [GPL-3.0 License](./LICENSE).
|
||||
@@ -0,0 +1,216 @@
|
||||
# EternalTerminal (ET) 集成清单 — 按 Mosh 方式重做
|
||||
|
||||
> 目标:在上游最新架构(分支 `feat/et-history-reapply`,基于 `031bf0ee`)上,
|
||||
> **完全照搬 Mosh 的方式**重新集成 EternalTerminal:
|
||||
> 1. **打包客户端** —— 像 `mosh-client` 那样,把 `et` 客户端二进制构建 + 下载 +
|
||||
> 捆绑进安装包,运行时只用捆绑的二进制(不依赖系统安装的 et)。
|
||||
> 2. **接入协议** —— 把旧分支 `feat/eternal-terminal`(tip `67e81616`)里的 ET
|
||||
> 后端 + UI 重新落到上游重构后的目录结构上,并让它启动**捆绑的** `et`。
|
||||
>
|
||||
> 旧实现参考:`git show 67e81616`(共 7 个 ET 提交,见 `feat/eternal-terminal`)。
|
||||
> Mosh 模板参考:`resources/mosh/README.md`、`scripts/*mosh*`、
|
||||
> `electron/bridges/terminalBridge/moshSession.cjs`、`.github/workflows/build-mosh-binaries.yml`。
|
||||
|
||||
## 关键设计差异(ET vs Mosh)
|
||||
|
||||
- **协议**:Mosh 需要 Node 重写 Perl 包装器(SSH bootstrap + 抓 `MOSH CONNECT` +
|
||||
换 PTY)。**ET 不需要** —— `et` 客户端自己完成 SSH 引导 + 协议握手,我们只要
|
||||
把 `et` 当作普通 PTY 进程 `pty.spawn` 即可。所以**没有** `etHandshake.cjs`。
|
||||
- **凭证注入**:Mosh 自己驱动 ssh、直接往 PTY 里敲密码;ET 内部驱动 ssh,需用
|
||||
**SSH_ASKPASS + 临时 ~/.ssh 环境**把保存的密码/密钥/跳板/算法喂给 et 内部的 ssh
|
||||
(旧实现 `prepareEtSshEnvironment` 已完整实现,直接搬运)。
|
||||
- **terminfo**:`et` 是纯传输客户端、本地不渲染终端,**无需** 捆绑 terminfo
|
||||
(Mosh 因静态 ncurses 才需要)。打包目录里只放 `et[.exe]`(+ Windows DLL)。
|
||||
- **构建系统**:Mosh 用 autotools;**ET 用 CMake + Ninja + vcpkg**
|
||||
(`cmake -DDISABLE_TELEMETRY=ON -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo`),
|
||||
产物是单个 `et`(Windows `et.exe`)。
|
||||
|
||||
## 命名约定(镜像 Mosh)
|
||||
|
||||
| Mosh | ET |
|
||||
|------|----|
|
||||
| `resources/mosh/<plat-arch>/mosh-client[.exe]` | `resources/et/<plat-arch>/et[.exe]` |
|
||||
| 打包后 `<Resources>/mosh/mosh-client` | 打包后 `<Resources>/et/et` |
|
||||
| `scripts/build-mosh/` | `scripts/build-et/` |
|
||||
| `scripts/fetch-mosh-binaries.cjs` | `scripts/fetch-et-binaries.cjs` |
|
||||
| `scripts/resolve-mosh-bin-release.cjs` | `scripts/resolve-et-bin-release.cjs` |
|
||||
| `scripts/mosh-extra-resources.cjs` | `scripts/et-extra-resources.cjs` |
|
||||
| env `MOSH_BIN_RELEASE` / 仓库 `Netcatty-mosh-bin` / tag `mosh-bin-*` | env `ET_BIN_RELEASE` / 仓库 `Netcatty-et-bin` / tag `et-bin-*` |
|
||||
| `npm run fetch:mosh[:dev]` | `npm run fetch:et[:dev]` |
|
||||
| `bundledMoshClient()` / `resolveBareMoshClient()` | `bundledEtClient()` / `resolveBareEtClient()` |
|
||||
| `.github/workflows/build-mosh-binaries.yml` | `.github/workflows/build-et-binaries.yml` |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — 打包基础设施(构建/下载/捆绑)
|
||||
|
||||
- [x] **1.1** `resources/et/README.md` —— 镜像 `resources/mosh/README.md`:说明
|
||||
二进制来源、`Netcatty-et-bin` 发布仓库、`et-bin-*` tag、许可证(ET 为
|
||||
Apache-2.0,与 GPL-3.0 兼容)、可复现构建命令。
|
||||
- [x] **1.2** `.gitignore` —— 追加 ET 段(镜像 mosh 段):
|
||||
`/resources/et/*/et`、`/resources/et/*/et.exe`、`/resources/et/*/*.dll`、
|
||||
`/resources/et/*/et-win32-*-dlls/`。保留 `resources/et/README.md`。
|
||||
- [x] **1.3** `scripts/build-et/build-linux.sh` —— manylinux2014 + vcpkg 静态三元组
|
||||
构建 `et`(x64/arm64),产物 `et-linux-<arch>.tar.gz`(+.sha256),内含单个 `et`。
|
||||
校验非系统动态库(同 mosh 的 ldd 白名单)。
|
||||
- [x] **1.4** `scripts/build-et/build-macos.sh` —— arm64 + x86_64 分别构建后 `lipo`
|
||||
成 universal,`MACOSX_DEPLOYMENT_TARGET=11.0`,产物 `et-darwin-universal.tar.gz`。
|
||||
- [x] **1.5** `scripts/build-et/build-windows.ps1`(或 `.sh`)—— MSVC + vcpkg
|
||||
`x64-windows-static`,产物 `et-win32-x64.tar.gz`(含 `et.exe`;若动态链接 CRT
|
||||
则随附 DLL 目录 `et-win32-x64-dlls/`,否则纯静态无 DLL)。
|
||||
- [x] **1.6** `scripts/et-extra-resources.cjs` —— 镜像 `mosh-extra-resources.cjs`:
|
||||
按平台/arch 仅当 `resources/et/<plat-arch>/et[.exe]` 存在时才产出 extraResources
|
||||
指令(`to: "et/"`);Windows 额外处理可选 DLL 目录。**去掉 terminfo 分支**。
|
||||
- [x] **1.7** `scripts/resolve-et-bin-release.cjs` —— 镜像 `resolve-mosh-bin-release.cjs`:
|
||||
`TAG_RE=/^et-bin-.../`,默认仓库 `Netcatty-et-bin`,env `ET_BIN_RELEASE` 优先。
|
||||
- [x] **1.8** `scripts/fetch-et-binaries.cjs` —— 镜像 `fetch-mosh-binaries.cjs`:
|
||||
`TARGETS` 四项(linux-x64/arm64、darwin-universal、win32-x64),全部 tar.gz;
|
||||
SHA256SUMS 校验;解包到 `resources/et/<plat-arch>/`。**Windows 用自建产物**
|
||||
(ET 官方有 Windows 构建,无需 FluentTerminal 那种 fallback)。去掉 terminfo 校验。
|
||||
- [x] **1.9** 单元测试:`scripts/fetch-et-binaries.test.cjs`、
|
||||
`scripts/resolve-et-bin-release.test.cjs`、`scripts/et-extra-resources.test.cjs`
|
||||
(镜像对应 mosh 测试,改名/改路径)。
|
||||
- [x] **1.10** `package.json` scripts:新增
|
||||
`"fetch:et": "node scripts/fetch-et-binaries.cjs"`、
|
||||
`"fetch:et:dev": "node scripts/fetch-et-binaries.cjs --host --resolve-release"`;
|
||||
把 `dev` 脚本改成先 `fetch:mosh:dev && fetch:et:dev`;`test` glob 已覆盖
|
||||
`scripts/*.test.cjs`(确认即可)。
|
||||
- [x] **1.11** `electron-builder.config.cjs`:引入 `etExtraResources`,在 darwin/win32/
|
||||
linux 三处把 `etExtraResources(plat)` 合并进 `extraResources`(与 mosh 数组拼接)。
|
||||
- [x] **1.12** `.github/workflows/build-et-binaries.yml` —— 镜像
|
||||
`build-mosh-binaries.yml`:四个构建 job + 一个 `release` job(dispatch 且
|
||||
`release_tag` 非空时发布到 `Netcatty-et-bin`,附 `SHA256SUMS`)。`paths` 过滤
|
||||
指向 `scripts/build-et/**`、`scripts/fetch-et-binaries.cjs`、`scripts/et-extra-resources.cjs`。
|
||||
env 用 `ET_REF`(默认 ET release tag,如 `et-v6.2.x`)。
|
||||
> 注:实际二进制由用户手动 `workflow_dispatch` 触发产出;本地/CI 未设
|
||||
> `ET_BIN_RELEASE` 时 fetch 步骤安静跳过(同 mosh)。
|
||||
|
||||
## Phase 2 — 运行时定位捆绑客户端
|
||||
|
||||
- [x] **2.1** `electron/bridges/terminalBridge.cjs` 新增 `bundledEtClient(opts)`
|
||||
—— 镜像 `bundledMoshClient`:打包路径 `<Resources>/et/et[.exe]`;dev 回退
|
||||
`<projectRoot>/resources/et/<plat-arch>/et[.exe]`;导出到 module.exports。
|
||||
|
||||
## Phase 3 — ET 协议后端(搬运旧实现到新架构)
|
||||
|
||||
- [x] **3.1** 新建 `electron/bridges/terminalBridge/etSession.cjs` —— 用上游
|
||||
`moshSession.cjs` 的 `createXxxSessionApi(ctx)` + `with(ctx)` 工厂模式,封装:
|
||||
`ET_ASKPASS_SCRIPT`、`writeSecureFile`、`prepareEtSshEnvironment`、
|
||||
`createEtAskpassArtifacts`、`cleanupStaleEtTempDirs`、
|
||||
`cleanupSessionExternalAuthArtifacts`、`execOnEtSession`、`startEtSession`。
|
||||
**改动点**:`etCmd` 由 `findExecutable('et')` 改为 `resolveBareEtClient()`
|
||||
(取捆绑二进制);找不到时抛错(同 mosh:提示跑 `npm run fetch:et:dev`)。
|
||||
Windows 若有 DLL 目录,复用 `prependEnvPath` 思路把 DLL 目录加进 PATH。
|
||||
- [x] **3.2** `terminalBridge.cjs` 接线 `createEtSessionApi(ctx)`(镜像 moshSessionApi
|
||||
的 ctx),传入 `bundledEtClient`、`tempDirBridge`、`execFile/execFileSync` 等;
|
||||
解构出 `startEtSession`、`execOnEtSession`、`cleanupStaleEtTempDirs`、
|
||||
`cleanupSessionExternalAuthArtifacts`、`resolveBareEtClient`。
|
||||
- [x] **3.3** `init()` 调 `cleanupStaleEtTempDirs()`;`registerHandlers` 加
|
||||
`ipcMain.handle("netcatty:et:start", startEtSession)`;`closeSession` 与
|
||||
`cleanupAllSessions` 调 `cleanupSessionExternalAuthArtifacts(session)`;
|
||||
`module.exports` 导出 `startEtSession`、`execOnEtSession`、`bundledEtClient`。
|
||||
- [x] **3.4** 测试:`terminalBridge.bundledEt.test.cjs`(路径解析)+
|
||||
`terminalBridge/etSession.test.cjs`(prepareEtSshEnvironment 的端口/密钥/
|
||||
askpass/跳板/legacy 算法分支)。可参考旧分支是否已有 ET 测试并搬运。
|
||||
|
||||
## Phase 4 — domain / 类型 / preload 接口面
|
||||
|
||||
- [x] **4.1** `domain/models.ts`:`HostProtocol` 加 `'et'`;`ProtocolConfig.etPort?`;
|
||||
`Host`/`GroupConfig` 加 `etEnabled?`/`etPort?`/`etTerminalPath?`;
|
||||
`TerminalSession.etEnabled?`;`ConnectionLog.protocol` 加 `'et'`。
|
||||
(照搬 `git show 794eecdf -- domain/models.ts`)
|
||||
- [x] **4.2** `domain/groupConfig.ts`:加 `etEnabled` 默认项(照搬旧 diff)。
|
||||
- [x] **4.3** `global.d.ts`:`NetcattyBridge` 加 `startEtSession?(options): Promise<...>`
|
||||
及相关 options 类型(照搬 `git show 794eecdf -- global.d.ts`,并补齐后续 ET 提交
|
||||
新增的 etPort/terminalPath/jumpHosts/legacyAlgorithms 字段)。
|
||||
- [x] **4.4** `electron/preload/api.cjs`:加 `startEtSession`(镜像第 26 行的
|
||||
`startMoshSession`)→ `ipcRenderer.invoke("netcatty:et:start", options)`。
|
||||
**注意**:上游已把 preload 重构成 `createPreloadApi`,落点在 `preload/api.cjs`,
|
||||
不是旧的 `preload.cjs` 内联对象。
|
||||
|
||||
## Phase 5 — 渲染层 + UI + i18n
|
||||
|
||||
- [x] **5.1** `application/state/useTerminalBackend.ts`:加 `etAvailable`(查
|
||||
`bridge?.startEtSession`)+ `startEtSession`,并在返回对象/依赖数组里登记
|
||||
(镜像 mosh 的第 10/42/198/205 行处)。
|
||||
- [x] **5.2** `application/state/useSessionState.ts`:路由 ET 会话(照搬旧 diff,+6 行)。
|
||||
- [x] **5.3** `components/terminal/runtime/createTerminalSessionStarters.ts`:加
|
||||
`startEt(term)`(镜像 `startMosh`,组装 options:etPort/terminalPath/
|
||||
jumpHosts/legacyAlgorithms/凭证/identityFilePaths)。
|
||||
**注意**:上游把它从旧的 `infrastructure/runtime/` 移到了
|
||||
`components/terminal/runtime/` —— 落点以上游为准。
|
||||
- [x] **5.4** UI 组件(照搬 `git show b1a306f8 6c0d5bf3 55caa268` 的相应文件,
|
||||
映射到上游同名组件):
|
||||
- [ ] `components/ProtocolSelectDialog.tsx` —— 新增 ET 选项
|
||||
- [ ] `components/QuickConnectWizard.tsx`
|
||||
- [ ] `components/HostDetailsPanel.tsx` —— ET 设置(启用、ET 端口、etterminal 路径)
|
||||
- [ ] `components/GroupDetailsPanel.tsx`
|
||||
- [ ] `components/VaultView.tsx`
|
||||
- [ ] `components/Terminal.tsx` / `components/TerminalLayer.tsx`
|
||||
- [ ] `components/terminal/TerminalConnectionDialog.tsx` / `TerminalToolbar.tsx`
|
||||
- [ ] `App.tsx`
|
||||
- [x] **5.5** i18n:`application/i18n/locales/en.ts` 与 `zh-CN.ts` 加 ET 文案
|
||||
(照搬旧 diff,键名对齐上游现有 mosh 文案结构)。
|
||||
|
||||
## Phase 6 — 校验
|
||||
|
||||
- [x] **6.1** `npm run lint`(确保新 .cjs 在 scripts/ 下不受 ESLint 限制,
|
||||
或按需加 eslint-disable,与 mosh 脚本一致)。
|
||||
- [x] **6.2** `npm test`(新增的 fetch/resolve/extra-resources/etSession 测试全绿)。
|
||||
- [x] **6.3** `npm run build`(渲染层 TS 编译通过,无类型错误)。
|
||||
- [ ] **6.4** 手动冒烟(需先有发布的二进制):
|
||||
`ET_BIN_RELEASE=et-bin-... npm run fetch:et` → `npm run start` →
|
||||
新建 ET 会话连一台装了 etserver 的主机,验证连接/输入/退出/凭证注入。
|
||||
|
||||
---
|
||||
|
||||
## 进度记录
|
||||
|
||||
- 状态:**Phase 1–5 已完成并通过校验**(仅余 1 个可选项 + CI 产二进制)
|
||||
- 验证结果:
|
||||
- `npx eslint <所有改动文件>` → 干净(0 错 0 警)
|
||||
- `npx tsc --noEmit` → 我的改动 **0 个新增类型错误**
|
||||
(`TerminalConnectionDialog` 里 `case 'mosh'` 的 TS2678 是既有问题,行号因我插入 ET 早返回从 60→64,非新增)
|
||||
- `node --test`(ET 相关)→ etSession/bundledEt/3 个脚本测试 **全绿**
|
||||
- `npm test` → 1383 通过 / 16 失败,**16 个全是既有的 Windows 环境失败**
|
||||
(mosh 打包测试的 GNU-tar `C:` 问题、`isExecutableFile` 无 x 位、ACP execPath、SKILL.md 权限、Comware DH 等;均在我未改动的文件里)
|
||||
- `npm run build`(Vite)→ **构建成功**(8.55s),渲染层打包通过
|
||||
|
||||
### 已完成
|
||||
- **Phase 1**:`scripts/et-extra-resources.cjs` / `resolve-et-bin-release.cjs` /
|
||||
`fetch-et-binaries.cjs`(+3 测试,27 通过)、`scripts/build-et/{build-linux.sh,
|
||||
build-macos.sh,build-windows.ps1}`、`.github/workflows/build-et-binaries.yml`、
|
||||
`resources/et/README.md`、`.gitignore`、`package.json`、`electron-builder.config.cjs`。
|
||||
- **Phase 2**:`terminalBridge.cjs` 新增并导出 `bundledEtClient`。
|
||||
- **Phase 3**:`terminalBridge/etSession.cjs`(startEtSession + prepareEtSshEnvironment +
|
||||
SSH_ASKPASS 机制 + execOnEtSession + 清理),接线进 terminalBridge.cjs(ctx/IPC
|
||||
`netcatty:et:start`/init 清理/close/quit 清理/导出),+2 测试(13 通过)。
|
||||
**et 指向捆绑二进制**(resolveBareEtClient→bundledEtClient),找不到则报错。
|
||||
- **Phase 4**:domain `connection.ts`/`history.ts`/`terminal.ts`、`groupConfig.ts`、
|
||||
`types/global/netcatty-bridge-session.d.ts`(startEtSession + NetcattyJumpHost[])、
|
||||
`electron/preload/api.cjs`、`domain/vaultImport.ts`(排除 'et' 导入协议)。
|
||||
- **Phase 5**:
|
||||
- 启动派发:`useTerminalEffects.ts`、`Terminal.tsx`(×3) → `startEt`
|
||||
- 运行时 starter:`createTerminalSessionStarters.ts` 新增 `startEt`(含单跳板/凭证/
|
||||
legacy 算法/askpass 路径),`.types.ts` 加 `etAvailable`/`startEtSession`
|
||||
- 后端 hook:`useTerminalBackend.ts`(etAvailable + startEtSession)
|
||||
- 会话透传 etEnabled:`sessionFactories.ts`、`useSessionState.ts`(×6)、
|
||||
`TerminalLayer.tsx`(×3)、`TerminalLayerSupport.tsx`、`AppHandlers.ts`(协议解析/日志/选择)
|
||||
- UI:`HostDetailsAdvancedSections.tsx`(ET 开关+端口+etterminal 路径,与 Mosh 互斥)、
|
||||
`HostDetailsPanel.tsx`、`ProtocolSelectDialog.tsx`(ET 选项)、
|
||||
`TerminalConnectionDialog.tsx`(ET 标签)、`TerminalToolbar.tsx`(编码菜单门控)、
|
||||
`GroupSshSettingsSection.tsx` + `GroupDetailsPanel.tsx`(组级 ET)、`VaultView.tsx`
|
||||
- i18n:en/zh-CN 的 `hostDetails.section.et`、`hostDetails.et.*`、
|
||||
`terminal.connection.protocol.et`、`terminal.et.*`
|
||||
|
||||
### 剩余(可选 / 非阻塞)
|
||||
- [ ] **QuickConnectWizard.tsx**:把 ET 加为“快速连接”协议按钮(type/端口/建主机映射 +
|
||||
UI 按钮)。当前快速连接未列 ET;保存主机后开启 ET 再连即可,故仅为便利项。
|
||||
- [ ] **产出二进制**:手动 `workflow_dispatch` 跑 `build-et-binaries.yml`(带
|
||||
`release_tag=et-bin-<ver>-1`)发布到 `Netcatty-et-bin`,并配 `ET_BIN_RELEASE_TOKEN`
|
||||
secret。之后 `ET_BIN_RELEASE=... npm run fetch:et` 即可本地/打包捆绑 `et`。
|
||||
build-et 脚本本机无法编译 C++,需在 CI 验证。
|
||||
- [ ] **端到端冒烟**:有二进制后 `npm run dev`,对装有 etserver 的主机建 ET 会话验证。
|
||||
|
||||
- 当前分支:`feat/et-history-reapply`(基于上游 `031bf0ee`)
|
||||
- 旧 ET 实现参考分支:`feat/eternal-terminal`(tip `67e81616`,7 个 ET 提交)
|
||||
+54
-76
@@ -5,13 +5,13 @@
|
||||
<h1 align="center">Netcatty</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>モダンな SSH クライアント、SFTP ブラウザ & ターミナルマネージャー</strong><br/>
|
||||
<strong>🔥 AI 搭載の SSH クライアント、SFTP ブラウザ & ターミナルマネージャー 🚀</strong><br/>
|
||||
<a href="https://netcatty.app"><strong>netcatty.app</strong></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Electron、React、xterm.js で構築された機能豊富な SSH ワークスペース。<br/>
|
||||
分割ターミナル、Vault ビュー、SFTP ワークフロー、カスタムテーマ、キーワードハイライト — すべてが一つに。
|
||||
🔥 内蔵 AI Agent · 分割ターミナル · Vault ビュー · SFTP ワークフロー · カスタムテーマ — すべてが一つに。
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -40,16 +40,45 @@
|
||||
|
||||
---
|
||||
|
||||
[](screenshots/main-window-dark.png)
|
||||
<img width="3142" height="1764" alt="Screenshot 2026-07-02 at 22 51 24" src="https://github.com/user-attachments/assets/3116165d-623a-4d3a-a28a-914befb9b72d" />
|
||||
|
||||
---
|
||||
|
||||
<a name="catty-agent"></a>
|
||||
# 🔥 Catty Agent — あなたの IT 運用 AI パートナー
|
||||
|
||||
> 🚀 **AI で日常の IT 運用作業を加速。** Catty Agent はサーバー環境を理解し、コマンドを実行し、自然な会話で複数ホストの複雑な操作をこなす内蔵 AI アシスタントです。
|
||||
|
||||
### 🔥 Catty Agent にできること
|
||||
|
||||
- 🚀 **自然言語でサーバー管理** — コマンドを暗記せず、必要なことを話しかけるだけ
|
||||
- 🔥 **リアルタイムサーバー診断** — 会話を通じてステータス確認、ログ調査、リソース監視
|
||||
- 🚀 **マルチホスト連携** — 複数サーバーのタスクを同時にコーディネート
|
||||
- 🔥 **インテリジェントなコンテキスト認識** — サーバー環境を理解し、的確な回答を提供
|
||||
- 🚀 **ワンステップで複雑な操作** — クラスター構築、サービスデプロイなど簡単な指示で実行
|
||||
|
||||
### 🎬 AI の動作デモ
|
||||
|
||||
#### 🔥 シングルホスト — インテリジェントなサーバー診断
|
||||
|
||||
Catty Agent にサーバーの健全性チェックを依頼すると、適切なコマンドを実行し、出力を分析して数秒で明確なサマリーを提示します。
|
||||
|
||||
<img width="1510" height="870" alt="ai-single" src="https://github.com/user-attachments/assets/d3f34e53-8476-4fab-8634-394b931d1ff1" />
|
||||
|
||||
#### 🚀 マルチホスト — Docker Swarm クラスターのセットアップ
|
||||
|
||||
1 つの会話で 2 台のサーバーをまたいで Docker Swarm クラスターをオーケストレーションする Catty Agent をご覧ください。初期化、トークン交換、ノード参加まですべてこなします — あなたは望む結果を伝えるだけ。
|
||||
|
||||
<img width="1515" height="870" alt="ai-muti" src="https://github.com/user-attachments/assets/ca166fc5-65d9-4d7b-951a-962c2ef230d8" />
|
||||
|
||||
---
|
||||
|
||||
# 目次 <!-- omit in toc -->
|
||||
|
||||
- [🔥 Catty Agent — AI パートナー](#catty-agent)
|
||||
- [Netcatty とは](#netcatty-とは)
|
||||
- [なぜ Netcatty](#なぜ-netcatty)
|
||||
- [機能](#機能)
|
||||
- [デモ](#デモ)
|
||||
- [スクリーンショット](#スクリーンショット)
|
||||
- [メインウィンドウ](#メインウィンドウ)
|
||||
- [Vault ビュー](#vault-ビュー)
|
||||
@@ -71,9 +100,10 @@
|
||||
**Netcatty** は、複数のリモートサーバーを効率的に管理する必要がある開発者、システム管理者、DevOps エンジニア向けに設計された、モダンなクロスプラットフォーム SSH クライアントおよびターミナルマネージャーです。
|
||||
|
||||
- **Netcatty は** PuTTY、Termius、SecureCRT、macOS Terminal.app の代替となる SSH 接続ツール
|
||||
- **Netcatty は** 強力な SFTP クライアント(ドラッグ&ドロップ + 内蔵エディタ)
|
||||
- **Netcatty は** デュアルペインのファイルブラウザを備えた強力な SFTP クライアント
|
||||
- **Netcatty は** 分割ペイン、タブ、セッション管理を備えたターミナルワークスペース
|
||||
- **Netcatty は** シェルの代替ではありません — SSH/Telnet/Mosh やローカル/シリアル経由でシェルに接続します(環境により異なります)
|
||||
- **Netcatty は** SSH、ローカルターミナル、Telnet、Mosh、シリアル接続をサポートします(利用可能な場合)
|
||||
- **Netcatty は** シェルの代替ではありません — SSH/Telnet/Mosh またはローカル/シリアルセッション経由でシェルに接続します
|
||||
|
||||
---
|
||||
|
||||
@@ -82,9 +112,9 @@
|
||||
|
||||
複数サーバーを日常的に扱うなら、Netcatty は「スピード」と「流れ」を重視した作りになっています:
|
||||
|
||||
- **ワークスペース中心** — 分割ペインで複数セッションを並行操作
|
||||
- **Vault の見やすさ** — グリッド/リスト/ツリーで状況に合わせて切り替え
|
||||
- **SFTP の作業感** — ドラッグ&ドロップと内蔵エディタでサクッと編集
|
||||
- **ワークスペース中心** — 分割ペイン + タブ + セッション復元で常時使うワークフローに対応
|
||||
- **Vault の整理** — グリッド/リスト/ツリー表示、高速検索、ドラッグしやすいワークフロー
|
||||
- **本格的な SFTP** — 内蔵エディタ + ドラッグ&ドロップ + スムーズなファイル操作
|
||||
|
||||
---
|
||||
|
||||
@@ -109,43 +139,6 @@
|
||||
|
||||
---
|
||||
|
||||
<a name="デモ"></a>
|
||||
# デモ
|
||||
|
||||
動画で機能をさっと確認できます(素材は `screenshots/gifs/`):
|
||||
|
||||
### Vault ビュー:グリッド / リスト / ツリー
|
||||
状況に合わせて見え方を切り替え。グリッドで全体像、リストで密度、ツリーで階層を扱えます。
|
||||
|
||||
https://github.com/user-attachments/assets/e2742987-3131-404d-bd4b-06423e5bfd99
|
||||
|
||||
### 分割ターミナル + セッション管理
|
||||
複数セッションを分割ペインで並べて作業。関連タスクを横並びにしてコンテキストスイッチを減らします。
|
||||
|
||||
https://github.com/user-attachments/assets/377d0c46-cc5a-4382-aa31-5acfd412ce62
|
||||
|
||||
### SFTP:ドラッグ&ドロップ + 内蔵エディタ
|
||||
ドラッグ&ドロップでファイルを移動し、内蔵エディタでそのまま編集できます。
|
||||
|
||||
https://github.com/user-attachments/assets/c6e06af4-b0d5-461c-b0c7-9d6f655af6c7
|
||||
|
||||
### ドラッグでアップロード
|
||||
ファイルをそのままドロップしてアップロードを開始。ダイアログ操作を減らせます。
|
||||
|
||||
https://github.com/user-attachments/assets/c8e0c4ff-f020-4e18-9b09-681ec97b003f
|
||||
|
||||
### カスタムテーマ
|
||||
テーマを調整して自分の好みに合わせた見た目に。
|
||||
|
||||
https://github.com/user-attachments/assets/77e2a693-4ef2-4823-8ca1-9bcbf14ed98b
|
||||
|
||||
### キーワードハイライト
|
||||
重要な出力(エラー/警告/マーカーなど)を見つけやすくするために、ハイライトをカスタマイズできます。
|
||||
|
||||
https://github.com/user-attachments/assets/e6516993-ad66-4594-8c28-57426082339b
|
||||
|
||||
---
|
||||
|
||||
<a name="スクリーンショット"></a>
|
||||
# スクリーンショット
|
||||
|
||||
@@ -154,29 +147,25 @@ https://github.com/user-attachments/assets/e6516993-ad66-4594-8c28-57426082339b
|
||||
|
||||
メインウィンドウは、長時間の SSH 作業を前提に設計されています。セッション、ナビゲーション、主要ツールへ素早くアクセスできます。
|
||||
|
||||

|
||||
<img width="1531" height="875" alt="black-grid" src="https://github.com/user-attachments/assets/004b80f6-5bbb-4f14-b8cd-33a0a5913b8c" />
|
||||
|
||||

|
||||
<img width="1550" height="876" alt="light" src="https://github.com/user-attachments/assets/2b59a999-a25e-4217-944c-9aef0a09f272" />
|
||||
|
||||
<a name="vault-ビュー"></a>
|
||||
## Vault ビュー
|
||||
|
||||
作業に合わせて見え方を切り替え:グリッドで全体像、リストでスキャン、ツリーで整理と階層ナビゲーション。
|
||||
|
||||

|
||||
<img width="1554" height="882" alt="list" src="https://github.com/user-attachments/assets/03249f15-b5f8-4770-a3c3-d5001636ea00" />
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
<img width="1561" height="878" alt="tree" src="https://github.com/user-attachments/assets/739f7b66-3898-43d0-8dd3-b9b97fd8e8de" />
|
||||
|
||||
<a name="分割ターミナル"></a>
|
||||
## 分割ターミナル
|
||||
|
||||
分割ペインで複数のサーバー/タスクを同時に扱えます(例:デプロイ + ログ + 監視)。
|
||||
|
||||

|
||||
<img width="1560" height="871" alt="split" src="https://github.com/user-attachments/assets/c93a8523-6256-4bb3-8b6e-d599831f2f9f" />
|
||||
|
||||
---
|
||||
|
||||
@@ -201,8 +190,6 @@ Netcatty は接続したホストの OS を検出し、ホスト一覧でアイ
|
||||
<img src="public/distro/almalinux.svg" width="48" alt="AlmaLinux" title="AlmaLinux">
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<a name="はじめに"></a>
|
||||
# はじめに
|
||||
|
||||
@@ -220,6 +207,16 @@ Netcatty は接続したホストの OS を検出し、ホスト一覧でアイ
|
||||
|
||||
> **macOS ユーザーへ:** 現在のリリースはコード署名と notarization が行われている想定です。Gatekeeper の警告が出る場合は、GitHub Releases から最新版の公式ビルドを取得しているか確認してください。
|
||||
|
||||
### Nix / NixOS
|
||||
|
||||
Netcatty は Nix および NixOS ユーザー向けに、公式 Linux AppImage リリースをラップした flake を提供しています:
|
||||
|
||||
```bash
|
||||
nix run github:binaricat/Netcatty
|
||||
```
|
||||
|
||||
宣言型インストールには、Netcatty flake を input として追加し、NixOS または Home Manager のパッケージリストで `inputs.netcatty.packages.${pkgs.system}.default` を使用してください。
|
||||
|
||||
### 前提条件
|
||||
- Node.js 18+ と npm
|
||||
- macOS、Windows 10+、または Linux
|
||||
@@ -238,25 +235,6 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### プロジェクト構造
|
||||
|
||||
```
|
||||
├── App.tsx # メイン React アプリケーション
|
||||
├── components/ # React コンポーネント
|
||||
│ ├── Terminal.tsx # ターミナルコンポーネント
|
||||
│ ├── SftpView.tsx # SFTP ブラウザ
|
||||
│ ├── VaultView.tsx # ホスト管理
|
||||
│ ├── KeyManager.tsx # SSH キー管理
|
||||
│ └── ...
|
||||
├── application/ # 状態管理 & 国際化
|
||||
├── domain/ # ドメインモデル & ロジック
|
||||
├── infrastructure/ # サービス & アダプター
|
||||
├── electron/ # Electron メインプロセス
|
||||
│ ├── main.cjs # メインエントリ
|
||||
│ └── bridges/ # IPC ブリッジ
|
||||
└── public/ # 静的アセット & アイコン
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a name="ビルドとパッケージ"></a>
|
||||
|
||||
@@ -40,8 +40,7 @@
|
||||
|
||||
---
|
||||
|
||||
<img width="2868" height="1784" alt="netcatty SSH (Window) 2026-04-23 11:19 PM" src="https://github.com/user-attachments/assets/d6df734f-9ebc-452a-8b7d-e8a0fdc9463a" />
|
||||
|
||||
<img width="3142" height="1764" alt="Screenshot 2026-07-02 at 22 51 24" src="https://github.com/user-attachments/assets/3116165d-623a-4d3a-a28a-914befb9b72d" />
|
||||
|
||||
---
|
||||
|
||||
@@ -63,25 +62,13 @@
|
||||
|
||||
Ask Catty Agent to check a server's health, and it runs the right commands, analyzes the output, and gives you a clear summary — all in seconds.
|
||||
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/f819a1b6-8cba-4910-8017-97dfc080b477
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<img width="1510" height="870" alt="ai-single" src="https://github.com/user-attachments/assets/d3f34e53-8476-4fab-8634-394b931d1ff1" />
|
||||
|
||||
#### 🚀 Multi-Host — Docker Swarm Cluster Setup
|
||||
|
||||
Watch Catty Agent orchestrate a Docker Swarm cluster across two servers in one conversation. It handles the init, token exchange, and node joining — you just tell it what you want.
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/52fd30b8-9f02-43d4-a3b2-142691e8e3ec
|
||||
|
||||
|
||||
|
||||
|
||||
<img width="1515" height="870" alt="ai-muti" src="https://github.com/user-attachments/assets/ca166fc5-65d9-4d7b-951a-962c2ef230d8" />
|
||||
|
||||
---
|
||||
|
||||
@@ -91,7 +78,6 @@ https://github.com/user-attachments/assets/52fd30b8-9f02-43d4-a3b2-142691e8e3ec
|
||||
- [What is Netcatty](#what-is-netcatty)
|
||||
- [Why Netcatty](#why-netcatty)
|
||||
- [Features](#features)
|
||||
- [Demos](#demos)
|
||||
- [Screenshots](#screenshots)
|
||||
- [Main Window](#main-window)
|
||||
- [Vault Views](#vault-views)
|
||||
@@ -150,76 +136,6 @@ If you regularly work with a fleet of servers, Netcatty is built for speed and f
|
||||
- **Custom themes** — tune the app appearance to your taste
|
||||
- **Keyword highlighting** — customize highlight rules for terminal output
|
||||
|
||||
---
|
||||
|
||||
<a name="demos"></a>
|
||||
# Demos
|
||||
|
||||
Video previews (stored in `screenshots/gifs/`), rendered inline on GitHub:
|
||||
|
||||
### Vault views: grid / list / tree
|
||||
Switch between different Vault views to match your workflow: overview in grid, dense scanning in list, and hierarchical navigation in tree.
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/1ff1f3f1-e5ae-40ea-b35a-0e5148c3afeb
|
||||
|
||||
|
||||
|
||||
### Split terminals + session management
|
||||
Work in multiple sessions at once with split panes. Keep related tasks side-by-side and reduce context switching.
|
||||
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/9c24b519-4b4b-4910-a22a-590d04c9af31
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### SFTP: drag & drop + built-in editor
|
||||
Move files with drag & drop, then edit quickly using the built-in editor without leaving the app.
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/f3afdb36-399d-4330-b9f3-4678f178f6db
|
||||
|
||||
|
||||
|
||||
|
||||
### Drag file upload
|
||||
Drop files into the app to kick off uploads without hunting through dialogs.
|
||||
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/e1e26f7a-3489-41cc-975e-8dccba56ea85
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Custom themes
|
||||
Make Netcatty yours: customize themes and UI appearance.
|
||||
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/1a6049aa-9a4c-4d52-a13d-0b007a791b00
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Keyword highlighting
|
||||
Highlight important terminal output so errors, warnings, and key events stand out at a glance.
|
||||
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/1a1db7bd-948b-4f3c-97cd-8fd0cbe7cce7
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
<a name="screenshots"></a>
|
||||
@@ -230,29 +146,27 @@ https://github.com/user-attachments/assets/1a1db7bd-948b-4f3c-97cd-8fd0cbe7cce7
|
||||
|
||||
The main window is designed for long-running SSH workflows: quick access to sessions, navigation, and core tools in one place.
|
||||
|
||||

|
||||
<img width="1531" height="875" alt="black-grid" src="https://github.com/user-attachments/assets/004b80f6-5bbb-4f14-b8cd-33a0a5913b8c" />
|
||||
|
||||
<img width="1550" height="876" alt="light" src="https://github.com/user-attachments/assets/2b59a999-a25e-4217-944c-9aef0a09f272" />
|
||||
|
||||

|
||||
|
||||
<a name="vault-views"></a>
|
||||
## Vault Views
|
||||
|
||||
Organize and navigate your hosts using the view that best fits the moment: grid for overview, list for scanning, tree for structure.
|
||||
|
||||

|
||||
<img width="1554" height="882" alt="list" src="https://github.com/user-attachments/assets/03249f15-b5f8-4770-a3c3-d5001636ea00" />
|
||||
|
||||

|
||||
<img width="1561" height="878" alt="tree" src="https://github.com/user-attachments/assets/739f7b66-3898-43d0-8dd3-b9b97fd8e8de" />
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<a name="split-terminals"></a>
|
||||
## Split Terminals
|
||||
|
||||
Split panes help you monitor multiple servers/services at the same time (deploy + logs + metrics) without juggling windows.
|
||||
|
||||

|
||||
<img width="1560" height="871" alt="split" src="https://github.com/user-attachments/assets/c93a8523-6256-4bb3-8b6e-d599831f2f9f" />
|
||||
|
||||
---
|
||||
|
||||
@@ -294,6 +208,16 @@ Or browse all releases at [GitHub Releases](https://github.com/binaricat/Netcatt
|
||||
|
||||
> **macOS Users:** Current releases are expected to be code-signed and notarized. If Gatekeeper still warns, make sure you downloaded the latest official build from GitHub Releases.
|
||||
|
||||
### Nix / NixOS
|
||||
|
||||
Netcatty provides a flake that wraps the official Linux AppImage release for Nix and NixOS users:
|
||||
|
||||
```bash
|
||||
nix run github:binaricat/Netcatty
|
||||
```
|
||||
|
||||
For declarative installs, add the Netcatty flake as an input and use `inputs.netcatty.packages.${pkgs.system}.default` in your NixOS or Home Manager package list.
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+ and npm
|
||||
- macOS, Windows 10+, or Linux
|
||||
@@ -312,25 +236,6 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
├── App.tsx # Main React application
|
||||
├── components/ # React components
|
||||
│ ├── Terminal.tsx # Terminal component
|
||||
│ ├── SftpView.tsx # SFTP browser
|
||||
│ ├── VaultView.tsx # Host management
|
||||
│ ├── KeyManager.tsx # SSH key management
|
||||
│ └── ...
|
||||
├── application/ # State management & i18n
|
||||
├── domain/ # Domain models & logic
|
||||
├── infrastructure/ # Services & adapters
|
||||
├── electron/ # Electron main process
|
||||
│ ├── main.cjs # Main entry
|
||||
│ └── bridges/ # IPC bridges
|
||||
└── public/ # Static assets & icons
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a name="build--package"></a>
|
||||
|
||||
+52
-73
@@ -5,13 +5,13 @@
|
||||
<h1 align="center">Netcatty</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>现代化 SSH 客户端、SFTP 浏览器 & 终端管理器</strong><br/>
|
||||
<strong>🔥 AI 驱动的 SSH 客户端、SFTP 浏览器 & 终端管理器 🚀</strong><br/>
|
||||
<a href="https://netcatty.app"><strong>netcatty.app</strong></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
一个基于 Electron、React 和 xterm.js 构建的功能丰富的 SSH 工作空间。<br/>
|
||||
分屏终端、Vault 多视图、SFTP 工作流、自定义主题、关键词高亮 —— 一应俱全。
|
||||
🔥 内置 AI Agent · 分屏终端 · Vault 多视图 · SFTP 工作流 · 自定义主题 —— 一应俱全。
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -40,16 +40,45 @@
|
||||
|
||||
---
|
||||
|
||||
[](screenshots/main-window-dark.png)
|
||||
<img width="3142" height="1764" alt="Screenshot 2026-07-02 at 22 51 24" src="https://github.com/user-attachments/assets/3116165d-623a-4d3a-a28a-914befb9b72d" />
|
||||
|
||||
---
|
||||
|
||||
<a name="catty-agent"></a>
|
||||
# 🔥 Catty Agent — 你的 IT 运维 AI 搭档
|
||||
|
||||
> 🚀 **用 AI 赋能你的日常 IT 运维工作。** Catty Agent 是内置的 AI 助手,能理解你的服务器环境、执行命令,并通过自然对话完成复杂的多主机操作。
|
||||
|
||||
### 🔥 Catty Agent 能做什么?
|
||||
|
||||
- 🚀 **自然语言管理服务器** —— 直接说出需求,不再需要记忆命令
|
||||
- 🔥 **实时服务器诊断** —— 通过对话检查状态、查看日志、监控资源
|
||||
- 🚀 **多主机协同** —— 跨多台服务器同时协调任务
|
||||
- 🔥 **智能上下文感知** —— 理解你的服务器环境,给出有针对性的回应
|
||||
- 🚀 **一键完成复杂操作** —— 搭集群、部署服务,一句话搞定
|
||||
|
||||
### 🎬 AI 实战演示
|
||||
|
||||
#### 🔥 单主机 —— 智能服务器诊断
|
||||
|
||||
让 Catty Agent 检查服务器健康状态,它会自动运行合适的命令、分析输出并给出清晰总结 —— 几秒内完成。
|
||||
|
||||
<img width="1510" height="870" alt="ai-single" src="https://github.com/user-attachments/assets/d3f34e53-8476-4fab-8634-394b931d1ff1" />
|
||||
|
||||
#### 🚀 多主机 —— Docker Swarm 集群搭建
|
||||
|
||||
看 Catty Agent 在一次对话中跨两台服务器编排 Docker Swarm 集群。它负责初始化、令牌交换和节点加入 —— 你只需说出想要的结果。
|
||||
|
||||
<img width="1515" height="870" alt="ai-muti" src="https://github.com/user-attachments/assets/ca166fc5-65d9-4d7b-951a-962c2ef230d8" />
|
||||
|
||||
---
|
||||
|
||||
# 目录 <!-- omit in toc -->
|
||||
|
||||
- [🔥 Catty Agent — AI 搭档](#catty-agent)
|
||||
- [Netcatty 是什么](#netcatty-是什么)
|
||||
- [为什么是 Netcatty](#为什么是-netcatty)
|
||||
- [功能特性](#功能特性)
|
||||
- [演示](#演示)
|
||||
- [界面截图](#界面截图)
|
||||
- [主界面](#主界面)
|
||||
- [Vault 视图](#vault-视图)
|
||||
@@ -83,9 +112,9 @@
|
||||
|
||||
如果你需要同时维护多台服务器,Netcatty 更像是“工作台”而不是单一终端:
|
||||
|
||||
- **以工作区为核心** —— 分屏 + 多会话并行,适合长期驻留的工作流
|
||||
- **Vault 管理** —— 网格/列表/树形视图,配合搜索与拖拽更顺手
|
||||
- **认真做的 SFTP** —— 内置编辑器 + 拖拽上传,文件操作更丝滑
|
||||
- **以工作区为核心** —— 分屏 + 标签页 + 会话恢复,适合长期驻留的工作流
|
||||
- **Vault 管理** —— 网格/列表/树形视图,配合快速搜索与拖拽流程更顺手
|
||||
- **认真做的 SFTP** —— 内置编辑器 + 拖拽上传/下载,文件操作更丝滑
|
||||
|
||||
---
|
||||
|
||||
@@ -110,43 +139,6 @@
|
||||
|
||||
---
|
||||
|
||||
<a name="演示"></a>
|
||||
# 演示
|
||||
|
||||
视频预览(素材均在 `screenshots/gifs/`),在 GitHub README 中可直接观看:
|
||||
|
||||
### Vault 视图:网格 / 列表 / 树形
|
||||
根据不同场景自由切换视图:网格适合总览,列表适合密集浏览,树形适合层级导航与整理。
|
||||
|
||||
https://github.com/user-attachments/assets/e2742987-3131-404d-bd4b-06423e5bfd99
|
||||
|
||||
### 分屏终端 + 会话管理
|
||||
用分屏把多个会话并排放在同一个工作区里,降低来回切换窗口/标签页的成本。
|
||||
|
||||
https://github.com/user-attachments/assets/377d0c46-cc5a-4382-aa31-5acfd412ce62
|
||||
|
||||
### SFTP:拖拽 + 内置编辑器
|
||||
通过拖拽完成文件传输,并用内置编辑器快速修改文件内容,不用来回切换工具。
|
||||
|
||||
https://github.com/user-attachments/assets/c6e06af4-b0d5-461c-b0c7-9d6f655af6c7
|
||||
|
||||
### 拖拽文件上传
|
||||
把文件直接拖进应用即可触发上传流程,省去多层对话框与路径选择。
|
||||
|
||||
https://github.com/user-attachments/assets/c8e0c4ff-f020-4e18-9b09-681ec97b003f
|
||||
|
||||
### 自定义主题
|
||||
按自己的审美与习惯定制主题与界面外观,让日常使用更顺手。
|
||||
|
||||
https://github.com/user-attachments/assets/77e2a693-4ef2-4823-8ca1-9bcbf14ed98b
|
||||
|
||||
### 关键词高亮
|
||||
让关键输出一眼可见:错误、告警或特定标记被高亮后更容易扫到与定位。
|
||||
|
||||
https://github.com/user-attachments/assets/e6516993-ad66-4594-8c28-57426082339b
|
||||
|
||||
---
|
||||
|
||||
<a name="界面截图"></a>
|
||||
# 界面截图
|
||||
|
||||
@@ -155,29 +147,25 @@ https://github.com/user-attachments/assets/e6516993-ad66-4594-8c28-57426082339b
|
||||
|
||||
主界面围绕长期 SSH 工作流设计:把会话、导航和常用工具集中到同一处,减少切换成本。
|
||||
|
||||

|
||||
<img width="1531" height="875" alt="black-grid" src="https://github.com/user-attachments/assets/004b80f6-5bbb-4f14-b8cd-33a0a5913b8c" />
|
||||
|
||||

|
||||
<img width="1550" height="876" alt="light" src="https://github.com/user-attachments/assets/2b59a999-a25e-4217-944c-9aef0a09f272" />
|
||||
|
||||
<a name="vault-视图"></a>
|
||||
## Vault 视图
|
||||
|
||||
用更适合当前任务的方式管理与浏览主机:网格看全局,列表做筛选,树形做整理与层级导航。
|
||||
|
||||

|
||||
<img width="1554" height="882" alt="list" src="https://github.com/user-attachments/assets/03249f15-b5f8-4770-a3c3-d5001636ea00" />
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
<img width="1561" height="878" alt="tree" src="https://github.com/user-attachments/assets/739f7b66-3898-43d0-8dd3-b9b97fd8e8de" />
|
||||
|
||||
<a name="分屏终端"></a>
|
||||
## 分屏终端
|
||||
|
||||
分屏适合同时处理多个任务(例如部署 + 日志 + 排障),不用频繁切换窗口。
|
||||
|
||||

|
||||
<img width="1560" height="871" alt="split" src="https://github.com/user-attachments/assets/c93a8523-6256-4bb3-8b6e-d599831f2f9f" />
|
||||
|
||||
---
|
||||
|
||||
@@ -219,6 +207,16 @@ Netcatty 会自动识别并在主机列表中展示对应的系统图标:
|
||||
|
||||
> **macOS 用户注意:** 当前发布版本应已完成代码签名和公证。如果 Gatekeeper 仍然提示风险,请确认您下载的是 GitHub Releases 中的最新官方构建。
|
||||
|
||||
### Nix / NixOS
|
||||
|
||||
Netcatty 提供了一个 flake,为 Nix 和 NixOS 用户封装了官方 Linux AppImage 发行版:
|
||||
|
||||
```bash
|
||||
nix run github:binaricat/Netcatty
|
||||
```
|
||||
|
||||
声明式安装时,将 Netcatty flake 添加为输入,并在 NixOS 或 Home Manager 的软件包列表中使用 `inputs.netcatty.packages.${pkgs.system}.default`。
|
||||
|
||||
### 前置条件
|
||||
- Node.js 18+ 和 npm
|
||||
- macOS、Windows 10+ 或 Linux
|
||||
@@ -237,25 +235,6 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
├── App.tsx # 主 React 应用
|
||||
├── components/ # React 组件
|
||||
│ ├── Terminal.tsx # 终端组件
|
||||
│ ├── SftpView.tsx # SFTP 浏览器
|
||||
│ ├── VaultView.tsx # 主机管理
|
||||
│ ├── KeyManager.tsx # SSH 密钥管理
|
||||
│ └── ...
|
||||
├── application/ # 状态管理 & 国际化
|
||||
├── domain/ # 领域模型 & 逻辑
|
||||
├── infrastructure/ # 服务 & 适配器
|
||||
├── electron/ # Electron 主进程
|
||||
│ ├── main.cjs # 主入口
|
||||
│ └── bridges/ # IPC 桥接
|
||||
└── public/ # 静态资源 & 图标
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a name="构建与打包"></a>
|
||||
@@ -271,7 +250,7 @@ npm run pack
|
||||
# 为特定平台打包
|
||||
npm run pack:mac # macOS (DMG + ZIP)
|
||||
npm run pack:win # Windows (NSIS 安装程序)
|
||||
npm run pack:linux # Linux (AppImage, deb, rpm)
|
||||
npm run pack:linux # Linux (AppImage + DEB + RPM)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
flushQueuedTrayPanelConnectHostsImpl,
|
||||
handleConnectToHostImpl,
|
||||
handleTrayPanelConnectRequestImpl,
|
||||
} from './app/AppHandlers.ts';
|
||||
import type { Host } from '../types';
|
||||
|
||||
const baseHost: Host = {
|
||||
id: 'host-1',
|
||||
label: '10.2.0.32',
|
||||
hostname: '10.2.0.32',
|
||||
username: 'root',
|
||||
tags: [],
|
||||
os: 'linux',
|
||||
protocol: 'ssh',
|
||||
};
|
||||
|
||||
test('connect host handler returns the created terminal tab id', () => {
|
||||
const logs: unknown[] = [];
|
||||
const connectedHosts: Host[] = [];
|
||||
const result = handleConnectToHostImpl(
|
||||
() => ({
|
||||
addConnectionLog: (entry: unknown) => logs.push(entry),
|
||||
connectToHost: (host: Host) => {
|
||||
connectedHosts.push(host);
|
||||
return 'session-from-connect';
|
||||
},
|
||||
identities: [],
|
||||
keys: [],
|
||||
resolveEffectiveHost: (host: Host) => host,
|
||||
resolveHostAuth: () => ({ username: 'root' }),
|
||||
systemInfoRef: { current: { username: 'local-user', hostname: 'local-host' } },
|
||||
}),
|
||||
baseHost,
|
||||
);
|
||||
|
||||
assert.equal(result, 'session-from-connect');
|
||||
assert.equal(connectedHosts.length, 1);
|
||||
assert.equal(logs.length, 1);
|
||||
});
|
||||
|
||||
test('connect serial host handler returns the created terminal tab id', () => {
|
||||
const serialHost: Host = {
|
||||
...baseHost,
|
||||
id: 'serial-1',
|
||||
label: '',
|
||||
hostname: '/dev/tty.usbserial',
|
||||
protocol: 'serial',
|
||||
};
|
||||
|
||||
const result = handleConnectToHostImpl(
|
||||
() => ({
|
||||
addConnectionLog: () => {},
|
||||
connectToHost: () => 'serial-session',
|
||||
identities: [],
|
||||
keys: [],
|
||||
resolveEffectiveHost: (host: Host) => host,
|
||||
resolveHostAuth: () => ({ username: 'root' }),
|
||||
systemInfoRef: { current: { username: 'local-user', hostname: 'local-host' } },
|
||||
}),
|
||||
serialHost,
|
||||
);
|
||||
|
||||
assert.equal(result, 'serial-session');
|
||||
});
|
||||
|
||||
test('tray panel connect request queues until the vault is initialized', () => {
|
||||
const queuedHostIds: string[] = [];
|
||||
const connectedHostIds: string[] = [];
|
||||
|
||||
handleTrayPanelConnectRequestImpl(
|
||||
() => ({
|
||||
connectNow: (hostId: string) => connectedHostIds.push(hostId),
|
||||
isVaultInitialized: false,
|
||||
queueConnect: (hostId: string) => queuedHostIds.push(hostId),
|
||||
}),
|
||||
'host-1',
|
||||
);
|
||||
|
||||
assert.deepEqual(queuedHostIds, ['host-1']);
|
||||
assert.deepEqual(connectedHostIds, []);
|
||||
});
|
||||
|
||||
test('tray panel connect request runs immediately after the vault is initialized', () => {
|
||||
const queuedHostIds: string[] = [];
|
||||
const connectedHostIds: string[] = [];
|
||||
|
||||
handleTrayPanelConnectRequestImpl(
|
||||
() => ({
|
||||
connectNow: (hostId: string) => connectedHostIds.push(hostId),
|
||||
isVaultInitialized: true,
|
||||
queueConnect: (hostId: string) => queuedHostIds.push(hostId),
|
||||
}),
|
||||
'host-1',
|
||||
);
|
||||
|
||||
assert.deepEqual(queuedHostIds, []);
|
||||
assert.deepEqual(connectedHostIds, ['host-1']);
|
||||
});
|
||||
|
||||
test('queued tray panel connects flush in order', () => {
|
||||
const connectedHostIds: string[] = [];
|
||||
let pendingHostIds = ['host-1', 'host-2'];
|
||||
|
||||
flushQueuedTrayPanelConnectHostsImpl(() => ({
|
||||
connectNow: (hostId: string) => connectedHostIds.push(hostId),
|
||||
pendingHostIds,
|
||||
setPendingHostIds: (nextHostIds: string[]) => {
|
||||
pendingHostIds = nextHostIds;
|
||||
},
|
||||
}));
|
||||
|
||||
assert.deepEqual(connectedHostIds, ['host-1', 'host-2']);
|
||||
assert.deepEqual(pendingHostIds, []);
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { executeHotkeyActionImpl, getLogHostVisualSnapshot, handleGlobalHotkeyKeyDownImpl } from './app/AppHandlers.ts';
|
||||
import { matchesKeyBinding } from '../domain/models.ts';
|
||||
import { DEFAULT_KEY_BINDINGS } from '../domain/models/keyBindings.ts';
|
||||
|
||||
class FakeInputHTMLElement {
|
||||
tagName = 'INPUT';
|
||||
isContentEditable = false;
|
||||
|
||||
closest(): FakeInputHTMLElement | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeHTMLElement {
|
||||
tagName = 'TEXTAREA';
|
||||
isContentEditable = false;
|
||||
classList = {
|
||||
contains: (className: string) => className === 'xterm-helper-textarea',
|
||||
};
|
||||
|
||||
closest(selector: string): FakeHTMLElement | null {
|
||||
return selector.includes('xterm') ? this : null;
|
||||
}
|
||||
|
||||
hasAttribute(name: string): boolean {
|
||||
return name === 'data-session-id';
|
||||
}
|
||||
}
|
||||
|
||||
const previousHTMLElement = globalThis.HTMLElement;
|
||||
globalThis.HTMLElement = FakeHTMLElement as unknown as typeof HTMLElement;
|
||||
|
||||
test.after(() => {
|
||||
globalThis.HTMLElement = previousHTMLElement;
|
||||
});
|
||||
|
||||
test('global hotkey handler lets terminal font size shortcuts reach xterm', () => {
|
||||
const target = new FakeHTMLElement();
|
||||
const handledActions: string[] = [];
|
||||
let prevented = false;
|
||||
let stopped = false;
|
||||
const event = {
|
||||
key: '=',
|
||||
code: 'Equal',
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
target,
|
||||
composedPath: () => [target],
|
||||
preventDefault: () => {
|
||||
prevented = true;
|
||||
},
|
||||
stopPropagation: () => {
|
||||
stopped = true;
|
||||
},
|
||||
} as unknown as KeyboardEvent;
|
||||
|
||||
handleGlobalHotkeyKeyDownImpl(
|
||||
() => ({
|
||||
HOTKEY_DEBUG: false,
|
||||
closeTabKeyStr: 'Ctrl + W',
|
||||
executeHotkeyAction: (action: string) => {
|
||||
handledActions.push(action);
|
||||
},
|
||||
hotkeyScheme: 'pc',
|
||||
keyBindings: DEFAULT_KEY_BINDINGS,
|
||||
matchesKeyBinding,
|
||||
}),
|
||||
event,
|
||||
);
|
||||
|
||||
assert.deepEqual(handledActions, []);
|
||||
assert.equal(prevented, false);
|
||||
assert.equal(stopped, false);
|
||||
});
|
||||
|
||||
test('global hotkey handler routes quick switch through focused search inputs', () => {
|
||||
const target = new FakeInputHTMLElement();
|
||||
const handledActions: string[] = [];
|
||||
const event = {
|
||||
key: 'j',
|
||||
code: 'KeyJ',
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
target,
|
||||
composedPath: () => [target],
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
} as unknown as KeyboardEvent;
|
||||
|
||||
handleGlobalHotkeyKeyDownImpl(
|
||||
() => ({
|
||||
HOTKEY_DEBUG: false,
|
||||
closeTabKeyStr: 'Ctrl + W',
|
||||
executeHotkeyAction: (action: string) => {
|
||||
handledActions.push(action);
|
||||
},
|
||||
hotkeyScheme: 'pc',
|
||||
keyBindings: DEFAULT_KEY_BINDINGS,
|
||||
matchesKeyBinding,
|
||||
}),
|
||||
event,
|
||||
);
|
||||
|
||||
assert.deepEqual(handledActions, ['quickSwitch']);
|
||||
});
|
||||
|
||||
test('quick switch hotkey toggles the quick switcher open state', () => {
|
||||
let isQuickSwitcherOpen = false;
|
||||
const setIsQuickSwitcherOpen = (next: boolean) => {
|
||||
isQuickSwitcherOpen = next;
|
||||
};
|
||||
const noop = () => {};
|
||||
const baseCtx = {
|
||||
IS_DEV: false,
|
||||
MOVE_FOCUS_DEBOUNCE_MS: 0,
|
||||
activeTabStore: { getActiveTabId: () => 'vault' },
|
||||
addConnectionLogRef: { current: noop },
|
||||
closeSession: noop,
|
||||
closeTabInFlightRef: { current: false },
|
||||
closeWorkspace: noop,
|
||||
collectSessionIds: () => [],
|
||||
confirmIfBusyLocalTerminal: async () => true,
|
||||
createLocalTerminalWithCurrentShell: noop,
|
||||
editorTabs: [],
|
||||
fromEditorTabId: () => null,
|
||||
handleOpenSettingsRef: { current: noop },
|
||||
handleRequestCloseEditorTabRef: { current: noop },
|
||||
isEditorTabId: () => false,
|
||||
isQuickSwitcherOpen,
|
||||
lastMoveFocusTimeRef: { current: 0 },
|
||||
moveFocusInWorkspace: noop,
|
||||
orderedTabs: [],
|
||||
resolveCloseIntent: () => ({ kind: 'noop' }),
|
||||
resolveSnippetsShortcutIntent: () => ({ kind: 'noop' }),
|
||||
sessions: [],
|
||||
setActiveTabId: noop,
|
||||
setAddToWorkspaceDialog: noop,
|
||||
setIsQuickSwitcherOpen,
|
||||
setNavigateToSection: noop,
|
||||
settings: { showSftpTab: true, shellOnlyTabNumberShortcuts: false },
|
||||
splitSessionWithCurrentShell: noop,
|
||||
systemInfoRef: { current: { username: 'user', hostname: 'host' } },
|
||||
toEditorTabId: (id: string) => `editor:${id}`,
|
||||
toggleBroadcast: noop,
|
||||
toggleScriptsSidePanelRef: { current: noop },
|
||||
toggleSidePanelRef: { current: noop },
|
||||
workspaces: [],
|
||||
};
|
||||
|
||||
const event = {
|
||||
key: 'j',
|
||||
code: 'KeyJ',
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
} as KeyboardEvent;
|
||||
|
||||
executeHotkeyActionImpl(() => baseCtx, 'quickSwitch', event);
|
||||
assert.equal(isQuickSwitcherOpen, true);
|
||||
|
||||
executeHotkeyActionImpl(() => ({ ...baseCtx, isQuickSwitcherOpen: true }), 'quickSwitch', event);
|
||||
assert.equal(isQuickSwitcherOpen, false);
|
||||
});
|
||||
|
||||
test('next tab includes pinned tabs when shell-only shortcut mode is disabled', () => {
|
||||
let activeTabId = '';
|
||||
const noop = () => {};
|
||||
|
||||
executeHotkeyActionImpl(
|
||||
() => ({
|
||||
IS_DEV: false,
|
||||
MOVE_FOCUS_DEBOUNCE_MS: 0,
|
||||
activeTabStore: { getActiveTabId: () => 'vault' },
|
||||
addConnectionLogRef: { current: noop },
|
||||
closeSession: noop,
|
||||
closeTabInFlightRef: { current: false },
|
||||
closeWorkspace: noop,
|
||||
collectSessionIds: () => [],
|
||||
confirmIfBusyLocalTerminal: async () => true,
|
||||
createLocalTerminalWithCurrentShell: noop,
|
||||
editorTabs: [{ id: 'editor-1' }],
|
||||
fromEditorTabId: () => null,
|
||||
handleOpenSettingsRef: { current: noop },
|
||||
handleRequestCloseEditorTabRef: { current: noop },
|
||||
isEditorTabId: () => false,
|
||||
isQuickSwitcherOpen: false,
|
||||
lastMoveFocusTimeRef: { current: 0 },
|
||||
moveFocusInWorkspace: noop,
|
||||
orderedTabs: ['session-1'],
|
||||
resolveCloseIntent: () => ({ kind: 'noop' }),
|
||||
resolveSnippetsShortcutIntent: () => ({ kind: 'noop' }),
|
||||
sessions: [],
|
||||
setActiveTabId: (id: string) => { activeTabId = id; },
|
||||
setAddToWorkspaceDialog: noop,
|
||||
setIsQuickSwitcherOpen: noop,
|
||||
setNavigateToSection: noop,
|
||||
settings: { showSftpTab: true, shellOnlyTabNumberShortcuts: false },
|
||||
splitSessionWithCurrentShell: noop,
|
||||
systemInfoRef: { current: { username: 'user', hostname: 'host' } },
|
||||
toEditorTabId: (id: string) => `editor:${id}`,
|
||||
toggleBroadcast: noop,
|
||||
toggleScriptsSidePanelRef: { current: noop },
|
||||
toggleSidePanelRef: { current: noop },
|
||||
toggleWorkspaceViewMode: noop,
|
||||
workspaces: [],
|
||||
}),
|
||||
'nextTab',
|
||||
{ key: 'Tab', ctrlKey: true } as KeyboardEvent,
|
||||
);
|
||||
|
||||
assert.equal(activeTabId, 'sftp');
|
||||
});
|
||||
|
||||
test('next tab skips pinned tabs when shell-only shortcut mode is enabled', () => {
|
||||
let activeTabId = '';
|
||||
const noop = () => {};
|
||||
|
||||
executeHotkeyActionImpl(
|
||||
() => ({
|
||||
IS_DEV: false,
|
||||
MOVE_FOCUS_DEBOUNCE_MS: 0,
|
||||
activeTabStore: { getActiveTabId: () => 'vault' },
|
||||
addConnectionLogRef: { current: noop },
|
||||
closeSession: noop,
|
||||
closeTabInFlightRef: { current: false },
|
||||
closeWorkspace: noop,
|
||||
collectSessionIds: () => [],
|
||||
confirmIfBusyLocalTerminal: async () => true,
|
||||
createLocalTerminalWithCurrentShell: noop,
|
||||
editorTabs: [{ id: 'editor-1' }],
|
||||
fromEditorTabId: () => null,
|
||||
handleOpenSettingsRef: { current: noop },
|
||||
handleRequestCloseEditorTabRef: { current: noop },
|
||||
isEditorTabId: () => false,
|
||||
isQuickSwitcherOpen: false,
|
||||
lastMoveFocusTimeRef: { current: 0 },
|
||||
moveFocusInWorkspace: noop,
|
||||
orderedTabs: ['session-1'],
|
||||
resolveCloseIntent: () => ({ kind: 'noop' }),
|
||||
resolveSnippetsShortcutIntent: () => ({ kind: 'noop' }),
|
||||
sessions: [],
|
||||
setActiveTabId: (id: string) => { activeTabId = id; },
|
||||
setAddToWorkspaceDialog: noop,
|
||||
setIsQuickSwitcherOpen: noop,
|
||||
setNavigateToSection: noop,
|
||||
settings: { showSftpTab: true, shellOnlyTabNumberShortcuts: true },
|
||||
splitSessionWithCurrentShell: noop,
|
||||
systemInfoRef: { current: { username: 'user', hostname: 'host' } },
|
||||
toEditorTabId: (id: string) => `editor:${id}`,
|
||||
toggleBroadcast: noop,
|
||||
toggleScriptsSidePanelRef: { current: noop },
|
||||
toggleSidePanelRef: { current: noop },
|
||||
toggleWorkspaceViewMode: noop,
|
||||
workspaces: [],
|
||||
}),
|
||||
'nextTab',
|
||||
{ key: 'Tab', ctrlKey: true } as KeyboardEvent,
|
||||
);
|
||||
|
||||
assert.equal(activeTabId, 'session-1');
|
||||
});
|
||||
|
||||
test('connection log host snapshot includes custom host icon fields', () => {
|
||||
assert.deepEqual(
|
||||
getLogHostVisualSnapshot({
|
||||
id: 'host-1',
|
||||
label: 'Database',
|
||||
hostname: 'db.example.com',
|
||||
username: 'root',
|
||||
tags: [],
|
||||
os: 'linux',
|
||||
distro: 'ubuntu',
|
||||
iconMode: 'custom',
|
||||
iconId: 'database',
|
||||
iconColor: 'blue',
|
||||
}),
|
||||
{
|
||||
hostOs: 'linux',
|
||||
hostDistro: 'ubuntu',
|
||||
hostIconMode: 'custom',
|
||||
hostIconId: 'database',
|
||||
hostIconColorMode: 'manual',
|
||||
hostIconColor: 'blue',
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type { TerminalSession } from "../domain/models";
|
||||
import { copySessionToNewWindowWithCurrentShellImpl } from "./app/AppHandlers";
|
||||
|
||||
const sourceSession = (overrides: Partial<TerminalSession> = {}): TerminalSession => ({
|
||||
id: "session-1",
|
||||
hostId: "host-1",
|
||||
hostLabel: "Prod SSH",
|
||||
hostname: "prod.example.com",
|
||||
username: "deploy",
|
||||
status: "connected",
|
||||
protocol: "ssh",
|
||||
port: 22,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test("copySessionToNewWindowWithCurrentShellImpl asks Electron to open a peer window for the selected session", async () => {
|
||||
const openedPayloads: unknown[] = [];
|
||||
|
||||
await copySessionToNewWindowWithCurrentShellImpl(
|
||||
() => ({
|
||||
classifyLocalShellType: () => "zsh",
|
||||
discoveredShells: [],
|
||||
netcattyBridge: {
|
||||
get: () => ({
|
||||
openSessionInNewWindow: async (payload: unknown) => {
|
||||
openedPayloads.push(payload);
|
||||
return { success: true };
|
||||
},
|
||||
}),
|
||||
},
|
||||
resolveShellSetting: () => ({ command: "/bin/zsh" }),
|
||||
sessions: [sourceSession()],
|
||||
terminalSettings: { localShell: "system-default" },
|
||||
}),
|
||||
"session-1",
|
||||
);
|
||||
|
||||
assert.equal(openedPayloads.length, 1);
|
||||
assert.deepEqual(openedPayloads[0], {
|
||||
title: "Prod SSH",
|
||||
sourceSession: sourceSession(),
|
||||
localShellType: "zsh",
|
||||
});
|
||||
});
|
||||
|
||||
test("copySessionToNewWindowWithCurrentShellImpl preserves local start directory in the source session", async () => {
|
||||
const openedPayloads: unknown[] = [];
|
||||
const localSession = sourceSession({
|
||||
hostLabel: "Local Terminal",
|
||||
hostname: "localhost",
|
||||
protocol: "local",
|
||||
localStartDir: "/Users/alice/project with spaces ",
|
||||
});
|
||||
|
||||
await copySessionToNewWindowWithCurrentShellImpl(
|
||||
() => ({
|
||||
classifyLocalShellType: () => "zsh",
|
||||
discoveredShells: [],
|
||||
netcattyBridge: {
|
||||
get: () => ({
|
||||
openSessionInNewWindow: async (payload: unknown) => {
|
||||
openedPayloads.push(payload);
|
||||
return { success: true };
|
||||
},
|
||||
}),
|
||||
},
|
||||
resolveShellSetting: () => ({ command: "/bin/zsh" }),
|
||||
sessions: [localSession],
|
||||
terminalSettings: { localShell: "system-default" },
|
||||
}),
|
||||
"session-1",
|
||||
);
|
||||
|
||||
assert.equal(openedPayloads.length, 1);
|
||||
assert.deepEqual(openedPayloads[0], {
|
||||
title: "Local Terminal",
|
||||
sourceSession: localSession,
|
||||
localShellType: "zsh",
|
||||
});
|
||||
});
|
||||
|
||||
test("copySessionToNewWindowWithCurrentShellImpl does nothing when the source session is gone", async () => {
|
||||
let called = false;
|
||||
|
||||
await copySessionToNewWindowWithCurrentShellImpl(
|
||||
() => ({
|
||||
classifyLocalShellType: () => "zsh",
|
||||
discoveredShells: [],
|
||||
netcattyBridge: {
|
||||
get: () => ({
|
||||
openSessionInNewWindow: async () => {
|
||||
called = true;
|
||||
return { success: true };
|
||||
},
|
||||
}),
|
||||
},
|
||||
resolveShellSetting: () => ({ command: "/bin/zsh" }),
|
||||
sessions: [],
|
||||
terminalSettings: { localShell: "system-default" },
|
||||
}),
|
||||
"missing-session",
|
||||
);
|
||||
|
||||
assert.equal(called, false);
|
||||
});
|
||||
|
||||
test("copySessionToNewWindowWithCurrentShellImpl shows an error when Electron cannot open the window", async () => {
|
||||
const errors: string[] = [];
|
||||
|
||||
const result = await copySessionToNewWindowWithCurrentShellImpl(
|
||||
() => ({
|
||||
classifyLocalShellType: () => "zsh",
|
||||
discoveredShells: [],
|
||||
netcattyBridge: {
|
||||
get: () => ({
|
||||
openSessionInNewWindow: async () => ({ success: false }),
|
||||
}),
|
||||
},
|
||||
resolveShellSetting: () => ({ command: "/bin/zsh" }),
|
||||
sessions: [sourceSession()],
|
||||
terminalSettings: { localShell: "system-default" },
|
||||
t: (key: string) => key === "tabs.copyTabToNewWindowFailed" ? "Could not open" : key,
|
||||
toast: {
|
||||
error: (message: string) => errors.push(message),
|
||||
},
|
||||
}),
|
||||
"session-1",
|
||||
);
|
||||
|
||||
assert.equal(result, false);
|
||||
assert.deepEqual(errors, ["Could not open"]);
|
||||
});
|
||||
|
||||
test("copySessionToNewWindowWithCurrentShellImpl shows an error when the bridge is unavailable", async () => {
|
||||
const errors: string[] = [];
|
||||
|
||||
const result = await copySessionToNewWindowWithCurrentShellImpl(
|
||||
() => ({
|
||||
classifyLocalShellType: () => "zsh",
|
||||
discoveredShells: [],
|
||||
netcattyBridge: {
|
||||
get: () => ({}),
|
||||
},
|
||||
resolveShellSetting: () => ({ command: "/bin/zsh" }),
|
||||
sessions: [sourceSession()],
|
||||
terminalSettings: { localShell: "system-default" },
|
||||
t: (key: string) => key === "tabs.copyTabToNewWindowFailed" ? "Could not open" : key,
|
||||
toast: {
|
||||
error: (message: string) => errors.push(message),
|
||||
},
|
||||
}),
|
||||
"session-1",
|
||||
);
|
||||
|
||||
assert.equal(result, false);
|
||||
assert.deepEqual(errors, ["Could not open"]);
|
||||
});
|
||||
|
||||
test("copySessionToNewWindowWithCurrentShellImpl shows an error when the bridge throws", async () => {
|
||||
const errors: string[] = [];
|
||||
|
||||
const result = await copySessionToNewWindowWithCurrentShellImpl(
|
||||
() => ({
|
||||
classifyLocalShellType: () => "zsh",
|
||||
discoveredShells: [],
|
||||
netcattyBridge: {
|
||||
get: () => ({
|
||||
openSessionInNewWindow: async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
}),
|
||||
},
|
||||
resolveShellSetting: () => ({ command: "/bin/zsh" }),
|
||||
sessions: [sourceSession()],
|
||||
terminalSettings: { localShell: "system-default" },
|
||||
t: (key: string) => key === "tabs.copyTabToNewWindowFailed" ? "Could not open" : key,
|
||||
toast: {
|
||||
error: (message: string) => errors.push(message),
|
||||
},
|
||||
}),
|
||||
"session-1",
|
||||
);
|
||||
|
||||
assert.equal(result, false);
|
||||
assert.deepEqual(errors, ["Could not open"]);
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
fromEditorTabId,
|
||||
isEditorTabId,
|
||||
useActiveTabId,
|
||||
} from '../state/activeTabStore';
|
||||
import { updateActiveChromeThemeDeps } from '../state/activeChromeThemeSync';
|
||||
import { useActiveChromeTheme } from '../state/useActiveChromeTheme';
|
||||
import { netcattyBridge } from '../../infrastructure/services/netcattyBridge';
|
||||
import { resolveActiveChromeTheme } from './activeChromeTheme';
|
||||
import type { TerminalAppearanceHostScope, ResolvedAppearance } from '../../domain/terminalAppearanceRuntime';
|
||||
import type {
|
||||
Host,
|
||||
TerminalSession,
|
||||
TerminalTheme,
|
||||
Workspace,
|
||||
} from '../../types';
|
||||
import type { LogView } from '../state/logViewState';
|
||||
import type { EditorTab } from '../state/editorTabStore';
|
||||
|
||||
interface AppActiveTabChromeProps {
|
||||
showSftpTab: boolean;
|
||||
setActiveTabId: (id: string) => void;
|
||||
applyAppTheme: () => void;
|
||||
hostById: Map<string, Host>;
|
||||
sessionById: Map<string, TerminalSession>;
|
||||
themeById: Map<string, TerminalTheme>;
|
||||
workspaceById: Map<string, Workspace>;
|
||||
currentTerminalTheme: TerminalTheme;
|
||||
followAppTerminalTheme: boolean;
|
||||
accentMode: 'theme' | 'custom';
|
||||
customAccent: string;
|
||||
editorTabs: readonly EditorTab[];
|
||||
logViews: readonly LogView[];
|
||||
resolveSessionAppearance?: (hostScope: TerminalAppearanceHostScope) => ResolvedAppearance;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owns the `activeTabId` subscription and the purely side-effectful "chrome"
|
||||
* work derived from it: window title and the SFTP-tab guard.
|
||||
* Extracted out of <App> so that switching top tabs only
|
||||
* re-renders this null-rendering component (and the self-subscribing leaves)
|
||||
* instead of forcing the entire App tree (which holds all vault/session/
|
||||
* settings state and rebuilds the giant AppView ctx) to re-render.
|
||||
*/
|
||||
export function AppActiveTabChrome({
|
||||
showSftpTab,
|
||||
setActiveTabId,
|
||||
applyAppTheme,
|
||||
hostById,
|
||||
sessionById,
|
||||
themeById,
|
||||
workspaceById,
|
||||
currentTerminalTheme,
|
||||
followAppTerminalTheme,
|
||||
accentMode,
|
||||
customAccent,
|
||||
editorTabs,
|
||||
logViews,
|
||||
resolveSessionAppearance,
|
||||
t,
|
||||
}: AppActiveTabChromeProps) {
|
||||
const activeTabId = useActiveTabId();
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSftpTab && activeTabId === 'sftp') {
|
||||
setActiveTabId('vault');
|
||||
}
|
||||
}, [showSftpTab, activeTabId, setActiveTabId]);
|
||||
|
||||
const chromeThemeDeps = useMemo(() => ({
|
||||
accentMode,
|
||||
applyAppTheme,
|
||||
currentTerminalTheme,
|
||||
customAccent,
|
||||
editorTabs,
|
||||
followAppTerminalTheme,
|
||||
hostById,
|
||||
logViews,
|
||||
resolveSessionAppearance,
|
||||
sessionById,
|
||||
themeById,
|
||||
workspaceById,
|
||||
}), [
|
||||
accentMode,
|
||||
applyAppTheme,
|
||||
currentTerminalTheme,
|
||||
customAccent,
|
||||
editorTabs,
|
||||
followAppTerminalTheme,
|
||||
hostById,
|
||||
logViews,
|
||||
resolveSessionAppearance,
|
||||
sessionById,
|
||||
themeById,
|
||||
workspaceById,
|
||||
]);
|
||||
|
||||
updateActiveChromeThemeDeps(chromeThemeDeps);
|
||||
|
||||
const activeChromeTheme = useMemo(() => resolveActiveChromeTheme({
|
||||
...chromeThemeDeps,
|
||||
activeTabId,
|
||||
}), [chromeThemeDeps, activeTabId]);
|
||||
|
||||
useActiveChromeTheme({
|
||||
activeTheme: activeChromeTheme,
|
||||
applyAppTheme,
|
||||
});
|
||||
|
||||
const editorTabFileNameCounts = useMemo(() => {
|
||||
const counts = new Map<string, number>();
|
||||
for (const tab of editorTabs) counts.set(tab.fileName, (counts.get(tab.fileName) ?? 0) + 1);
|
||||
return counts;
|
||||
}, [editorTabs]);
|
||||
|
||||
const activeWindowTitle = useMemo(() => {
|
||||
if (activeTabId === 'vault') return 'Vaults';
|
||||
if (activeTabId === 'sftp') return 'SFTP';
|
||||
if (isEditorTabId(activeTabId)) {
|
||||
const editorTab = editorTabs.find((tab) => tab.id === fromEditorTabId(activeTabId));
|
||||
if (!editorTab) return 'Editor';
|
||||
const suffix = (editorTabFileNameCounts.get(editorTab.fileName) ?? 0) > 1
|
||||
? ` · ${editorTab.remotePath.split('/').slice(-2, -1)[0] || '/'}`
|
||||
: '';
|
||||
return `${editorTab.fileName}${suffix}`;
|
||||
}
|
||||
const workspace = workspaceById.get(activeTabId);
|
||||
if (workspace) return workspace.title;
|
||||
const session = sessionById.get(activeTabId);
|
||||
if (session) return session.hostLabel;
|
||||
const logView = logViews.find((item) => item.id === activeTabId);
|
||||
if (logView) {
|
||||
const isLocal = logView.log.protocol === 'local' || logView.log.hostname === 'localhost';
|
||||
return `${t('tabs.logPrefix')} ${isLocal ? t('tabs.logLocal') : logView.log.hostname}`;
|
||||
}
|
||||
return 'Netcatty';
|
||||
}, [activeTabId, editorTabFileNameCounts, editorTabs, logViews, sessionById, t, workspaceById]);
|
||||
|
||||
useEffect(() => {
|
||||
void netcattyBridge.get()?.setWindowTitle?.(activeWindowTitle);
|
||||
}, [activeWindowTitle]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const appSource = readFileSync(new URL("../../App.tsx", import.meta.url), "utf8");
|
||||
const appViewSource = readFileSync(new URL("./AppView.tsx", import.meta.url), "utf8");
|
||||
const runtimeSource = readFileSync(new URL("../state/useThemeRuntime.ts", import.meta.url), "utf8");
|
||||
const settingsSource = readFileSync(new URL("../state/useSettingsState.ts", import.meta.url), "utf8");
|
||||
|
||||
test("follow-app terminal theme selection updates the matching UI theme via ThemeRuntime", () => {
|
||||
assert.match(runtimeSource, /getFollowAppTerminalThemeSelectionUpdate\(themeId\)/);
|
||||
assert.match(runtimeSource, /setDarkUiThemeId\(update\.uiThemeId\)/);
|
||||
assert.match(runtimeSource, /setLightUiThemeId\(update\.uiThemeId\)/);
|
||||
assert.match(runtimeSource, /setTheme\(update\.appTheme\)/);
|
||||
assert.doesNotMatch(runtimeSource, /isFollowAppIntentSettled\(userIntent\.themeId/);
|
||||
assert.match(appSource, /useThemeRuntime\(/);
|
||||
assert.match(appSource, /themeRuntime\.pickTheme\(themeId\)/);
|
||||
assert.match(appSource, /useTerminalAppearanceInjection/);
|
||||
assert.match(appSource, /includeChromeSurfaces: followAppTerminalTheme/);
|
||||
assert.match(appSource, /clearThemeIntent\(\)/);
|
||||
assert.match(runtimeSource, /injectTerminalAppearanceVars\(appearance\.theme, \{ includeChromeSurfaces \}\)/);
|
||||
assert.doesNotMatch(settingsSource, /pendingFollowAppTerminalThemeId/);
|
||||
assert.doesNotMatch(settingsSource, /applyFollowAppTerminalThemePick/);
|
||||
assert.match(settingsSource, /appearanceTransitionModeRef\.current = 'instant'/);
|
||||
assert.match(appViewSource, /data-terminal-appearance-root/);
|
||||
assert.match(appViewSource, /pickTerminalTheme=\{ctx\.pickTerminalTheme\}/);
|
||||
});
|
||||
|
||||
test("default terminal theme selection clears the current mode override", () => {
|
||||
assert.match(appSource, /const handleDefaultTerminalThemeChange = useCallback\(\(themeId: string\) => \{/);
|
||||
assert.match(appSource, /setTerminalThemeId\(themeId\)/);
|
||||
assert.match(appSource, /resolvedTheme === 'dark'[\s\S]*setTerminalThemeDarkId\(TERMINAL_THEME_AUTO\)/);
|
||||
assert.match(appSource, /setTerminalThemeLightId\(TERMINAL_THEME_AUTO\)/);
|
||||
assert.match(appViewSource, /onUpdateTerminalThemeId=\{handleDefaultTerminalThemeChange\}/);
|
||||
});
|
||||
@@ -0,0 +1,956 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type React from 'react';
|
||||
import type { Host, HostProtocol } from '../../types';
|
||||
import type { PassphraseRequest } from '../../components/PassphraseModal';
|
||||
import { getEffectiveHostDistro } from '../../domain/host';
|
||||
import { sanitizeHostIconFields } from '../../domain/hostIcon';
|
||||
import { getTerminalPassthroughActions } from '../state/useGlobalHotkeys';
|
||||
import { buildNumberShortcutTabTargets } from './tabShortcutTargets';
|
||||
|
||||
type AppContextGetter = () => Record<string, any>;
|
||||
const TERMINAL_PASSTHROUGH_ACTIONS = getTerminalPassthroughActions();
|
||||
|
||||
export const getLogHostVisualSnapshot = (host: Host) => {
|
||||
const icon = sanitizeHostIconFields(host);
|
||||
return {
|
||||
hostOs: host.os,
|
||||
hostDistro: getEffectiveHostDistro(host) || undefined,
|
||||
hostIconMode: icon.iconMode,
|
||||
hostIconId: icon.iconId,
|
||||
...(icon.iconColorMode ? { hostIconColorMode: icon.iconColorMode } : {}),
|
||||
...(icon.iconColor ? { hostIconColor: icon.iconColor } : {}),
|
||||
...(icon.iconColorCustom ? { hostIconColorCustom: icon.iconColorCustom } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
export function handleTrayJumpToSessionImpl(getCtx: AppContextGetter, sessionId: string) {
|
||||
const { sessions, setActiveTabId, setWorkspaceFocusedSession } = getCtx();
|
||||
{
|
||||
const session = sessions.find((item) => item.id === sessionId);
|
||||
if (session?.workspaceId) {
|
||||
setActiveTabId(session.workspaceId);
|
||||
setWorkspaceFocusedSession(session.workspaceId, sessionId);
|
||||
return;
|
||||
}
|
||||
setActiveTabId(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleTrayTogglePortForwardImpl(getCtx: AppContextGetter, ruleId: string, start: boolean) {
|
||||
const { hosts, identities, keys, knownHosts, portForwardingRules, resolveEffectiveHost, startTunnel, stopTunnel, t, terminalSettings, toast } = getCtx();
|
||||
{
|
||||
const rule = portForwardingRules.find((item) => item.id === ruleId);
|
||||
if (!rule) return;
|
||||
const host = rule.hostId ? hosts.find((item) => item.id === rule.hostId) : undefined;
|
||||
if (!host) {
|
||||
toast.error(t("pf.error.hostNotFound"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (start) {
|
||||
const effectiveHost = resolveEffectiveHost(host);
|
||||
void startTunnel(rule, effectiveHost, hosts.map(resolveEffectiveHost), keys, identities, (status, error) => {
|
||||
if (status === "error" && error) toast.error(error);
|
||||
}, rule.autoStart, terminalSettings, knownHosts);
|
||||
return;
|
||||
}
|
||||
|
||||
void stopTunnel(ruleId);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleTrayPanelConnectImpl(getCtx: AppContextGetter, hostId: string) {
|
||||
const { addConnectionLog, connectToHost, hosts, identities, keys, resolveEffectiveHost, resolveHostAuth, systemInfoRef, t, toast } = getCtx();
|
||||
{
|
||||
const host = hosts.find((item) => item.id === hostId);
|
||||
if (!host) {
|
||||
toast.error(t("pf.error.hostNotFound"));
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveHost = resolveEffectiveHost(host);
|
||||
|
||||
const { username, hostname: localHost } = systemInfoRef.current;
|
||||
if (effectiveHost.protocol === 'serial') {
|
||||
const portName = host.hostname.split('/').pop() || host.hostname;
|
||||
const sessionId = connectToHost(effectiveHost);
|
||||
addConnectionLog({
|
||||
sessionId,
|
||||
hostId: host.id,
|
||||
hostLabel: host.label || `Serial: ${portName}`,
|
||||
hostname: host.hostname,
|
||||
username,
|
||||
protocol: 'serial',
|
||||
...getLogHostVisualSnapshot(effectiveHost),
|
||||
startTime: Date.now(),
|
||||
localUsername: username,
|
||||
localHostname: localHost,
|
||||
saved: false,
|
||||
});
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
const protocol = effectiveHost.etEnabled ? 'et' : effectiveHost.moshEnabled ? 'mosh' : (effectiveHost.protocol || 'ssh');
|
||||
const resolvedAuth = resolveHostAuth({ host: effectiveHost, keys, identities });
|
||||
const sessionId = connectToHost(effectiveHost);
|
||||
addConnectionLog({
|
||||
sessionId,
|
||||
hostId: host.id,
|
||||
hostLabel: host.label,
|
||||
hostname: host.hostname,
|
||||
username: resolvedAuth.username || 'root',
|
||||
protocol: protocol as 'ssh' | 'telnet' | 'local' | 'mosh' | 'et',
|
||||
...getLogHostVisualSnapshot(effectiveHost),
|
||||
startTime: Date.now(),
|
||||
localUsername: username,
|
||||
localHostname: localHost,
|
||||
saved: false,
|
||||
});
|
||||
return sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
export function handleTrayPanelConnectRequestImpl(getCtx: AppContextGetter, hostId: string) {
|
||||
const { connectNow, isVaultInitialized, queueConnect } = getCtx();
|
||||
if (!hostId) return;
|
||||
if (!isVaultInitialized) {
|
||||
queueConnect(hostId);
|
||||
return;
|
||||
}
|
||||
connectNow(hostId);
|
||||
}
|
||||
|
||||
export function flushQueuedTrayPanelConnectHostsImpl(getCtx: AppContextGetter) {
|
||||
const { connectNow, pendingHostIds, setPendingHostIds } = getCtx();
|
||||
if (!Array.isArray(pendingHostIds) || pendingHostIds.length === 0) return;
|
||||
const hostIds = [...pendingHostIds];
|
||||
setPendingHostIds([]);
|
||||
hostIds.forEach((hostId: string) => connectNow(hostId));
|
||||
}
|
||||
|
||||
export function handleGlobalHotkeyKeyDownImpl(getCtx: AppContextGetter, e: KeyboardEvent) {
|
||||
const { HOTKEY_DEBUG, closeTabKeyStr, executeHotkeyAction, hotkeyScheme, keyBindings, matchesKeyBinding } = getCtx();
|
||||
{
|
||||
const isMac = hotkeyScheme === 'mac';
|
||||
const target = e.target as HTMLElement;
|
||||
const isCloseTabHotkey = closeTabKeyStr ? matchesKeyBinding(e, closeTabKeyStr, isMac) : false;
|
||||
const dialogHotkeyScope = target.closest?.('[data-hotkey-close-tab="true"]');
|
||||
|
||||
if (isCloseTabHotkey && dialogHotkeyScope) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCloseTabHotkey) {
|
||||
const openDialogs = Array.from(document.querySelectorAll<HTMLElement>('[role="dialog"][data-state="open"]'));
|
||||
const topmostOpenDialog = openDialogs[openDialogs.length - 1] ?? null;
|
||||
const topmostDialogClose = topmostOpenDialog?.querySelector<HTMLElement>('[data-dialog-close="true"]');
|
||||
if (topmostDialogClose) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
topmostDialogClose.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const isFormElement = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
||||
const isMonacoElement =
|
||||
target instanceof HTMLElement &&
|
||||
!!target.closest?.('.monaco-editor, .monaco-diff-editor, .monaco-inputbox');
|
||||
const isXtermInput =
|
||||
target instanceof HTMLElement &&
|
||||
!!target.closest?.(".xterm, .xterm-helper-textarea, .xterm-screen, .xterm-viewport");
|
||||
|
||||
const quickSwitchBinding = keyBindings.find((binding) => binding.action === 'quickSwitch');
|
||||
const quickSwitchKeyStr = quickSwitchBinding ? (isMac ? quickSwitchBinding.mac : quickSwitchBinding.pc) : null;
|
||||
const isQuickSwitchHotkey = quickSwitchKeyStr ? matchesKeyBinding(e, quickSwitchKeyStr, isMac) : false;
|
||||
|
||||
if ((isFormElement || isMonacoElement) && !isXtermInput && e.key !== 'Escape' && !isQuickSwitchHotkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isTerminalElement =
|
||||
target instanceof HTMLElement &&
|
||||
!!target.closest?.(".xterm, .xterm-helper-textarea, .xterm-screen, .xterm-viewport");
|
||||
const isTerminalInPath = Boolean(
|
||||
e.composedPath?.().some(
|
||||
(node) =>
|
||||
node instanceof HTMLElement &&
|
||||
(node.classList.contains("xterm") ||
|
||||
node.classList.contains("xterm-helper-textarea") ||
|
||||
node.classList.contains("xterm-screen") ||
|
||||
node.classList.contains("xterm-viewport") ||
|
||||
node.hasAttribute("data-session-id")),
|
||||
),
|
||||
);
|
||||
|
||||
for (const binding of keyBindings) {
|
||||
const keyStr = isMac ? binding.mac : binding.pc;
|
||||
if (!matchesKeyBinding(e, keyStr, isMac)) continue;
|
||||
if (HOTKEY_DEBUG) console.log('[Hotkeys] Matched binding:', binding.action, keyStr);
|
||||
if (binding.category === 'sftp') {
|
||||
continue;
|
||||
}
|
||||
if (TERMINAL_PASSTHROUGH_ACTIONS.has(binding.action)) {
|
||||
if (isTerminalElement) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (HOTKEY_DEBUG) {
|
||||
console.log('[Hotkeys] Global handle', {
|
||||
action: binding.action,
|
||||
key: e.key,
|
||||
meta: e.metaKey,
|
||||
ctrl: e.ctrlKey,
|
||||
alt: e.altKey,
|
||||
shift: e.shiftKey,
|
||||
targetTag: target?.tagName,
|
||||
isTerminalElement,
|
||||
isTerminalInPath,
|
||||
});
|
||||
}
|
||||
executeHotkeyAction(binding.action, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleEscapeKeyDownImpl(getCtx: AppContextGetter, e: KeyboardEvent) {
|
||||
const { isQuickSwitcherOpen, setIsQuickSwitcherOpen } = getCtx();
|
||||
{
|
||||
if (e.key === 'Escape' && isQuickSwitcherOpen) {
|
||||
setIsQuickSwitcherOpen(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleKeyboardInteractiveSubmitImpl(getCtx: AppContextGetter, requestId: string, responses: string[], savePassword?: string) {
|
||||
const { hosts, keyboardInteractiveQueue, netcattyBridge, sessions, setKeyboardInteractiveQueue, updateHosts } = getCtx();
|
||||
{
|
||||
const bridge = netcattyBridge.get();
|
||||
if (bridge?.respondKeyboardInteractive) {
|
||||
void bridge.respondKeyboardInteractive(requestId, responses, false);
|
||||
}
|
||||
// Save password to host if requested
|
||||
if (savePassword) {
|
||||
const request = keyboardInteractiveQueue.find(r => r.requestId === requestId);
|
||||
if (request?.sessionId) {
|
||||
const session = sessions.find(s => s.id === request.sessionId);
|
||||
// Only save when the prompting hostname matches the session's host,
|
||||
// to avoid overwriting the destination host's password with a jump host's password
|
||||
if (session?.hostId && (!request.hostname || request.hostname === session.hostname)) {
|
||||
const host = hosts.find(h => h.id === session.hostId);
|
||||
if (host) {
|
||||
updateHosts(hosts.map(h => h.id === host.id ? { ...h, password: savePassword, savePassword: true } : h));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove from queue by requestId
|
||||
setKeyboardInteractiveQueue(prev => prev.filter(r => r.requestId !== requestId));
|
||||
}
|
||||
}
|
||||
|
||||
export function handleKeyboardInteractiveCancelImpl(getCtx: AppContextGetter, requestId: string) {
|
||||
const { netcattyBridge, setKeyboardInteractiveQueue } = getCtx();
|
||||
{
|
||||
const bridge = netcattyBridge.get();
|
||||
if (bridge?.respondKeyboardInteractive) {
|
||||
void bridge.respondKeyboardInteractive(requestId, [], true);
|
||||
}
|
||||
// Remove from queue by requestId
|
||||
setKeyboardInteractiveQueue(prev => prev.filter(r => r.requestId !== requestId));
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlePassphraseSubmitImpl(getCtx: AppContextGetter, requestId: string, passphrase: string, remember: boolean) {
|
||||
const { keysRef, netcattyBridge, passphraseQueue, rememberKeyPassphrase, setPassphraseQueue, updateKeys } = getCtx();
|
||||
{
|
||||
const bridge = netcattyBridge.get();
|
||||
const request = passphraseQueue.find((r: PassphraseRequest) => r.requestId === requestId);
|
||||
|
||||
// Save passphrase if requested
|
||||
if (remember && request?.keyPath) {
|
||||
console.log('[App] Saving passphrase for:', request.keyPath);
|
||||
try {
|
||||
await rememberKeyPassphrase({
|
||||
keyPath: request.keyPath,
|
||||
passphrase,
|
||||
keys: keysRef.current,
|
||||
updateKeys,
|
||||
setCurrentKeys: (updated) => {
|
||||
keysRef.current = updated;
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[App] Failed to save passphrase:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (bridge?.respondPassphrase) {
|
||||
void bridge.respondPassphrase(requestId, passphrase, false);
|
||||
}
|
||||
|
||||
setPassphraseQueue(prev => prev.filter(r => r.requestId !== requestId));
|
||||
}
|
||||
}
|
||||
|
||||
export function handlePassphraseCancelImpl(getCtx: AppContextGetter, requestId: string) {
|
||||
const { netcattyBridge, setPassphraseQueue } = getCtx();
|
||||
{
|
||||
const bridge = netcattyBridge.get();
|
||||
if (bridge?.respondPassphrase) {
|
||||
// Cancel = stop the entire passphrase flow
|
||||
void bridge.respondPassphrase(requestId, '', true);
|
||||
}
|
||||
setPassphraseQueue(prev => prev.filter(r => r.requestId !== requestId));
|
||||
}
|
||||
}
|
||||
|
||||
export function handlePassphraseSkipImpl(getCtx: AppContextGetter, requestId: string) {
|
||||
const { netcattyBridge, setPassphraseQueue } = getCtx();
|
||||
{
|
||||
const bridge = netcattyBridge.get();
|
||||
if (bridge?.respondPassphraseSkip) {
|
||||
// Skip = skip this key but continue asking for others
|
||||
void bridge.respondPassphraseSkip(requestId);
|
||||
} else if (bridge?.respondPassphrase) {
|
||||
// Fallback for older API
|
||||
void bridge.respondPassphrase(requestId, '', false);
|
||||
}
|
||||
setPassphraseQueue(prev => prev.filter(r => r.requestId !== requestId));
|
||||
}
|
||||
}
|
||||
|
||||
export function createLocalTerminalWithCurrentShellImpl(getCtx: AppContextGetter) {
|
||||
const { classifyLocalShellType, createLocalTerminal, discoveredShells, resolveShellSetting, terminalSettings } = getCtx();
|
||||
{
|
||||
const resolved = resolveShellSetting(terminalSettings.localShell, discoveredShells, terminalSettings.localShellArgs);
|
||||
const matchedShell = discoveredShells.find(s => s.id === terminalSettings.localShell);
|
||||
return createLocalTerminal({
|
||||
shellType: classifyLocalShellType(resolved?.command || terminalSettings.localShell, navigator.userAgent),
|
||||
shell: resolved?.command,
|
||||
shellArgs: resolved?.args,
|
||||
shellName: matchedShell?.name,
|
||||
shellIcon: matchedShell?.icon,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function splitSessionWithCurrentShellImpl(getCtx: AppContextGetter, sessionId: string, direction: 'horizontal' | 'vertical') {
|
||||
const { classifyLocalShellType, discoveredShells, resolveShellSetting, splitSession, terminalSettings } = getCtx();
|
||||
{
|
||||
const resolved = resolveShellSetting(terminalSettings.localShell, discoveredShells);
|
||||
return splitSession(sessionId, direction, {
|
||||
localShellType: classifyLocalShellType(resolved?.command || terminalSettings.localShell, navigator.userAgent),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function copySessionWithCurrentShellImpl(getCtx: AppContextGetter, sessionId: string) {
|
||||
const { classifyLocalShellType, copySession, discoveredShells, resolveShellSetting, terminalSettings } = getCtx();
|
||||
{
|
||||
const resolved = resolveShellSetting(terminalSettings.localShell, discoveredShells);
|
||||
return copySession(sessionId, {
|
||||
localShellType: classifyLocalShellType(resolved?.command || terminalSettings.localShell, navigator.userAgent),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function copySessionToNewWindowWithCurrentShellImpl(getCtx: AppContextGetter, sessionId: string) {
|
||||
const { classifyLocalShellType, discoveredShells, netcattyBridge, resolveShellSetting, sessions, terminalSettings, t, toast } = getCtx();
|
||||
{
|
||||
const sourceSession = sessions.find((session: { id: string }) => session.id === sessionId);
|
||||
if (!sourceSession) return false;
|
||||
|
||||
const resolved = resolveShellSetting(terminalSettings.localShell, discoveredShells);
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.openSessionInNewWindow) {
|
||||
toast?.error?.(t?.('tabs.copyTabToNewWindowFailed') ?? 'Failed to open tab in a new window');
|
||||
return false;
|
||||
}
|
||||
|
||||
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
|
||||
try {
|
||||
const result = await bridge.openSessionInNewWindow({
|
||||
title: sourceSession.hostLabel,
|
||||
sourceSession,
|
||||
localShellType: classifyLocalShellType(resolved?.command || terminalSettings.localShell, userAgent),
|
||||
});
|
||||
const success = result?.success === true;
|
||||
if (!success) toast?.error?.(t?.('tabs.copyTabToNewWindowFailed') ?? 'Failed to open tab in a new window');
|
||||
return success;
|
||||
} catch {
|
||||
toast?.error?.(t?.('tabs.copyTabToNewWindowFailed') ?? 'Failed to open tab in a new window');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function confirmIfBusyLocalTerminalImpl(getCtx: AppContextGetter, sessionIds: string[]) {
|
||||
const { netcattyBridge, sessions, t } = getCtx();
|
||||
{
|
||||
const bridge = netcattyBridge.get();
|
||||
const localIds = sessionIds.filter((id) => {
|
||||
const s = sessions.find((x) => x.id === id);
|
||||
return s?.protocol === 'local';
|
||||
});
|
||||
const busyCommands: string[] = [];
|
||||
for (const id of localIds) {
|
||||
const children = (await bridge?.ptyGetChildProcesses?.(id)) ?? [];
|
||||
if (children.length > 0) {
|
||||
busyCommands.push(children[0].command);
|
||||
}
|
||||
}
|
||||
if (busyCommands.length === 0) return true;
|
||||
|
||||
const primary = busyCommands[0];
|
||||
const extraCount = busyCommands.length - 1;
|
||||
const message =
|
||||
extraCount > 0
|
||||
? t('confirm.closeBusyTerminal.messageWithMore', {
|
||||
command: primary,
|
||||
count: extraCount,
|
||||
})
|
||||
: t('confirm.closeBusyTerminal.message', { command: primary });
|
||||
|
||||
const ok = await bridge?.confirmCloseBusy?.({
|
||||
command: primary,
|
||||
title: t('confirm.closeBusyTerminal.title'),
|
||||
message,
|
||||
cancelLabel: t('confirm.closeBusyTerminal.cancel'),
|
||||
closeLabel: t('confirm.closeBusyTerminal.close'),
|
||||
});
|
||||
return ok === true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeTabsBatchImpl(getCtx: AppContextGetter, targetIds: string[]) {
|
||||
const { closeLogView, closeSession, closeTabsInFlightRef, closeWorkspace, confirmIfBusyLocalTerminal, logViews, sessions, workspaces } = getCtx();
|
||||
{
|
||||
if (targetIds.length === 0) return;
|
||||
if (closeTabsInFlightRef.current) return;
|
||||
|
||||
// Expand workspace ids into their constituent session ids so the busy
|
||||
// probe sees every local shell that's about to be killed.
|
||||
const sessionIdsToProbe: string[] = [];
|
||||
for (const tabId of targetIds) {
|
||||
const ws = workspaces.find((w) => w.id === tabId);
|
||||
if (ws) {
|
||||
for (const s of sessions) {
|
||||
if (s.workspaceId === tabId) sessionIdsToProbe.push(s.id);
|
||||
}
|
||||
} else if (sessions.find((s) => s.id === tabId)) {
|
||||
sessionIdsToProbe.push(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
closeTabsInFlightRef.current = true;
|
||||
try {
|
||||
const ok = await confirmIfBusyLocalTerminal(sessionIdsToProbe);
|
||||
if (!ok) return;
|
||||
for (const tabId of targetIds) {
|
||||
if (workspaces.find((w) => w.id === tabId)) {
|
||||
closeWorkspace(tabId);
|
||||
} else if (sessions.find((s) => s.id === tabId)) {
|
||||
closeSession(tabId);
|
||||
} else if (logViews.find((lv) => lv.id === tabId)) {
|
||||
closeLogView(tabId);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
closeTabsInFlightRef.current = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function executeHotkeyActionImpl(getCtx: AppContextGetter, action: string, e: KeyboardEvent) {
|
||||
const { IS_DEV, MOVE_FOCUS_DEBOUNCE_MS, activeTabStore, addConnectionLogRef, closeSession, closeTabInFlightRef, closeWorkspace, collectSessionIds, confirmIfBusyLocalTerminal, createLocalTerminalWithCurrentShell, editorTabs, fromEditorTabId, handleOpenSettingsRef, handleRequestCloseEditorTabRef, isEditorTabId, isQuickSwitcherOpen, lastMoveFocusTimeRef, moveFocusInWorkspace, orderedTabs, resolveCloseIntent, resolveSnippetsShortcutIntent, sessions, setActiveTabId, setAddToWorkspaceDialog, setIsQuickSwitcherOpen, setNavigateToSection, settings, splitSessionWithCurrentShell, systemInfoRef, toEditorTabId, toggleBroadcast, toggleScriptsSidePanelRef, toggleSidePanelRef, toggleWorkspaceViewMode, workspaces } = getCtx();
|
||||
{
|
||||
const shortcutTabs = buildNumberShortcutTabTargets({
|
||||
showSftpTab: settings.showSftpTab ?? true,
|
||||
shellOnlyTabNumberShortcuts: settings.shellOnlyTabNumberShortcuts ?? false,
|
||||
orderedTabs,
|
||||
editorTabIds: editorTabs.map((t) => toEditorTabId(t.id)),
|
||||
});
|
||||
switch (action) {
|
||||
case 'switchToTab': {
|
||||
// Get the number key pressed (1-9)
|
||||
const num = parseInt(e.key, 10);
|
||||
if (num >= 1 && num <= 9) {
|
||||
if (num <= shortcutTabs.length) {
|
||||
setActiveTabId(shortcutTabs[num - 1]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'nextTab': {
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
const currentIdx = shortcutTabs.indexOf(currentId);
|
||||
if (currentIdx !== -1 && shortcutTabs.length > 0) {
|
||||
const nextIdx = (currentIdx + 1) % shortcutTabs.length;
|
||||
setActiveTabId(shortcutTabs[nextIdx]);
|
||||
} else if (shortcutTabs.length > 0) {
|
||||
setActiveTabId(shortcutTabs[0]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'prevTab': {
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
const currentIdx = shortcutTabs.indexOf(currentId);
|
||||
if (currentIdx !== -1 && shortcutTabs.length > 0) {
|
||||
const prevIdx = (currentIdx - 1 + shortcutTabs.length) % shortcutTabs.length;
|
||||
setActiveTabId(shortcutTabs[prevIdx]);
|
||||
} else if (shortcutTabs.length > 0) {
|
||||
setActiveTabId(shortcutTabs[shortcutTabs.length - 1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'closeTab': {
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
if (!currentId || currentId === 'vault' || currentId === 'sftp') break;
|
||||
if (closeTabInFlightRef.current) break;
|
||||
|
||||
// Editor tabs route through their own dirty-confirm close flow.
|
||||
if (isEditorTabId(currentId)) {
|
||||
const editorId = fromEditorTabId(currentId);
|
||||
if (editorId) handleRequestCloseEditorTabRef.current(editorId);
|
||||
break;
|
||||
}
|
||||
|
||||
const session = sessions.find((s) => s.id === currentId) ?? null;
|
||||
const workspace = workspaces.find((w) => w.id === currentId) ?? null;
|
||||
|
||||
const focusIsInsideTerminal = !!document.activeElement?.closest('[data-session-id]');
|
||||
|
||||
const intent = resolveCloseIntent({
|
||||
activeTabId: currentId,
|
||||
workspace: workspace ? { id: workspace.id, focusedSessionId: workspace.focusedSessionId } : null,
|
||||
sessionForTab: session,
|
||||
focusIsInsideTerminal,
|
||||
});
|
||||
|
||||
closeTabInFlightRef.current = true;
|
||||
(async () => {
|
||||
try {
|
||||
switch (intent.kind) {
|
||||
case 'closeTerminal':
|
||||
case 'closeSingleTab': {
|
||||
const ok = await confirmIfBusyLocalTerminal([intent.sessionId]);
|
||||
if (ok) closeSession(intent.sessionId);
|
||||
return;
|
||||
}
|
||||
case 'closeWorkspace': {
|
||||
const ids = sessions.filter((s) => s.workspaceId === intent.workspaceId).map((s) => s.id);
|
||||
const ok = await confirmIfBusyLocalTerminal(ids);
|
||||
if (ok) closeWorkspace(intent.workspaceId);
|
||||
return;
|
||||
}
|
||||
case 'noop':
|
||||
default:
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
closeTabInFlightRef.current = false;
|
||||
}
|
||||
})();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'closeSession': {
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
if (!currentId || currentId === 'vault' || currentId === 'sftp') break;
|
||||
if (closeTabInFlightRef.current) break;
|
||||
|
||||
const session = sessions.find((s) => s.id === currentId) ?? null;
|
||||
const workspace = workspaces.find((w) => w.id === currentId) ?? null;
|
||||
|
||||
closeTabInFlightRef.current = true;
|
||||
(async () => {
|
||||
try {
|
||||
// If active tab is a workspace, close the focused session (pane)
|
||||
if (workspace) {
|
||||
// Validate focusedSessionId is still valid — it can become stale
|
||||
// if the previously focused session was already closed
|
||||
const aliveIds = collectSessionIds(workspace.root);
|
||||
const focusedId = aliveIds.includes(workspace.focusedSessionId)
|
||||
? workspace.focusedSessionId
|
||||
: aliveIds[0];
|
||||
if (focusedId) {
|
||||
const ok = await confirmIfBusyLocalTerminal([focusedId]);
|
||||
if (ok) closeSession(focusedId);
|
||||
}
|
||||
} else if (session) {
|
||||
// Standalone session tab — close the session
|
||||
const ok = await confirmIfBusyLocalTerminal([session.id]);
|
||||
if (ok) closeSession(session.id);
|
||||
}
|
||||
} finally {
|
||||
closeTabInFlightRef.current = false;
|
||||
}
|
||||
})();
|
||||
break;
|
||||
}
|
||||
case 'newTab':
|
||||
case 'openLocal': {
|
||||
const sessionId = createLocalTerminalWithCurrentShell();
|
||||
addConnectionLogRef.current({
|
||||
sessionId,
|
||||
hostId: '',
|
||||
hostLabel: 'Local Terminal',
|
||||
hostname: 'localhost',
|
||||
username: systemInfoRef.current.username,
|
||||
protocol: 'local',
|
||||
startTime: Date.now(),
|
||||
localUsername: systemInfoRef.current.username,
|
||||
localHostname: systemInfoRef.current.hostname,
|
||||
saved: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'openHosts':
|
||||
setActiveTabId('vault');
|
||||
break;
|
||||
case 'openSftp':
|
||||
if (settings.showSftpTab) {
|
||||
setActiveTabId('sftp');
|
||||
}
|
||||
break;
|
||||
case 'quickSwitch':
|
||||
setIsQuickSwitcherOpen(!isQuickSwitcherOpen);
|
||||
break;
|
||||
case 'commandPalette':
|
||||
setIsQuickSwitcherOpen(true);
|
||||
break;
|
||||
case 'newWorkspace':
|
||||
// Dedicated shortcut to launch the AddToWorkspaceDialog in
|
||||
// create mode — same entry as QuickSwitcher's "New Workspace"
|
||||
// button, but without having to open QS first.
|
||||
setAddToWorkspaceDialog({ mode: 'create' });
|
||||
break;
|
||||
case 'portForwarding':
|
||||
// Navigate to vault and open port forwarding section
|
||||
setActiveTabId('vault');
|
||||
setNavigateToSection('port');
|
||||
break;
|
||||
case 'snippets':
|
||||
{
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
const intent = resolveSnippetsShortcutIntent({
|
||||
activeTabId: currentId,
|
||||
sessionForTab: sessions.find((s) => s.id === currentId) ?? null,
|
||||
workspaceForTab: workspaces.find((w) => w.id === currentId) ?? null,
|
||||
terminalScriptsToggleAvailable: !!toggleScriptsSidePanelRef.current,
|
||||
});
|
||||
|
||||
if (intent.kind === 'toggleTerminalScripts') {
|
||||
toggleScriptsSidePanelRef.current();
|
||||
break;
|
||||
}
|
||||
|
||||
setActiveTabId('vault');
|
||||
setNavigateToSection('snippets');
|
||||
}
|
||||
break;
|
||||
case 'toggleSidePanel':
|
||||
toggleSidePanelRef.current?.();
|
||||
break;
|
||||
case 'broadcast': {
|
||||
// Toggle broadcast mode for the active workspace
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
const activeWs = workspaces.find(w => w.id === currentId);
|
||||
if (activeWs) {
|
||||
toggleBroadcast(activeWs.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'openSettings':
|
||||
handleOpenSettingsRef.current();
|
||||
break;
|
||||
case 'splitHorizontal': {
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
const activeSession = sessions.find(s => s.id === currentId);
|
||||
const activeWs = workspaces.find(w => w.id === currentId);
|
||||
if (activeSession && !activeSession.workspaceId) {
|
||||
splitSessionWithCurrentShell(activeSession.id, 'horizontal');
|
||||
} else if (activeWs) {
|
||||
const liveIds = collectSessionIds(activeWs.root);
|
||||
const targetId = (activeWs.focusedSessionId && liveIds.includes(activeWs.focusedSessionId))
|
||||
? activeWs.focusedSessionId
|
||||
: liveIds[0];
|
||||
if (targetId) splitSessionWithCurrentShell(targetId, 'horizontal');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'splitVertical': {
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
const activeSession = sessions.find(s => s.id === currentId);
|
||||
const activeWs = workspaces.find(w => w.id === currentId);
|
||||
if (activeSession && !activeSession.workspaceId) {
|
||||
splitSessionWithCurrentShell(activeSession.id, 'vertical');
|
||||
} else if (activeWs) {
|
||||
const liveIds = collectSessionIds(activeWs.root);
|
||||
const targetId = (activeWs.focusedSessionId && liveIds.includes(activeWs.focusedSessionId))
|
||||
? activeWs.focusedSessionId
|
||||
: liveIds[0];
|
||||
if (targetId) splitSessionWithCurrentShell(targetId, 'vertical');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'togglePaneZoom': {
|
||||
// Toggle workspace between split and focus (zoom) mode
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
const activeWs = workspaces.find(w => w.id === currentId);
|
||||
if (activeWs) {
|
||||
toggleWorkspaceViewMode(activeWs.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'moveFocus': {
|
||||
// Debounce to prevent double-triggering when focus switches between terminals
|
||||
const now = Date.now();
|
||||
if (now - lastMoveFocusTimeRef.current < MOVE_FOCUS_DEBOUNCE_MS) {
|
||||
if (IS_DEV) console.log('[App] moveFocus debounced, ignoring');
|
||||
break;
|
||||
}
|
||||
lastMoveFocusTimeRef.current = now;
|
||||
|
||||
// Move focus between split panes
|
||||
if (IS_DEV) console.log('[App] moveFocus action triggered, key:', e.key);
|
||||
const direction = e.key === 'ArrowUp' ? 'up'
|
||||
: e.key === 'ArrowDown' ? 'down'
|
||||
: e.key === 'ArrowLeft' ? 'left'
|
||||
: e.key === 'ArrowRight' ? 'right'
|
||||
: null;
|
||||
if (IS_DEV) console.log('[App] moveFocus direction:', direction);
|
||||
if (direction) {
|
||||
// Find the active workspace
|
||||
const currentId = activeTabStore.getActiveTabId();
|
||||
if (IS_DEV) console.log('[App] Active tab ID:', currentId);
|
||||
const activeWs = workspaces.find(w => w.id === currentId);
|
||||
if (IS_DEV) console.log('[App] Active workspace:', activeWs?.id, activeWs?.title);
|
||||
if (activeWs) {
|
||||
const result = moveFocusInWorkspace(activeWs.id, direction as 'up' | 'down' | 'left' | 'right');
|
||||
if (IS_DEV) console.log('[App] moveFocusInWorkspace result:', result);
|
||||
} else {
|
||||
if (IS_DEV) console.log('[App] No active workspace found');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleCreateLocalTerminalImpl(
|
||||
getCtx: AppContextGetter,
|
||||
shell?: { command: string; args?: string[]; name?: string; icon?: string },
|
||||
options?: { localStartDir?: string },
|
||||
) {
|
||||
const { addConnectionLog, classifyLocalShellType, createLocalTerminal, discoveredShells, resolveShellSetting, systemInfoRef, terminalSettings } = getCtx();
|
||||
{
|
||||
const { username, hostname } = systemInfoRef.current;
|
||||
const resolved = shell ?? resolveShellSetting(terminalSettings.localShell, discoveredShells, terminalSettings.localShellArgs);
|
||||
// Match by ID (not command) to avoid WSL distros all sharing wsl.exe
|
||||
const matchedShell = !shell ? discoveredShells.find(s => s.id === terminalSettings.localShell) : undefined;
|
||||
const shellName = shell?.name ?? matchedShell?.name;
|
||||
const shellIcon = shell?.icon ?? matchedShell?.icon;
|
||||
const sessionId = createLocalTerminal({
|
||||
shellType: classifyLocalShellType(resolved?.command || terminalSettings.localShell, navigator.userAgent),
|
||||
shell: resolved?.command,
|
||||
shellArgs: resolved?.args,
|
||||
shellName,
|
||||
shellIcon,
|
||||
localStartDir: options?.localStartDir,
|
||||
});
|
||||
addConnectionLog({
|
||||
sessionId,
|
||||
hostId: '',
|
||||
hostLabel: shellName || 'Local Terminal',
|
||||
hostname: 'localhost',
|
||||
username: username,
|
||||
protocol: 'local',
|
||||
startTime: Date.now(),
|
||||
localUsername: username,
|
||||
localHostname: hostname,
|
||||
saved: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function handleConnectToHostImpl(getCtx: AppContextGetter, host: Host) {
|
||||
const { addConnectionLog, connectToHost, identities, keys, resolveEffectiveHost, resolveHostAuth, systemInfoRef } = getCtx();
|
||||
{
|
||||
const { username, hostname: localHost } = systemInfoRef.current;
|
||||
|
||||
const effectiveHost = resolveEffectiveHost(host);
|
||||
|
||||
// Handle serial hosts separately
|
||||
if (effectiveHost.protocol === 'serial') {
|
||||
const portName = host.hostname.split('/').pop() || host.hostname;
|
||||
const sessionId = connectToHost(effectiveHost);
|
||||
addConnectionLog({
|
||||
sessionId,
|
||||
hostId: host.id,
|
||||
hostLabel: host.label || `Serial: ${portName}`,
|
||||
hostname: host.hostname,
|
||||
username: username,
|
||||
protocol: 'serial',
|
||||
...getLogHostVisualSnapshot(effectiveHost),
|
||||
startTime: Date.now(),
|
||||
localUsername: username,
|
||||
localHostname: localHost,
|
||||
saved: false,
|
||||
});
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
const protocol = effectiveHost.etEnabled ? 'et' : effectiveHost.moshEnabled ? 'mosh' : (effectiveHost.protocol || 'ssh');
|
||||
const resolvedAuth = resolveHostAuth({ host: effectiveHost, keys, identities });
|
||||
const sessionId = connectToHost(effectiveHost);
|
||||
addConnectionLog({
|
||||
sessionId,
|
||||
hostId: host.id,
|
||||
hostLabel: host.label,
|
||||
hostname: host.hostname,
|
||||
username: resolvedAuth.username || 'root',
|
||||
protocol: protocol as 'ssh' | 'telnet' | 'local' | 'mosh' | 'et',
|
||||
...getLogHostVisualSnapshot(effectiveHost),
|
||||
startTime: Date.now(),
|
||||
localUsername: username,
|
||||
localHostname: localHost,
|
||||
saved: false,
|
||||
});
|
||||
return sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
export function handleTerminalDataCaptureImpl(getCtx: AppContextGetter, sessionId: string, data: string) {
|
||||
const { IS_DEV, connectionLogs, selectConnectionLogForTerminalDataCapture, sessions, updateConnectionLog } = getCtx();
|
||||
{
|
||||
if (IS_DEV) console.log('[handleTerminalDataCapture] Called', { sessionId, dataLength: data.length });
|
||||
const session = sessions.find(s => s.id === sessionId);
|
||||
if (IS_DEV) console.log('[handleTerminalDataCapture] Session', session);
|
||||
if (IS_DEV) console.log('[handleTerminalDataCapture] All logs:', connectionLogs.map(l => ({ id: l.id, sessionId: l.sessionId, hostname: l.hostname, endTime: l.endTime, hasTerminalData: !!l.terminalData })));
|
||||
|
||||
const matchingLog = selectConnectionLogForTerminalDataCapture(
|
||||
connectionLogs,
|
||||
{ sessionId, hostname: session?.hostname },
|
||||
);
|
||||
|
||||
if (IS_DEV) console.log('[handleTerminalDataCapture] Matching log', matchingLog);
|
||||
|
||||
if (matchingLog) {
|
||||
updateConnectionLog(matchingLog.id, {
|
||||
endTime: Date.now(),
|
||||
terminalData: data,
|
||||
});
|
||||
if (IS_DEV) console.log('[handleTerminalDataCapture] Updated log with terminalData');
|
||||
|
||||
// Auto-save is now handled by real-time streaming in the main process
|
||||
// via sessionLogStreamManager. No renderer-side fallback needed.
|
||||
} else {
|
||||
if (IS_DEV) console.log('[handleTerminalDataCapture] No matching log found!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function hasMultipleProtocolsImpl(getCtx: AppContextGetter, host: Host) {
|
||||
const { resolveEffectiveHost } = getCtx();
|
||||
{
|
||||
// Gates the protocol picker (legacy name kept for its existing wiring).
|
||||
// Only prompt when Telnet is available but isn't the host's default protocol;
|
||||
// SSH-only, SSH+Mosh and Telnet-default all connect directly.
|
||||
const effective = resolveEffectiveHost(host);
|
||||
return Boolean(effective.telnetEnabled) && effective.protocol !== 'telnet';
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHostConnectWithProtocolCheckImpl(getCtx: AppContextGetter, host: Host) {
|
||||
const { handleConnectToHost, hasMultipleProtocols, resolveEffectiveHost, setIsQuickSwitcherOpen, setProtocolSelectHost, setQuickSearch } = getCtx();
|
||||
{
|
||||
if (hasMultipleProtocols(host)) {
|
||||
setProtocolSelectHost(resolveEffectiveHost(host));
|
||||
setIsQuickSwitcherOpen(false);
|
||||
setQuickSearch('');
|
||||
} else {
|
||||
handleConnectToHost(host);
|
||||
setIsQuickSwitcherOpen(false);
|
||||
setQuickSearch('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleProtocolSelectImpl(getCtx: AppContextGetter, protocol: HostProtocol, port: number) {
|
||||
const { handleConnectToHost, protocolSelectHost, setProtocolSelectHost } = getCtx();
|
||||
{
|
||||
if (protocolSelectHost) {
|
||||
const hostWithProtocol: Host = {
|
||||
...protocolSelectHost,
|
||||
protocol: (protocol === 'mosh' || protocol === 'et') ? 'ssh' : protocol,
|
||||
port,
|
||||
moshEnabled: protocol === 'mosh',
|
||||
etEnabled: protocol === 'et',
|
||||
};
|
||||
handleConnectToHost(hostWithProtocol);
|
||||
setProtocolSelectHost(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleToggleThemeImpl(getCtx: AppContextGetter) {
|
||||
const { openSettingsWindow, resolvedTheme, setTheme, t, theme, toast } = getCtx();
|
||||
{
|
||||
if (theme === 'system') {
|
||||
toast.info(
|
||||
t('topTabs.toggleTheme.systemExitMessage'),
|
||||
{
|
||||
title: t('topTabs.toggleTheme.systemExitTitle'),
|
||||
actionLabel: t('topTabs.toggleTheme.openSettings'),
|
||||
onClick: () => {
|
||||
void (async () => {
|
||||
const opened = await openSettingsWindow();
|
||||
if (!opened) toast.error(t('toast.settingsUnavailable'), t('common.settings'));
|
||||
})();
|
||||
},
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
export function handleRootContextMenuImpl(getCtx: AppContextGetter, e: React.MouseEvent<HTMLDivElement>) {
|
||||
void getCtx;
|
||||
{
|
||||
const editableSelector =
|
||||
"input, textarea, [contenteditable], .monaco-editor, .monaco-diff-editor, .monaco-inputbox, .monaco-menu-container";
|
||||
|
||||
const nativeEvent = e.nativeEvent;
|
||||
const path = typeof nativeEvent.composedPath === "function" ? nativeEvent.composedPath() : [];
|
||||
const allowFromPath = path.some(
|
||||
(node) => node instanceof Element && !!node.closest(editableSelector),
|
||||
);
|
||||
|
||||
const target = e.target;
|
||||
const targetElement =
|
||||
target instanceof Element
|
||||
? target
|
||||
: target instanceof Node
|
||||
? target.parentElement
|
||||
: null;
|
||||
const allowFromTarget = !!targetElement?.closest(editableSelector);
|
||||
|
||||
const allowNativeContextMenu = allowFromPath || allowFromTarget;
|
||||
|
||||
if (allowNativeContextMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
const storage = new Map<string, string>();
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => storage.set(key, value),
|
||||
removeItem: (key: string) => storage.delete(key),
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
getAppHostTreeLayerStyle,
|
||||
} = await import('./AppHostTreeLayer');
|
||||
const hostTreeLayerSource = readFileSync(new URL('./AppHostTreeLayer.tsx', import.meta.url), 'utf8');
|
||||
|
||||
test('shared host tree layer is visible above work tabs', () => {
|
||||
assert.deepEqual(getAppHostTreeLayerStyle(true), {
|
||||
visibility: 'visible',
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 30,
|
||||
});
|
||||
});
|
||||
|
||||
test('shared host tree layer is hidden behind root pages', () => {
|
||||
assert.deepEqual(getAppHostTreeLayerStyle(false), {
|
||||
visibility: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('shared host tree does not force open when entering a work tab surface', () => {
|
||||
assert.doesNotMatch(hostTreeLayerSource, /setIsOpen\(true\)/);
|
||||
assert.doesNotMatch(hostTreeLayerSource, /shouldAutoOpenHostTreeOnSurfaceChange/);
|
||||
});
|
||||
|
||||
test('host tree layer hides immediately when leaving work tab surfaces', () => {
|
||||
assert.match(hostTreeLayerSource, /getAppHostTreeLayerStyle\(surfaceVisible\)/);
|
||||
assert.doesNotMatch(hostTreeLayerSource, /layerVisible/);
|
||||
});
|
||||
|
||||
test('shared host tree theme follows active chrome resolution and manual chrome injection', () => {
|
||||
assert.match(hostTreeLayerSource, /resolveActiveChromeTheme/);
|
||||
assert.match(hostTreeLayerSource, /useManualTerminalChromeSurfaceInjection/);
|
||||
assert.match(hostTreeLayerSource, /resolveSessionAppearance/);
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useActiveTabId } from '../state/activeTabStore';
|
||||
import type { EditorTab } from '../state/editorTabStore';
|
||||
import type { LogView } from '../state/logViewState';
|
||||
import { useManualTerminalChromeSurfaceInjection } from '../state/useManualTerminalChromeSurfaceInjection';
|
||||
import { TerminalHostTreeSidebar } from '../../components/terminalLayer/TerminalHostTreeSidebar';
|
||||
import type {
|
||||
ResolvedAppearance,
|
||||
TerminalAppearanceHostScope,
|
||||
} from '../../domain/terminalAppearanceRuntime';
|
||||
import type { GroupConfig, Host, TerminalSession, TerminalTheme, Workspace } from '../../types';
|
||||
import { resolveActiveChromeTheme } from './activeChromeTheme';
|
||||
import {
|
||||
isHostTreeWorkTabSurface,
|
||||
resolveWorkTabActiveHostId,
|
||||
} from './workTabSurface';
|
||||
|
||||
interface AppHostTreeLayerProps {
|
||||
enabled: boolean;
|
||||
hosts: Host[];
|
||||
customGroups: string[];
|
||||
groupConfigs: GroupConfig[];
|
||||
sessions: TerminalSession[];
|
||||
workspaces: Workspace[];
|
||||
editorTabs: readonly EditorTab[];
|
||||
logViews: readonly LogView[];
|
||||
orderedTabs: readonly string[];
|
||||
accentMode: 'theme' | 'custom';
|
||||
currentTerminalTheme: TerminalTheme;
|
||||
customAccent: string;
|
||||
followAppTerminalTheme: boolean;
|
||||
hostById: ReadonlyMap<string, Host>;
|
||||
themeById: ReadonlyMap<string, TerminalTheme>;
|
||||
resolveSessionAppearance?: (hostScope: TerminalAppearanceHostScope) => ResolvedAppearance;
|
||||
onConnect: (host: Host) => void;
|
||||
onCreateLocalTerminal?: () => void;
|
||||
}
|
||||
|
||||
export function getAppHostTreeLayerStyle(surfaceVisible: boolean): React.CSSProperties {
|
||||
return {
|
||||
visibility: surfaceVisible ? 'visible' : 'hidden',
|
||||
pointerEvents: surfaceVisible ? 'auto' : 'none',
|
||||
zIndex: surfaceVisible ? 30 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export const AppHostTreeLayer: React.FC<AppHostTreeLayerProps> = ({
|
||||
enabled,
|
||||
hosts,
|
||||
customGroups,
|
||||
groupConfigs,
|
||||
sessions,
|
||||
workspaces,
|
||||
editorTabs,
|
||||
logViews,
|
||||
orderedTabs,
|
||||
accentMode,
|
||||
currentTerminalTheme,
|
||||
customAccent,
|
||||
followAppTerminalTheme,
|
||||
hostById,
|
||||
themeById,
|
||||
resolveSessionAppearance,
|
||||
onConnect,
|
||||
onCreateLocalTerminal,
|
||||
}) => {
|
||||
const activeTabId = useActiveTabId();
|
||||
const sessionIds = useMemo(() => new Set(sessions.map((session) => session.id)), [sessions]);
|
||||
const workspaceIds = useMemo(() => new Set(workspaces.map((workspace) => workspace.id)), [workspaces]);
|
||||
const logViewIds = useMemo(() => new Set(logViews.map((logView) => logView.id)), [logViews]);
|
||||
const sessionById = useMemo(
|
||||
() => new Map(sessions.map((session) => [session.id, session])),
|
||||
[sessions],
|
||||
);
|
||||
const workspaceById = useMemo(
|
||||
() => new Map(workspaces.map((workspace) => [workspace.id, workspace])),
|
||||
[workspaces],
|
||||
);
|
||||
const surfaceVisible = isHostTreeWorkTabSurface({
|
||||
enabled,
|
||||
activeTabId,
|
||||
logViewIds,
|
||||
orderedTabs,
|
||||
sessionIds,
|
||||
workspaceIds,
|
||||
});
|
||||
|
||||
const activeHostId = useMemo(() => resolveWorkTabActiveHostId({
|
||||
activeTabId,
|
||||
editorTabs,
|
||||
sessions,
|
||||
workspaces,
|
||||
}), [activeTabId, editorTabs, sessions, workspaces]);
|
||||
|
||||
const hostTreeTheme = useMemo(() => (
|
||||
resolveActiveChromeTheme({
|
||||
accentMode,
|
||||
activeTabId,
|
||||
currentTerminalTheme,
|
||||
customAccent,
|
||||
editorTabs,
|
||||
followAppTerminalTheme,
|
||||
hostById,
|
||||
logViews,
|
||||
resolveSessionAppearance,
|
||||
sessionById,
|
||||
themeById,
|
||||
workspaceById,
|
||||
}) ?? currentTerminalTheme
|
||||
), [
|
||||
accentMode,
|
||||
activeTabId,
|
||||
currentTerminalTheme,
|
||||
customAccent,
|
||||
editorTabs,
|
||||
followAppTerminalTheme,
|
||||
hostById,
|
||||
logViews,
|
||||
resolveSessionAppearance,
|
||||
sessionById,
|
||||
themeById,
|
||||
workspaceById,
|
||||
]);
|
||||
|
||||
useManualTerminalChromeSurfaceInjection(
|
||||
hostTreeTheme,
|
||||
!followAppTerminalTheme && surfaceVisible,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 flex min-h-0"
|
||||
data-section="app-host-tree-layer"
|
||||
style={getAppHostTreeLayerStyle(surfaceVisible)}
|
||||
>
|
||||
<TerminalHostTreeSidebar
|
||||
enabled={enabled}
|
||||
surfaceVisible={surfaceVisible}
|
||||
hosts={hosts}
|
||||
customGroups={customGroups}
|
||||
groupConfigs={groupConfigs}
|
||||
resolvedPreviewTheme={hostTreeTheme}
|
||||
activeHostId={activeHostId}
|
||||
onConnect={onConnect}
|
||||
onCreateLocalTerminal={onCreateLocalTerminal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
const storage = new Map<string, string>();
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => storage.set(key, value),
|
||||
removeItem: (key: string) => storage.delete(key),
|
||||
},
|
||||
});
|
||||
|
||||
const { getLogViewWrapperStyle, shouldRenderTerminalLayerMount } = await import('./AppMounts.tsx');
|
||||
const activeTabChromeSource = readFileSync(new URL('./AppActiveTabChrome.tsx', import.meta.url), 'utf8');
|
||||
const appViewSource = readFileSync(new URL('./AppView.tsx', import.meta.url), 'utf8');
|
||||
const appMountsSource = readFileSync(new URL('./AppMounts.tsx', import.meta.url), 'utf8');
|
||||
const globalCssSource = readFileSync(new URL('../../index.css', import.meta.url), 'utf8');
|
||||
|
||||
test('visible log view leaves room for the terminal host sidebar', () => {
|
||||
assert.deepEqual(getLogViewWrapperStyle(true, 220), {
|
||||
left: 220,
|
||||
});
|
||||
});
|
||||
|
||||
test('hidden log view remains hidden while preserving host sidebar offset', () => {
|
||||
assert.deepEqual(getLogViewWrapperStyle(false, 220), {
|
||||
visibility: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
zIndex: -1,
|
||||
left: 220,
|
||||
});
|
||||
});
|
||||
|
||||
test('terminal layer renders only after terminal content is visible or mounted', () => {
|
||||
assert.equal(shouldRenderTerminalLayerMount(true, false), true);
|
||||
assert.equal(shouldRenderTerminalLayerMount(false, true), true);
|
||||
assert.equal(shouldRenderTerminalLayerMount(false, false), false);
|
||||
});
|
||||
|
||||
test('inactive app surfaces suppress background color transitions', () => {
|
||||
assert.match(appMountsSource, /data-inactive-app-surface=\{isActive \? undefined : "true"\}/);
|
||||
assert.match(appMountsSource, /data-inactive-app-surface=\{isVisible \? undefined : "true"\}/);
|
||||
assert.match(globalCssSource, /\[data-inactive-app-surface\][\s\S]*transition: none !important;/);
|
||||
});
|
||||
|
||||
test('vault activation suppresses inherited text color transitions', () => {
|
||||
assert.match(appMountsSource, /data-app-surface-transition-suppressed/);
|
||||
assert.match(appMountsSource, /setSuppressActiveTransition\(false\)/);
|
||||
assert.match(globalCssSource, /\[data-app-surface-transition-suppressed\][\s\S]*transition: none !important;/);
|
||||
});
|
||||
|
||||
test('vault surface carries app theme vars while terminal chrome is active', () => {
|
||||
assert.match(appMountsSource, /appThemeStyle\?: React\.CSSProperties/);
|
||||
assert.match(appMountsSource, /style=\{\{ \.\.\.appThemeStyle, \.\.\.containerStyle \}\}/);
|
||||
assert.match(appViewSource, /buildAppThemeCssVars\(tokens, accentMode, customAccent\)/);
|
||||
assert.match(appViewSource, /<VaultViewContainer appThemeStyle=\{appThemeStyle\}>/);
|
||||
});
|
||||
|
||||
test('active tab chrome keeps removed theme side effects unmounted', () => {
|
||||
const removedThemeHook = ['use', 'Im', 'mersive', 'Mode'].join('');
|
||||
const removedThemeStoreSetter = ['set', 'Im', 'mersive', 'Active'].join('');
|
||||
assert.equal(activeTabChromeSource.includes(removedThemeHook), false);
|
||||
assert.equal(activeTabChromeSource.includes(removedThemeStoreSetter), false);
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
import React, { Suspense, lazy, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useActiveTabId, useIsSftpActive, useIsVaultActive } from '../state/activeTabStore';
|
||||
import { useTerminalHostTreeLayoutWidth } from '../state/terminalHostTreeStore';
|
||||
import { isTerminalContentTabSurface } from './workTabSurface';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { ConnectionLog, TerminalTheme } from '../../types';
|
||||
import { LazyLoadBoundary } from '../../components/ui/lazy-load-boundary';
|
||||
import type { LogView as LogViewType } from '../state/logViewState';
|
||||
import type { SftpView as SftpViewComponent } from '../../components/SftpView';
|
||||
import type { TerminalLayer as TerminalLayerComponent } from '../../components/TerminalLayer';
|
||||
|
||||
// Visibility container for VaultView - isolates isActive subscription
|
||||
export const VaultViewContainer: React.FC<{
|
||||
children: React.ReactNode;
|
||||
appThemeStyle?: React.CSSProperties;
|
||||
}> = ({ children, appThemeStyle }) => {
|
||||
const isActive = useIsVaultActive();
|
||||
const wasActiveRef = useRef(isActive);
|
||||
const [suppressActiveTransition, setSuppressActiveTransition] = useState(false);
|
||||
const isActivating = isActive && !wasActiveRef.current;
|
||||
const shouldSuppressTransition = isActivating || suppressActiveTransition;
|
||||
const containerStyle: React.CSSProperties = isActive
|
||||
? {}
|
||||
: { visibility: 'hidden', pointerEvents: 'none', position: 'absolute', zIndex: -1 };
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const wasActive = wasActiveRef.current;
|
||||
wasActiveRef.current = isActive;
|
||||
if (!isActive || wasActive) return;
|
||||
|
||||
setSuppressActiveTransition(true);
|
||||
const view = window;
|
||||
let firstFrame = 0;
|
||||
let secondFrame = 0;
|
||||
firstFrame = view.requestAnimationFrame(() => {
|
||||
secondFrame = view.requestAnimationFrame(() => {
|
||||
setSuppressActiveTransition(false);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
view.cancelAnimationFrame(firstFrame);
|
||||
view.cancelAnimationFrame(secondFrame);
|
||||
};
|
||||
}, [isActive]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("absolute inset-0", isActive ? "z-20" : "")}
|
||||
data-inactive-app-surface={isActive ? undefined : "true"}
|
||||
data-app-surface-transition-suppressed={shouldSuppressTransition ? "true" : undefined}
|
||||
style={{ ...appThemeStyle, ...containerStyle }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// LogView wrapper - manages visibility based on active tab
|
||||
interface LogViewWrapperProps {
|
||||
logView: LogViewType;
|
||||
defaultTerminalTheme: TerminalTheme;
|
||||
defaultFontSize: number;
|
||||
onClose: () => void;
|
||||
onUpdateLog: (logId: string, updates: Partial<ConnectionLog>) => void;
|
||||
}
|
||||
|
||||
export function getLogViewWrapperStyle(
|
||||
isVisible: boolean,
|
||||
hostTreeLayoutWidth: number,
|
||||
): React.CSSProperties {
|
||||
const baseStyle = {
|
||||
left: hostTreeLayoutWidth,
|
||||
};
|
||||
return isVisible
|
||||
? baseStyle
|
||||
: { visibility: 'hidden', pointerEvents: 'none', position: 'absolute', zIndex: -1, ...baseStyle };
|
||||
}
|
||||
|
||||
export const LogViewWrapper: React.FC<LogViewWrapperProps> = ({ logView, defaultTerminalTheme, defaultFontSize, onClose, onUpdateLog }) => {
|
||||
const activeTabId = useActiveTabId();
|
||||
const isVisible = activeTabId === logView.id;
|
||||
const hostTreeLayoutWidth = useTerminalHostTreeLayoutWidth();
|
||||
|
||||
const containerStyle = getLogViewWrapperStyle(isVisible, hostTreeLayoutWidth);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("absolute inset-0", isVisible ? "z-20" : "")}
|
||||
data-inactive-app-surface={isVisible ? undefined : "true"}
|
||||
style={containerStyle}
|
||||
>
|
||||
<LazyLoadBoundary name="Log view" resetKey={logView.id}>
|
||||
<Suspense fallback={<LogViewFallback />}>
|
||||
<LazyLogView
|
||||
log={logView.log}
|
||||
defaultTerminalTheme={defaultTerminalTheme}
|
||||
defaultFontSize={defaultFontSize}
|
||||
isVisible={isVisible}
|
||||
onClose={onClose}
|
||||
onUpdateLog={onUpdateLog}
|
||||
/>
|
||||
</Suspense>
|
||||
</LazyLoadBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LazyLogView = lazy(() => import('../../components/LogView'));
|
||||
|
||||
const LazySftpView = lazy(() =>
|
||||
import('../../components/SftpView').then((m) => ({ default: m.SftpView })),
|
||||
);
|
||||
|
||||
const LazyTerminalLayer = lazy(() =>
|
||||
import('../../components/TerminalLayer').then((m) => ({ default: m.TerminalLayer })),
|
||||
);
|
||||
|
||||
type SftpViewProps = React.ComponentProps<typeof SftpViewComponent>;
|
||||
type TerminalLayerProps = React.ComponentProps<typeof TerminalLayerComponent>;
|
||||
|
||||
const LogViewFallback = () => (
|
||||
<div className="netcatty-lazy-fade-in h-full min-h-0 bg-background" aria-hidden="true" />
|
||||
);
|
||||
|
||||
const SftpViewFallback = ({ visible }: { visible: boolean }) => {
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<div className="netcatty-lazy-fade-in absolute inset-0 z-20 bg-background" aria-hidden="true" />
|
||||
);
|
||||
};
|
||||
|
||||
const TerminalLayerFallback = ({ visible }: { visible: boolean }) => {
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<div className="netcatty-lazy-fade-in absolute inset-0 z-20 bg-background" aria-hidden="true" />
|
||||
);
|
||||
};
|
||||
|
||||
export function shouldRenderTerminalLayerMount(
|
||||
isVisible: boolean,
|
||||
shouldMount: boolean,
|
||||
): boolean {
|
||||
return isVisible || shouldMount;
|
||||
}
|
||||
|
||||
export const SftpViewMount: React.FC<SftpViewProps> = (props) => {
|
||||
const isActive = useIsSftpActive();
|
||||
const [shouldMount, setShouldMount] = useState(isActive);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) setShouldMount(true);
|
||||
}, [isActive]);
|
||||
|
||||
if (!shouldMount) return null;
|
||||
|
||||
return (
|
||||
<LazyLoadBoundary name="SFTP" resetKey={isActive ? "active" : "idle"}>
|
||||
<Suspense fallback={<SftpViewFallback visible={isActive} />}>
|
||||
<LazySftpView {...props} />
|
||||
</Suspense>
|
||||
</LazyLoadBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export const TerminalLayerMount: React.FC<TerminalLayerProps> = (props) => {
|
||||
const activeTabId = useActiveTabId();
|
||||
const sessionIds = useMemo(() => new Set(props.sessions.map((session) => session.id)), [props.sessions]);
|
||||
const workspaceIds = useMemo(() => new Set(props.workspaces.map((workspace) => workspace.id)), [props.workspaces]);
|
||||
const isVisible = isTerminalContentTabSurface({
|
||||
activeTabId,
|
||||
sessionIds,
|
||||
workspaceIds,
|
||||
}) || !!props.draggingSessionId;
|
||||
const [shouldMount, setShouldMount] = useState(isVisible);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) setShouldMount(true);
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldMount) return;
|
||||
type IdleWindow = Window & {
|
||||
requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number;
|
||||
cancelIdleCallback?: (id: number) => void;
|
||||
};
|
||||
const idleWindow = window as IdleWindow;
|
||||
if (typeof idleWindow.requestIdleCallback === "function") {
|
||||
const id = idleWindow.requestIdleCallback(() => setShouldMount(true), { timeout: 5000 });
|
||||
return () => idleWindow.cancelIdleCallback?.(id);
|
||||
}
|
||||
const id = window.setTimeout(() => setShouldMount(true), 5000);
|
||||
return () => window.clearTimeout(id);
|
||||
}, [shouldMount]);
|
||||
|
||||
const shouldRender = shouldRenderTerminalLayerMount(isVisible, shouldMount);
|
||||
|
||||
if (!shouldRender) return null;
|
||||
|
||||
return (
|
||||
<LazyLoadBoundary name="Terminal" resetKey={activeTabId}>
|
||||
<Suspense fallback={<TerminalLayerFallback visible={isVisible} />}>
|
||||
<LazyTerminalLayer {...props} />
|
||||
</Suspense>
|
||||
</LazyLoadBoundary>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,730 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { Suspense, lazy, useCallback, useMemo } from 'react';
|
||||
import { AlertTriangle, Download, Trash2 } from 'lucide-react';
|
||||
import { activeTabStore, toEditorTabId, useIsEditorTabActive } from '../state/activeTabStore';
|
||||
import { editorTabStore } from '../state/editorTabStore';
|
||||
import { releaseEditorTabSaveCoordinator, saveEditorTab } from '../state/editorTabSave';
|
||||
import { useTerminalHostTreeLayoutWidth } from '../state/terminalHostTreeStore';
|
||||
import { TopTabs } from '../../components/TopTabs';
|
||||
import { VaultView } from '../../components/VaultView';
|
||||
import { QuickAddSnippetDialog } from '../../components/QuickAddSnippetDialog';
|
||||
import { QuickScriptEditorDialog } from '../../components/scripts/QuickScriptEditorDialog';
|
||||
import { AddToWorkspaceDialog } from '../../components/workspace/AddToWorkspaceDialog';
|
||||
import { KeyboardInteractiveModal } from '../../components/KeyboardInteractiveModal';
|
||||
import { PassphraseModal } from '../../components/PassphraseModal';
|
||||
import { UnsavedChangesProvider } from '../../components/editor/UnsavedChangesDialog';
|
||||
import { SnippetExecutionProvider } from '../../components/SnippetExecutionProvider';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../../components/ui/dialog';
|
||||
import { Input } from '../../components/ui/input';
|
||||
import { Label } from '../../components/ui/label';
|
||||
import { LazyLoadBoundary } from '../../components/ui/lazy-load-boundary';
|
||||
import { toast } from '../../components/ui/toast';
|
||||
import { AppHostTreeLayer } from './AppHostTreeLayer';
|
||||
import { getUiThemeById } from '../../infrastructure/config/uiThemes';
|
||||
import { buildAppThemeCssVars } from '../state/settingsStateDefaults';
|
||||
import { useMainWindowInputFocusRecovery } from '../state/useMainWindowInputFocusRecovery';
|
||||
|
||||
const LazyProtocolSelectDialog = lazy(() => import('../../components/ProtocolSelectDialog'));
|
||||
const LazyQuickSwitcher = lazy(() =>
|
||||
import('../../components/QuickSwitcher').then((m) => ({ default: m.QuickSwitcher })),
|
||||
);
|
||||
const LazyCreateWorkspaceDialog = lazy(() =>
|
||||
import('../../components/CreateWorkspaceDialog').then((m) => ({ default: m.CreateWorkspaceDialog })),
|
||||
);
|
||||
const LazyTextEditorTabView = lazy(() =>
|
||||
import('../../components/editor/TextEditorTabView').then((m) => ({ default: m.TextEditorTabView })),
|
||||
);
|
||||
|
||||
const TextEditorTabFallback = ({ tabId }: { tabId: string }) => {
|
||||
const isVisible = useIsEditorTabActive(tabId);
|
||||
const hostTreeLayoutWidth = useTerminalHostTreeLayoutWidth();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...(isVisible ? null : { pointerEvents: 'none', visibility: 'hidden' }),
|
||||
zIndex: 20,
|
||||
left: hostTreeLayoutWidth,
|
||||
}}
|
||||
className="netcatty-lazy-fade-in absolute top-0 right-0 bottom-0 min-h-0 flex flex-col bg-background"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type AppViewContext = Record<string, any>;
|
||||
|
||||
export function AppView({ ctx }: { ctx: AppViewContext }) {
|
||||
const {
|
||||
resetSessionRename,
|
||||
resetWorkspaceRename,
|
||||
setAddToWorkspaceDialog,
|
||||
setIsCreateWorkspaceOpen,
|
||||
setIsQuickSwitcherOpen,
|
||||
setProtocolSelectHost,
|
||||
setQuickSearch,
|
||||
} = ctx;
|
||||
|
||||
const dismissTransientOverlays = useCallback(() => {
|
||||
setIsQuickSwitcherOpen(false);
|
||||
setQuickSearch('');
|
||||
setIsCreateWorkspaceOpen(false);
|
||||
setProtocolSelectHost(null);
|
||||
setAddToWorkspaceDialog(null);
|
||||
resetSessionRename();
|
||||
resetWorkspaceRename();
|
||||
}, [
|
||||
resetSessionRename,
|
||||
resetWorkspaceRename,
|
||||
setAddToWorkspaceDialog,
|
||||
setIsCreateWorkspaceOpen,
|
||||
setIsQuickSwitcherOpen,
|
||||
setProtocolSelectHost,
|
||||
setQuickSearch,
|
||||
]);
|
||||
|
||||
useMainWindowInputFocusRecovery({ onPageHidden: dismissTransientOverlays });
|
||||
|
||||
const {
|
||||
accentMode, addShellHistoryEntry, addSessionToWorkspace, addToWorkspaceDialog, appendHostToWorkspace, appendLocalTerminalToWorkspace,
|
||||
clearAndRemoveSource, clearAndRemoveSources, clearUnsavedConnectionLogs, closeLogView, closeSession, closeTabsBatch, closeWorkspace, copySessionToNewWindowWithCurrentShell, copySessionWithCurrentShell,
|
||||
connectionLogs, convertKnownHostToHost, createWorkspaceFromSessions, createWorkspaceFromTargets, createWorkspaceWithHosts, customAccent,
|
||||
customGroups, currentTerminalTheme, deepLinkHostDraft, deleteConnectionLog, draggingSessionId, effectiveKnownHosts, editorTabs, editorWordWrap, emptyVaultConflict,
|
||||
followAppTerminalTheme,
|
||||
groupConfigs, handleAddKnownHost, handleConnectSerial, handleConnectToHost, handleCreateLocalTerminal, handleDefaultTerminalThemeChange, handleDeleteHost,
|
||||
handleEndSessionDrag, handleFollowAppTerminalThemeChange, handleHostConnectWithProtocolCheck, handleHotkeyAction, handleKeyboardInteractiveCancel, handleKeyboardInteractiveSubmit,
|
||||
handleOpenHostFromVaultNote, handleOpenQuickSwitcher, handleOpenSettings, handleOpenVaultHostFromChat, handleOpenVaultNoteFromChat, handleOpenVaultSectionFromChat, handleOpenVaultSnippetFromChat, handleRootContextMenu, handlePassphraseCancel, handlePassphraseSkip, handlePassphraseSubmit, handleProtocolSelect,
|
||||
handleRequestCloseEditorTabRef, handleSessionStatusChange, handleSyncNowManual, handleTerminalDataCapture, handleToggleTheme, handleUpdateHostFromTerminal,
|
||||
hostById, hosts, terminalHosts, updateTerminalHosts, hotkeyScheme, identities, importOrReuseKey, isBroadcastEnabled, isCreateWorkspaceOpen, isMacClient, isQuickSwitcherOpen,
|
||||
keyBindings, keyboardInteractiveQueue, keys, logViews, managedSources, navigateToSection, noteGroups, notes, openLogView, openNoteRequest, orderedTabsWithEditors, orphanSessions,
|
||||
passphraseQueue, protocolSelectHost, proxyProfiles, portForwardingRules, quickResults, quickSearch, removeSessionFromWorkspace, reorderWorkTabs, reorderWorkspaceSessions,
|
||||
resolveEmptyVaultConflict, resolvedTheme, resolveSessionAppearance, runSnippet, sessionLogsDir, sessionLogsEnabled, sessionLogsFormat, sessionLogsTimestampsEnabled, sessionRenameTarget, sshDebugLogsEnabled,
|
||||
sessionRenameValue, sessions, setActiveTabId, setDeepLinkHostDraft, setDraggingSessionId, setEditorWordWrap,
|
||||
setNavigateToSection, setSessionRenameValue, setTerminalFontFamilyId, setTerminalFontSize, setVaultFocusRequest, updateSessionFontSize, updateSessionRestoreCwd, updateSessionDynamicTitle, updateSessionCodingCliProvider, clearSessionFontSizeOverride,
|
||||
setWorkspaceFocusedSession, setWorkspaceRenameValue, settings, sftpAutoOpenSidebar, sftpFollowTerminalCwd, setSftpFollowTerminalCwd, sftpAutoSync, sftpDefaultViewMode, sftpDoubleClickBehavior,
|
||||
sftpShowHiddenFiles, sftpUseCompressedUpload, shellHistory, snippetPackages, snippets, splitSessionWithCurrentShell, startSessionRename,
|
||||
startWorkspaceRename, submitSessionRename, submitWorkspaceRename, t, terminalFontFamilyId, terminalFontSize, terminalSettings, terminalThemeId, themeById,
|
||||
toggleBroadcast, toggleConnectionLogSaved, toggleScriptsSidePanelRef, toggleSidePanelRef, toggleWorkspaceViewMode, unmanageSource, updateConnectionLog,
|
||||
updateCustomGroups, updateGroupConfigs, updateHostDistro, updateHosts, updateIdentities, updateKeys, updateKnownHosts, updateManagedSources,
|
||||
updateNoteGroups, updateNotes, updateProxyProfiles, updateSnippetPackages, updateSnippets, updateSplitSizes, updateTerminalSetting, vaultFocusRequest, workspaceRenameTarget, workspaceRenameValue, workspaces,
|
||||
VaultViewContainer, SftpViewMount, TerminalLayerMount, LogViewWrapper,
|
||||
} = ctx;
|
||||
|
||||
const appThemeStyle = useMemo(() => {
|
||||
const tokens = getUiThemeById(
|
||||
resolvedTheme,
|
||||
resolvedTheme === 'dark' ? settings.darkUiThemeId : settings.lightUiThemeId,
|
||||
).tokens;
|
||||
return {
|
||||
...buildAppThemeCssVars(tokens, accentMode, customAccent),
|
||||
colorScheme: resolvedTheme,
|
||||
} as React.CSSProperties;
|
||||
}, [accentMode, customAccent, resolvedTheme, settings.darkUiThemeId, settings.lightUiThemeId]);
|
||||
|
||||
return (
|
||||
<SnippetExecutionProvider>
|
||||
<UnsavedChangesProvider>
|
||||
{({ prompt }) => {
|
||||
// Helper: close an editor tab and activate the neighbor (left-preference), or vault.
|
||||
const closeEditorAndActivateNeighbor = (id: string) => {
|
||||
const closingTabId = toEditorTabId(id);
|
||||
const list = orderedTabsWithEditors;
|
||||
const idx = list.indexOf(closingTabId);
|
||||
releaseEditorTabSaveCoordinator(id);
|
||||
editorTabStore.close(id);
|
||||
if (activeTabStore.getActiveTabId() !== closingTabId) return;
|
||||
const next = list[idx - 1] ?? list[idx + 1] ?? 'vault';
|
||||
activeTabStore.setActiveTabId(next === closingTabId ? 'vault' : next);
|
||||
};
|
||||
|
||||
// Real dirty-confirm close handler.
|
||||
const handleRequestCloseEditorTab = async (id: string): Promise<boolean> => {
|
||||
const tab = editorTabStore.getTab(id);
|
||||
if (!tab) return false;
|
||||
const dirty = tab.content !== tab.baselineContent;
|
||||
if (!dirty) {
|
||||
closeEditorAndActivateNeighbor(id);
|
||||
return true;
|
||||
}
|
||||
const choice = await prompt(tab.fileName);
|
||||
if (choice === 'cancel') return false;
|
||||
if (choice === 'discard') {
|
||||
closeEditorAndActivateNeighbor(id);
|
||||
return true;
|
||||
}
|
||||
if (choice === 'save') {
|
||||
const ok = await saveEditorTab(id);
|
||||
if (!ok) {
|
||||
const msg = editorTabStore.getTab(id)?.saveError ?? 'Save failed';
|
||||
toast.error(msg, 'SFTP');
|
||||
return false;
|
||||
}
|
||||
const latest = editorTabStore.getTab(id);
|
||||
if (!latest || latest.content !== latest.baselineContent) return false;
|
||||
closeEditorAndActivateNeighbor(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Expose to the hotkey dispatcher (Cmd/Ctrl+W).
|
||||
handleRequestCloseEditorTabRef.current = handleRequestCloseEditorTab;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen text-foreground font-sans netcatty-shell" data-terminal-appearance-root onContextMenu={handleRootContextMenu}>
|
||||
<TopTabs
|
||||
theme={resolvedTheme}
|
||||
hosts={hosts}
|
||||
sessions={sessions}
|
||||
orphanSessions={orphanSessions}
|
||||
workspaces={workspaces}
|
||||
logViews={logViews}
|
||||
orderedTabs={orderedTabsWithEditors}
|
||||
draggingSessionId={draggingSessionId}
|
||||
isMacClient={isMacClient}
|
||||
onCloseSession={closeSession}
|
||||
onRenameSession={startSessionRename}
|
||||
onCopySession={copySessionWithCurrentShell}
|
||||
onCopySessionToNewWindow={copySessionToNewWindowWithCurrentShell}
|
||||
onRenameWorkspace={startWorkspaceRename}
|
||||
onCloseWorkspace={closeWorkspace}
|
||||
onCloseLogView={closeLogView}
|
||||
onCloseTabsBatch={closeTabsBatch}
|
||||
onOpenQuickSwitcher={handleOpenQuickSwitcher}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
onOpenSettings={handleOpenSettings}
|
||||
windowOpacity={settings.windowOpacity}
|
||||
setWindowOpacity={settings.setWindowOpacity}
|
||||
onSyncNow={handleSyncNowManual}
|
||||
onStartSessionDrag={setDraggingSessionId}
|
||||
onEndSessionDrag={handleEndSessionDrag}
|
||||
onReorderTabs={reorderWorkTabs}
|
||||
onRemoveSessionFromWorkspace={removeSessionFromWorkspace}
|
||||
showSftpTab={settings.showSftpTab}
|
||||
showHostTreeSidebar={settings.showHostTreeSidebar}
|
||||
dynamicTabTitleMode={settings.terminalSettings.dynamicTabTitleMode}
|
||||
editorTabs={editorTabs}
|
||||
onRequestCloseEditorTab={handleRequestCloseEditorTab}
|
||||
hostById={hostById}
|
||||
/>
|
||||
|
||||
<div className="flex-1 relative min-h-0">
|
||||
<AppHostTreeLayer
|
||||
enabled={settings.showHostTreeSidebar}
|
||||
hosts={hosts}
|
||||
customGroups={customGroups}
|
||||
groupConfigs={groupConfigs}
|
||||
sessions={sessions}
|
||||
workspaces={workspaces}
|
||||
editorTabs={editorTabs}
|
||||
logViews={logViews}
|
||||
orderedTabs={orderedTabsWithEditors}
|
||||
accentMode={accentMode}
|
||||
currentTerminalTheme={currentTerminalTheme}
|
||||
customAccent={customAccent}
|
||||
followAppTerminalTheme={followAppTerminalTheme}
|
||||
hostById={hostById}
|
||||
themeById={themeById}
|
||||
resolveSessionAppearance={resolveSessionAppearance}
|
||||
onConnect={handleConnectToHost}
|
||||
onCreateLocalTerminal={handleCreateLocalTerminal}
|
||||
/>
|
||||
|
||||
<VaultViewContainer appThemeStyle={appThemeStyle}>
|
||||
<VaultView
|
||||
hosts={hosts}
|
||||
keys={keys}
|
||||
identities={identities}
|
||||
proxyProfiles={proxyProfiles}
|
||||
snippets={snippets}
|
||||
snippetPackages={snippetPackages}
|
||||
notes={notes}
|
||||
noteGroups={noteGroups}
|
||||
customGroups={customGroups}
|
||||
knownHosts={effectiveKnownHosts}
|
||||
shellHistory={shellHistory}
|
||||
connectionLogs={connectionLogs}
|
||||
managedSources={managedSources}
|
||||
sessionCount={sessions.length}
|
||||
hotkeyScheme={hotkeyScheme}
|
||||
keyBindings={keyBindings}
|
||||
terminalThemeId={terminalThemeId}
|
||||
terminalFontSize={terminalFontSize}
|
||||
onOpenSettings={handleOpenSettings}
|
||||
onOpenQuickSwitcher={handleOpenQuickSwitcher}
|
||||
onCreateLocalTerminal={handleCreateLocalTerminal}
|
||||
onConnectSerial={handleConnectSerial}
|
||||
onDeleteHost={handleDeleteHost}
|
||||
onConnect={handleConnectToHost}
|
||||
onOpenHostFromNote={handleOpenHostFromVaultNote}
|
||||
groupConfigs={groupConfigs}
|
||||
onUpdateGroupConfigs={updateGroupConfigs}
|
||||
onUpdateHosts={updateHosts}
|
||||
onUpdateKeys={updateKeys}
|
||||
onImportOrReuseKey={importOrReuseKey}
|
||||
onUpdateIdentities={updateIdentities}
|
||||
onUpdateProxyProfiles={updateProxyProfiles}
|
||||
onUpdateSnippets={updateSnippets}
|
||||
onUpdateSnippetPackages={updateSnippetPackages}
|
||||
onUpdateNotes={updateNotes}
|
||||
onUpdateNoteGroups={updateNoteGroups}
|
||||
onUpdateCustomGroups={updateCustomGroups}
|
||||
onUpdateKnownHosts={updateKnownHosts}
|
||||
onUpdateManagedSources={updateManagedSources}
|
||||
onClearAndRemoveManagedSource={clearAndRemoveSource}
|
||||
onClearAndRemoveManagedSources={clearAndRemoveSources}
|
||||
onUnmanageSource={unmanageSource}
|
||||
onConvertKnownHost={convertKnownHostToHost}
|
||||
onToggleConnectionLogSaved={toggleConnectionLogSaved}
|
||||
onDeleteConnectionLog={deleteConnectionLog}
|
||||
onClearUnsavedConnectionLogs={clearUnsavedConnectionLogs}
|
||||
onRunSnippet={runSnippet}
|
||||
onOpenLogView={openLogView}
|
||||
showRecentHosts={settings.showRecentHosts}
|
||||
showOnlyUngroupedHostsInRoot={settings.showOnlyUngroupedHostsInRoot}
|
||||
navigateToSection={navigateToSection}
|
||||
onNavigateToSectionHandled={() => setNavigateToSection(null)}
|
||||
deepLinkHostDraft={deepLinkHostDraft}
|
||||
onDeepLinkHostDraftHandled={() => setDeepLinkHostDraft(null)}
|
||||
vaultFocusRequest={vaultFocusRequest}
|
||||
onVaultFocusRequestHandled={() => setVaultFocusRequest(null)}
|
||||
terminalSettings={terminalSettings}
|
||||
/>
|
||||
</VaultViewContainer>
|
||||
|
||||
<SftpViewMount
|
||||
hosts={terminalHosts}
|
||||
writableHosts={hosts}
|
||||
sessions={sessions}
|
||||
keys={keys}
|
||||
identities={identities}
|
||||
knownHosts={effectiveKnownHosts}
|
||||
proxyProfiles={proxyProfiles}
|
||||
groupConfigs={groupConfigs}
|
||||
updateHosts={updateTerminalHosts}
|
||||
onAddKnownHost={handleAddKnownHost}
|
||||
sftpDefaultViewMode={sftpDefaultViewMode}
|
||||
sftpDoubleClickBehavior={sftpDoubleClickBehavior}
|
||||
sftpAutoSync={sftpAutoSync}
|
||||
sftpShowHiddenFiles={sftpShowHiddenFiles}
|
||||
sftpUseCompressedUpload={sftpUseCompressedUpload}
|
||||
hotkeyScheme={hotkeyScheme}
|
||||
keyBindings={keyBindings}
|
||||
editorWordWrap={editorWordWrap}
|
||||
setEditorWordWrap={setEditorWordWrap}
|
||||
terminalSettings={terminalSettings}
|
||||
/>
|
||||
|
||||
<TerminalLayerMount
|
||||
hosts={terminalHosts}
|
||||
portForwardingRules={portForwardingRules}
|
||||
customGroups={customGroups}
|
||||
groupConfigs={groupConfigs}
|
||||
proxyProfiles={proxyProfiles}
|
||||
keys={keys}
|
||||
identities={identities}
|
||||
snippets={snippets}
|
||||
snippetPackages={snippetPackages}
|
||||
notes={notes}
|
||||
noteGroups={noteGroups}
|
||||
sessions={sessions}
|
||||
workspaces={workspaces}
|
||||
knownHosts={effectiveKnownHosts}
|
||||
draggingSessionId={draggingSessionId}
|
||||
terminalTheme={currentTerminalTheme}
|
||||
terminalThemeId={terminalThemeId}
|
||||
followAppTerminalTheme={followAppTerminalTheme}
|
||||
pickTerminalTheme={ctx.pickTerminalTheme}
|
||||
clearThemeIntent={ctx.clearThemeIntent}
|
||||
settleManualThemeIntent={ctx.settleManualThemeIntent}
|
||||
resolveSessionAppearance={ctx.resolveSessionAppearance}
|
||||
accentMode={accentMode}
|
||||
customAccent={customAccent}
|
||||
terminalSettings={terminalSettings}
|
||||
terminalFontFamilyId={terminalFontFamilyId}
|
||||
fontSize={terminalFontSize}
|
||||
hotkeyScheme={hotkeyScheme}
|
||||
disableTerminalFontZoom={settings.disableTerminalFontZoom}
|
||||
restoreTerminalCwd={settings.restoreTerminalCwd}
|
||||
keyBindings={keyBindings}
|
||||
onHotkeyAction={handleHotkeyAction}
|
||||
onUpdateTerminalThemeId={handleDefaultTerminalThemeChange}
|
||||
onUpdateFollowAppTerminalThemeId={handleFollowAppTerminalThemeChange}
|
||||
onUpdateTerminalFontFamilyId={setTerminalFontFamilyId}
|
||||
onUpdateTerminalFontSize={setTerminalFontSize}
|
||||
onUpdateSessionFontSize={updateSessionFontSize}
|
||||
onUpdateSessionRestoreCwd={updateSessionRestoreCwd}
|
||||
onUpdateSessionDynamicTitle={updateSessionDynamicTitle}
|
||||
onUpdateSessionCodingCliProvider={updateSessionCodingCliProvider}
|
||||
onClearSessionFontSizeOverride={clearSessionFontSizeOverride}
|
||||
onUpdateTerminalFontWeight={(w) => updateTerminalSetting('fontWeight', w)}
|
||||
onCloseSession={closeSession}
|
||||
onUpdateSessionStatus={handleSessionStatusChange}
|
||||
onUpdateHostDistro={updateHostDistro}
|
||||
onUpdateHost={handleUpdateHostFromTerminal}
|
||||
onAddKnownHost={handleAddKnownHost}
|
||||
onCommandExecuted={(command, hostId, hostLabel, sessionId) => {
|
||||
addShellHistoryEntry({ command, hostId, hostLabel, sessionId });
|
||||
}}
|
||||
shellHistory={shellHistory}
|
||||
onTerminalDataCapture={handleTerminalDataCapture}
|
||||
onCreateWorkspaceFromSessions={createWorkspaceFromSessions}
|
||||
onAddSessionToWorkspace={addSessionToWorkspace}
|
||||
onRequestAddToWorkspace={(workspaceId) =>
|
||||
setAddToWorkspaceDialog({ mode: 'append', workspaceId })
|
||||
}
|
||||
onUpdateSplitSizes={updateSplitSizes}
|
||||
onSetDraggingSessionId={setDraggingSessionId}
|
||||
onToggleWorkspaceViewMode={toggleWorkspaceViewMode}
|
||||
onSetWorkspaceFocusedSession={setWorkspaceFocusedSession}
|
||||
onReorderWorkspaceSessions={reorderWorkspaceSessions}
|
||||
onReorderTabs={reorderWorkTabs}
|
||||
onCopySession={copySessionWithCurrentShell}
|
||||
onCopySessionToNewWindow={copySessionToNewWindowWithCurrentShell}
|
||||
onSplitSession={splitSessionWithCurrentShell}
|
||||
onConnectToHost={handleConnectToHost}
|
||||
openNoteRequest={openNoteRequest}
|
||||
onOpenVaultNoteFromChat={handleOpenVaultNoteFromChat}
|
||||
onOpenVaultHostFromChat={handleOpenVaultHostFromChat}
|
||||
onOpenVaultSectionFromChat={handleOpenVaultSectionFromChat}
|
||||
onOpenVaultSnippetFromChat={handleOpenVaultSnippetFromChat}
|
||||
onCreateLocalTerminal={handleCreateLocalTerminal}
|
||||
isBroadcastEnabled={isBroadcastEnabled}
|
||||
onToggleBroadcast={toggleBroadcast}
|
||||
updateHosts={updateTerminalHosts}
|
||||
updateSnippets={updateSnippets}
|
||||
updateSnippetPackages={updateSnippetPackages}
|
||||
updateNotes={updateNotes}
|
||||
updateNoteGroups={updateNoteGroups}
|
||||
sftpDefaultViewMode={sftpDefaultViewMode}
|
||||
sftpDoubleClickBehavior={sftpDoubleClickBehavior}
|
||||
sftpAutoSync={sftpAutoSync}
|
||||
sftpShowHiddenFiles={sftpShowHiddenFiles}
|
||||
sftpUseCompressedUpload={sftpUseCompressedUpload}
|
||||
sftpAutoOpenSidebar={sftpAutoOpenSidebar}
|
||||
terminalSidePanelAutoOpen={settings.terminalSidePanelAutoOpen}
|
||||
terminalSidePanelAutoOpenTab={settings.terminalSidePanelAutoOpenTab}
|
||||
sftpFollowTerminalCwd={sftpFollowTerminalCwd}
|
||||
setSftpFollowTerminalCwd={setSftpFollowTerminalCwd}
|
||||
editorWordWrap={editorWordWrap}
|
||||
setEditorWordWrap={setEditorWordWrap}
|
||||
sessionLogsEnabled={sessionLogsEnabled}
|
||||
sessionLogsDir={sessionLogsDir}
|
||||
sessionLogsFormat={sessionLogsFormat}
|
||||
sessionLogsTimestampsEnabled={sessionLogsTimestampsEnabled}
|
||||
sshDebugLogsEnabled={sshDebugLogsEnabled}
|
||||
showHostTreeSidebar={settings.showHostTreeSidebar}
|
||||
toggleScriptsSidePanelRef={toggleScriptsSidePanelRef}
|
||||
toggleSidePanelRef={toggleSidePanelRef}
|
||||
onStartSessionRename={startSessionRename}
|
||||
onSubmitSessionRename={submitSessionRename}
|
||||
onRemoveSessionFromWorkspace={removeSessionFromWorkspace}
|
||||
/>
|
||||
|
||||
{/* Log Views - readonly terminal replays */}
|
||||
{logViews.map(logView => {
|
||||
// Get the latest log data from connectionLogs to reflect updates
|
||||
const latestLog = connectionLogs.find(l => l.id === logView.connectionLogId) || logView.log;
|
||||
return (
|
||||
<LogViewWrapper
|
||||
key={logView.id}
|
||||
logView={{ ...logView, log: latestLog }}
|
||||
defaultTerminalTheme={currentTerminalTheme}
|
||||
defaultFontSize={terminalFontSize}
|
||||
onClose={() => closeLogView(logView.id)}
|
||||
onUpdateLog={updateConnectionLog}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Editor Tabs — kept mounted for Monaco instance persistence; visibility toggled via CSS */}
|
||||
{editorTabs.map((tab) => (
|
||||
<LazyLoadBoundary key={tab.id} name="Editor" resetKey={tab.id}>
|
||||
<Suspense fallback={<TextEditorTabFallback tabId={tab.id} />}>
|
||||
<LazyTextEditorTabView
|
||||
tabId={tab.id}
|
||||
hotkeyScheme={hotkeyScheme}
|
||||
keyBindings={keyBindings}
|
||||
hostById={hostById}
|
||||
onRequestClose={(id) => handleRequestCloseEditorTabRef.current(id)}
|
||||
/>
|
||||
</Suspense>
|
||||
</LazyLoadBoundary>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Global "quick add / edit snippet" dialog, triggered by the
|
||||
netcatty:snippets:add and :edit window events (from ScriptsSidePanel
|
||||
"+" button and right-click menu). Delete is handled by a sibling
|
||||
useEffect above — it does not need a dialog. */}
|
||||
<QuickAddSnippetDialog
|
||||
snippets={snippets}
|
||||
packages={snippetPackages}
|
||||
onCreateSnippet={(snippet) => updateSnippets([...snippets, snippet])}
|
||||
onUpdateSnippet={(snippet) =>
|
||||
updateSnippets(snippets.map((s) => (s.id === snippet.id ? snippet : s)))
|
||||
}
|
||||
onCreatePackage={(pkg) =>
|
||||
updateSnippetPackages(Array.from(new Set([...snippetPackages, pkg])))
|
||||
}
|
||||
/>
|
||||
<QuickScriptEditorDialog
|
||||
snippets={snippets}
|
||||
packages={snippetPackages}
|
||||
hosts={hosts}
|
||||
customGroups={customGroups}
|
||||
onCreateSnippet={(snippet) => updateSnippets([...snippets, snippet])}
|
||||
onUpdateSnippet={(snippet) =>
|
||||
updateSnippets(snippets.map((s) => (s.id === snippet.id ? snippet : s)))
|
||||
}
|
||||
onCreatePackage={(pkg) =>
|
||||
updateSnippetPackages(Array.from(new Set([...snippetPackages, pkg])))
|
||||
}
|
||||
onUpdateHosts={updateHosts}
|
||||
onRunSnippet={runSnippet}
|
||||
/>
|
||||
|
||||
{/* Root-mounted AddToWorkspaceDialog — triggered by the focus-mode
|
||||
"+" button (mode='append') or QuickSwitcher's "New Workspace"
|
||||
button (mode='create'). Single instance so dialog state and
|
||||
styling stay consistent across entry points. */}
|
||||
{addToWorkspaceDialog && (
|
||||
<AddToWorkspaceDialog
|
||||
open
|
||||
onOpenChange={(open) => { if (!open) setAddToWorkspaceDialog(null); }}
|
||||
// Filter serial hosts only in append mode — appendHostToWorkspace
|
||||
// has no serial code path. Create mode goes through
|
||||
// createWorkspaceFromTargets, which builds a SerialConfig-backed
|
||||
// session for serial hosts, so those should remain pickable.
|
||||
hosts={addToWorkspaceDialog.mode === 'append'
|
||||
? hosts.filter((h) => h.protocol !== 'serial')
|
||||
: hosts}
|
||||
workspaceTitle={
|
||||
addToWorkspaceDialog.mode === 'append'
|
||||
? workspaces.find((w) => w.id === addToWorkspaceDialog.workspaceId)?.title
|
||||
: 'New Workspace'
|
||||
}
|
||||
onAdd={(targets) => {
|
||||
if (addToWorkspaceDialog.mode === 'append') {
|
||||
// Match the workspace root's current split direction so
|
||||
// the new panes peer the existing siblings instead of
|
||||
// wrapping the whole tree into one side of a fresh split
|
||||
// (which would happen if we always passed the helper's
|
||||
// default 'vertical').
|
||||
const ws = workspaces.find((w) => w.id === addToWorkspaceDialog.workspaceId);
|
||||
const rootDir = ws && ws.root.type === 'split' ? ws.root.direction : 'vertical';
|
||||
for (const target of targets) {
|
||||
if (target.kind === 'local') {
|
||||
appendLocalTerminalToWorkspace(addToWorkspaceDialog.workspaceId, undefined, rootDir);
|
||||
} else {
|
||||
appendHostToWorkspace(addToWorkspaceDialog.workspaceId, target.host, rootDir);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createWorkspaceFromTargets(targets);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isQuickSwitcherOpen && (
|
||||
<LazyLoadBoundary name="Quick switcher" resetKey={quickSearch}>
|
||||
<Suspense fallback={null}>
|
||||
<LazyQuickSwitcher
|
||||
isOpen={isQuickSwitcherOpen}
|
||||
query={quickSearch}
|
||||
results={quickResults}
|
||||
sessions={sessions}
|
||||
workspaces={workspaces}
|
||||
showSftpTab={settings.showSftpTab}
|
||||
onQueryChange={setQuickSearch}
|
||||
onSelect={handleHostConnectWithProtocolCheck}
|
||||
onSelectTab={(tabId) => {
|
||||
setActiveTabId(tabId);
|
||||
setIsQuickSwitcherOpen(false);
|
||||
setQuickSearch('');
|
||||
}}
|
||||
onCreateLocalTerminal={(shell) => {
|
||||
handleCreateLocalTerminal(shell);
|
||||
setIsQuickSwitcherOpen(false);
|
||||
setQuickSearch('');
|
||||
}}
|
||||
onCreateWorkspace={() => {
|
||||
setIsQuickSwitcherOpen(false);
|
||||
setQuickSearch('');
|
||||
setAddToWorkspaceDialog({ mode: 'create' });
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsQuickSwitcherOpen(false);
|
||||
setQuickSearch('');
|
||||
}}
|
||||
keyBindings={keyBindings}
|
||||
terminalSettings={terminalSettings}
|
||||
/>
|
||||
</Suspense>
|
||||
</LazyLoadBoundary>
|
||||
)}
|
||||
|
||||
<Dialog open={!!sessionRenameTarget} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
resetSessionRename();
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('dialog.renameSession.title')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 py-2">
|
||||
<Label htmlFor="session-name">{t('field.name')}</Label>
|
||||
<Input
|
||||
id="session-name"
|
||||
value={sessionRenameValue}
|
||||
onChange={(e) => setSessionRenameValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') submitSessionRename(); }}
|
||||
autoFocus
|
||||
placeholder={t('placeholder.sessionName')}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={resetSessionRename}>{t('common.cancel')}</Button>
|
||||
<Button onClick={submitSessionRename} disabled={!sessionRenameValue.trim()}>{t('common.save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={!!workspaceRenameTarget} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
resetWorkspaceRename();
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('dialog.renameWorkspace.title')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2 py-2">
|
||||
<Label htmlFor="workspace-name">{t('field.name')}</Label>
|
||||
<Input
|
||||
id="workspace-name"
|
||||
value={workspaceRenameValue}
|
||||
onChange={(e) => setWorkspaceRenameValue(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') submitWorkspaceRename(); }}
|
||||
autoFocus
|
||||
placeholder={t('placeholder.workspaceName')}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={resetWorkspaceRename}>{t('common.cancel')}</Button>
|
||||
<Button onClick={submitWorkspaceRename} disabled={!workspaceRenameValue.trim()}>{t('common.save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{isCreateWorkspaceOpen && (
|
||||
<LazyLoadBoundary name="Create workspace" resetKey="create-workspace">
|
||||
<Suspense fallback={null}>
|
||||
<LazyCreateWorkspaceDialog
|
||||
isOpen={isCreateWorkspaceOpen}
|
||||
onClose={() => setIsCreateWorkspaceOpen(false)}
|
||||
hosts={hosts}
|
||||
onCreate={createWorkspaceWithHosts}
|
||||
/>
|
||||
</Suspense>
|
||||
</LazyLoadBoundary>
|
||||
)}
|
||||
|
||||
{/* Protocol Select Dialog for QuickSwitcher */}
|
||||
{protocolSelectHost && (
|
||||
<LazyLoadBoundary name="Protocol selector" resetKey={protocolSelectHost.id}>
|
||||
<Suspense fallback={null}>
|
||||
<LazyProtocolSelectDialog
|
||||
host={protocolSelectHost}
|
||||
onSelect={handleProtocolSelect}
|
||||
onCancel={() => setProtocolSelectHost(null)}
|
||||
/>
|
||||
</Suspense>
|
||||
</LazyLoadBoundary>
|
||||
)}
|
||||
|
||||
{/* Global Keyboard-Interactive Authentication Modal (2FA/MFA) - processes queue */}
|
||||
<KeyboardInteractiveModal
|
||||
request={keyboardInteractiveQueue[0] || null}
|
||||
onSubmit={handleKeyboardInteractiveSubmit}
|
||||
onCancel={handleKeyboardInteractiveCancel}
|
||||
/>
|
||||
{/* Indicator when more 2FA requests are pending */}
|
||||
{keyboardInteractiveQueue.length > 1 && (
|
||||
<div className="fixed bottom-4 right-4 z-50 bg-muted/90 backdrop-blur-sm text-sm px-3 py-1.5 rounded-full border shadow-sm">
|
||||
{keyboardInteractiveQueue.length - 1} more pending
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Global Passphrase Modal for encrypted SSH keys */}
|
||||
<PassphraseModal
|
||||
request={passphraseQueue[0] || null}
|
||||
onSubmit={handlePassphraseSubmit}
|
||||
onCancel={handlePassphraseCancel}
|
||||
onSkip={handlePassphraseSkip}
|
||||
/>
|
||||
|
||||
{/* Empty vault vs cloud data confirmation dialog (#679).
|
||||
This dialog intentionally cannot be dismissed — the user MUST
|
||||
choose "Restore" or "Keep Empty" before the sync flow can
|
||||
proceed. hideCloseButton removes the X button, onOpenChange
|
||||
is a no-op so ESC also does nothing, and onInteractOutside
|
||||
prevents click-away. */}
|
||||
<Dialog open={!!emptyVaultConflict} onOpenChange={() => { /* intentionally non-dismissable */ }}>
|
||||
<DialogContent className="max-w-md" hideCloseButton onInteractOutside={(e) => e.preventDefault()} onEscapeKeyDown={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-amber-500" />
|
||||
{t('sync.autoSync.emptyVaultConflict.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('sync.autoSync.emptyVaultConflict.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{emptyVaultConflict && (
|
||||
<div className="bg-muted/30 rounded-lg p-3 text-sm">
|
||||
<div className="font-medium text-muted-foreground mb-1">{t('sync.autoSync.emptyVaultConflict.cloudLabel')}</div>
|
||||
<div>{t('sync.autoSync.emptyVaultConflict.cloudSummary', {
|
||||
hosts: emptyVaultConflict.hostCount,
|
||||
keys: emptyVaultConflict.keyCount,
|
||||
snippets: emptyVaultConflict.snippetCount,
|
||||
notes: emptyVaultConflict.noteCount,
|
||||
proxyProfiles: emptyVaultConflict.proxyProfileCount,
|
||||
})}</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter className="flex-col gap-2 sm:flex-col">
|
||||
<Button
|
||||
onClick={() => resolveEmptyVaultConflict('restore')}
|
||||
className="w-full justify-start gap-2"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>
|
||||
{t('sync.autoSync.emptyVaultConflict.restore')}
|
||||
<span className="text-xs opacity-70 ml-1">— {t('sync.autoSync.emptyVaultConflict.restoreDesc')}</span>
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => resolveEmptyVaultConflict('keep-empty')}
|
||||
className="w-full justify-start gap-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>
|
||||
{t('sync.autoSync.emptyVaultConflict.keepEmpty')}
|
||||
<span className="text-xs opacity-70 ml-1">— {t('sync.autoSync.emptyVaultConflict.keepEmptyDesc')}</span>
|
||||
</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</UnsavedChangesProvider>
|
||||
</SnippetExecutionProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { toEditorTabId } from "../state/activeTabStore.ts";
|
||||
import type { EditorTab } from "../state/editorTabStore.ts";
|
||||
import type { LogView } from "../state/logViewState.ts";
|
||||
import { isActiveChromeThemeResolvable, resolveActiveChromeTheme } from "./activeChromeTheme.ts";
|
||||
import type { Host, TerminalSession, TerminalTheme, Workspace } from "../../types";
|
||||
|
||||
const theme = (id: string, type: "dark" | "light" = "dark"): TerminalTheme => ({
|
||||
id,
|
||||
name: id,
|
||||
type,
|
||||
colors: {
|
||||
background: type === "dark" ? "#111111" : "#eeeeee",
|
||||
foreground: type === "dark" ? "#eeeeee" : "#111111",
|
||||
cursor: "#22aaff",
|
||||
},
|
||||
});
|
||||
|
||||
const currentTheme = theme("current");
|
||||
const hostTheme = theme("host-theme");
|
||||
const logTheme = theme("log-theme", "light");
|
||||
|
||||
const baseInput = {
|
||||
accentMode: "theme" as const,
|
||||
currentTerminalTheme: currentTheme,
|
||||
customAccent: "221.2 83.2% 53.3%",
|
||||
editorTabs: [],
|
||||
followAppTerminalTheme: false,
|
||||
hostById: new Map<string, Host>(),
|
||||
logViews: [],
|
||||
sessionById: new Map<string, TerminalSession>(),
|
||||
themeById: new Map([
|
||||
[currentTheme.id, currentTheme],
|
||||
[hostTheme.id, hostTheme],
|
||||
[logTheme.id, logTheme],
|
||||
]),
|
||||
workspaceById: new Map<string, Workspace>(),
|
||||
};
|
||||
|
||||
test("editor tabs use the owning host terminal theme when follow-app terminal theme is off", () => {
|
||||
const editorTab = {
|
||||
id: "editor-1",
|
||||
hostId: "host-1",
|
||||
sessionId: "sftp-1",
|
||||
};
|
||||
|
||||
const resolved = resolveActiveChromeTheme({
|
||||
...baseInput,
|
||||
activeTabId: toEditorTabId(editorTab.id),
|
||||
editorTabs: [editorTab as unknown as EditorTab],
|
||||
hostById: new Map([
|
||||
["host-1", { id: "host-1", theme: hostTheme.id } as unknown as Host],
|
||||
]),
|
||||
});
|
||||
|
||||
assert.equal(resolved?.id, hostTheme.id);
|
||||
});
|
||||
|
||||
test("editor tabs use the followed terminal theme when follow-app terminal theme is on", () => {
|
||||
const editorTab = {
|
||||
id: "editor-1",
|
||||
hostId: "host-1",
|
||||
sessionId: "sftp-1",
|
||||
};
|
||||
|
||||
const resolved = resolveActiveChromeTheme({
|
||||
...baseInput,
|
||||
activeTabId: toEditorTabId(editorTab.id),
|
||||
editorTabs: [editorTab as unknown as EditorTab],
|
||||
followAppTerminalTheme: true,
|
||||
hostById: new Map([
|
||||
["host-1", { id: "host-1", theme: hostTheme.id } as unknown as Host],
|
||||
]),
|
||||
});
|
||||
|
||||
assert.equal(resolved?.id, currentTheme.id);
|
||||
});
|
||||
|
||||
test("log tabs use the saved log theme when available", () => {
|
||||
const resolved = resolveActiveChromeTheme({
|
||||
...baseInput,
|
||||
activeTabId: "log-1",
|
||||
logViews: [{
|
||||
id: "log-1",
|
||||
connectionLogId: "1",
|
||||
log: { id: "1", themeId: logTheme.id },
|
||||
} as unknown as LogView],
|
||||
});
|
||||
|
||||
assert.equal(resolved?.id, logTheme.id);
|
||||
});
|
||||
|
||||
test("root pages use the normal application theme", () => {
|
||||
const resolved = resolveActiveChromeTheme({
|
||||
...baseInput,
|
||||
activeTabId: "vault",
|
||||
});
|
||||
|
||||
assert.equal(resolved, null);
|
||||
});
|
||||
|
||||
test("follow-app workspace split view always uses the global terminal theme", () => {
|
||||
const workspace: Workspace = {
|
||||
id: "ws-1",
|
||||
name: "Workspace",
|
||||
viewMode: "split",
|
||||
focusedSessionId: "session-1",
|
||||
root: {
|
||||
type: "split",
|
||||
direction: "horizontal",
|
||||
sizes: [50, 50],
|
||||
children: [
|
||||
{ type: "session", sessionId: "session-1" },
|
||||
{ type: "session", sessionId: "session-2" },
|
||||
],
|
||||
},
|
||||
} as unknown as Workspace;
|
||||
|
||||
const hostA = { id: "host-a", theme: hostTheme.id, themeOverride: true } as unknown as Host;
|
||||
const hostB = { id: "host-b", theme: logTheme.id, themeOverride: true } as unknown as Host;
|
||||
|
||||
const resolved = resolveActiveChromeTheme({
|
||||
...baseInput,
|
||||
activeTabId: "ws-1",
|
||||
followAppTerminalTheme: true,
|
||||
hostById: new Map([
|
||||
["host-a", hostA],
|
||||
["host-b", hostB],
|
||||
]),
|
||||
sessionById: new Map([
|
||||
["session-1", { id: "session-1", hostId: "host-a" } as TerminalSession],
|
||||
["session-2", { id: "session-2", hostId: "host-b" } as TerminalSession],
|
||||
]),
|
||||
workspaceById: new Map([["ws-1", workspace]]),
|
||||
});
|
||||
|
||||
assert.equal(resolved?.id, currentTheme.id);
|
||||
});
|
||||
|
||||
test("manual workspace split view uses the focused session theme when panes differ", () => {
|
||||
const workspace: Workspace = {
|
||||
id: "ws-1",
|
||||
name: "Workspace",
|
||||
viewMode: "split",
|
||||
focusedSessionId: "session-2",
|
||||
root: {
|
||||
type: "split",
|
||||
direction: "horizontal",
|
||||
sizes: [50, 50],
|
||||
children: [
|
||||
{ type: "pane", sessionId: "session-1" },
|
||||
{ type: "pane", sessionId: "session-2" },
|
||||
],
|
||||
},
|
||||
} as unknown as Workspace;
|
||||
|
||||
const hostA = { id: "host-a", theme: hostTheme.id, themeOverride: true } as unknown as Host;
|
||||
const hostB = { id: "host-b", theme: logTheme.id, themeOverride: true } as unknown as Host;
|
||||
const focusedTheme = theme("focused-intent");
|
||||
|
||||
const resolved = resolveActiveChromeTheme({
|
||||
...baseInput,
|
||||
activeTabId: "ws-1",
|
||||
hostById: new Map([
|
||||
["host-a", hostA],
|
||||
["host-b", hostB],
|
||||
]),
|
||||
sessionById: new Map([
|
||||
["session-1", { id: "session-1", hostId: "host-a" } as TerminalSession],
|
||||
["session-2", { id: "session-2", hostId: "host-b" } as TerminalSession],
|
||||
]),
|
||||
workspaceById: new Map([["ws-1", workspace]]),
|
||||
resolveSessionAppearance: ({ host }) => (
|
||||
host?.id === "host-b"
|
||||
? { themeId: focusedTheme.id, theme: focusedTheme, source: "intent", appThemeUpdate: null }
|
||||
: { themeId: hostTheme.id, theme: hostTheme, source: "host-override", appThemeUpdate: null }
|
||||
),
|
||||
});
|
||||
|
||||
assert.equal(resolved?.id, focusedTheme.id);
|
||||
});
|
||||
|
||||
test("manual split workspace falls back to the first tree session theme when focus is stale", () => {
|
||||
const workspace: Workspace = {
|
||||
id: "ws-1",
|
||||
name: "Workspace",
|
||||
viewMode: "split",
|
||||
focusedSessionId: "missing-session",
|
||||
root: {
|
||||
type: "split",
|
||||
direction: "horizontal",
|
||||
sizes: [50, 50],
|
||||
children: [
|
||||
{ type: "pane", sessionId: "session-1" },
|
||||
{ type: "pane", sessionId: "session-2" },
|
||||
],
|
||||
},
|
||||
} as unknown as Workspace;
|
||||
|
||||
const hostA = { id: "host-a", theme: hostTheme.id, themeOverride: true } as unknown as Host;
|
||||
const hostB = { id: "host-b", theme: logTheme.id, themeOverride: true } as unknown as Host;
|
||||
|
||||
const resolved = resolveActiveChromeTheme({
|
||||
...baseInput,
|
||||
activeTabId: "ws-1",
|
||||
hostById: new Map([
|
||||
["host-a", hostA],
|
||||
["host-b", hostB],
|
||||
]),
|
||||
sessionById: new Map([
|
||||
["session-1", { id: "session-1", hostId: "host-a" } as TerminalSession],
|
||||
["session-2", { id: "session-2", hostId: "host-b" } as TerminalSession],
|
||||
]),
|
||||
workspaceById: new Map([["ws-1", workspace]]),
|
||||
});
|
||||
|
||||
assert.equal(resolved?.id, hostTheme.id);
|
||||
});
|
||||
|
||||
test("manual mode prefers runtime session appearance over stale host theme ids", () => {
|
||||
const intentTheme = theme("intent-theme");
|
||||
const resolved = resolveActiveChromeTheme({
|
||||
...baseInput,
|
||||
activeTabId: "session-1",
|
||||
hostById: new Map([
|
||||
["host-1", { id: "host-1", theme: hostTheme.id, themeOverride: true } as unknown as Host],
|
||||
]),
|
||||
sessionById: new Map([
|
||||
["session-1", { id: "session-1", hostId: "host-1" } as TerminalSession],
|
||||
]),
|
||||
resolveSessionAppearance: () => ({
|
||||
themeId: intentTheme.id,
|
||||
theme: intentTheme,
|
||||
source: "intent",
|
||||
appThemeUpdate: null,
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(resolved?.id, intentTheme.id);
|
||||
});
|
||||
|
||||
test("chrome theme sync waits until a newly opened session is present in deps", () => {
|
||||
assert.equal(
|
||||
isActiveChromeThemeResolvable({
|
||||
activeTabId: "session-new",
|
||||
editorTabs: [],
|
||||
logViews: [],
|
||||
sessionById: new Map(),
|
||||
workspaceById: new Map(),
|
||||
}),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
isActiveChromeThemeResolvable({
|
||||
activeTabId: "session-new",
|
||||
editorTabs: [],
|
||||
logViews: [],
|
||||
sessionById: new Map([["session-new", { id: "session-new" } as TerminalSession]]),
|
||||
workspaceById: new Map(),
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { fromEditorTabId, isEditorTabId } from "../state/activeTabStore";
|
||||
|
||||
import { applyCustomAccentToTerminalTheme, resolveHostTerminalThemeId } from "../../domain/terminalAppearance";
|
||||
import type {
|
||||
ResolvedAppearance,
|
||||
TerminalAppearanceHostScope,
|
||||
} from "../../domain/terminalAppearanceRuntime";
|
||||
import { collectSessionIds } from "../../domain/workspace";
|
||||
import type { EditorTab } from "../state/editorTabStore";
|
||||
import type { LogView } from "../state/logViewState";
|
||||
import type { Host, TerminalSession, TerminalTheme, Workspace } from "../../types";
|
||||
import { resolveWorkspaceTargetSessionFromMap } from "./workTabSurface";
|
||||
|
||||
export type ResolveActiveChromeThemeInput = {
|
||||
accentMode: "theme" | "custom";
|
||||
activeTabId: string;
|
||||
currentTerminalTheme: TerminalTheme;
|
||||
customAccent: string;
|
||||
editorTabs: readonly EditorTab[];
|
||||
followAppTerminalTheme: boolean;
|
||||
hostById: Map<string, Host>;
|
||||
logViews: readonly LogView[];
|
||||
resolveSessionAppearance?: (hostScope: TerminalAppearanceHostScope) => ResolvedAppearance;
|
||||
sessionById: Map<string, TerminalSession>;
|
||||
themeById: Map<string, TerminalTheme>;
|
||||
workspaceById: Map<string, Workspace>;
|
||||
};
|
||||
|
||||
export function isActiveChromeThemeResolvable({
|
||||
activeTabId,
|
||||
editorTabs,
|
||||
logViews,
|
||||
sessionById,
|
||||
workspaceById,
|
||||
}: Pick<
|
||||
ResolveActiveChromeThemeInput,
|
||||
"activeTabId" | "editorTabs" | "logViews" | "sessionById" | "workspaceById"
|
||||
>): boolean {
|
||||
if (activeTabId === "vault" || activeTabId === "sftp") return true;
|
||||
if (isEditorTabId(activeTabId)) {
|
||||
return editorTabs.some((tab) => tab.id === fromEditorTabId(activeTabId));
|
||||
}
|
||||
if (logViews.some((item) => item.id === activeTabId)) return true;
|
||||
if (workspaceById.has(activeTabId)) return true;
|
||||
if (sessionById.has(activeTabId)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function resolveActiveChromeTheme({
|
||||
accentMode,
|
||||
activeTabId,
|
||||
currentTerminalTheme,
|
||||
customAccent,
|
||||
editorTabs,
|
||||
followAppTerminalTheme,
|
||||
hostById,
|
||||
logViews,
|
||||
resolveSessionAppearance,
|
||||
sessionById,
|
||||
themeById,
|
||||
workspaceById,
|
||||
}: ResolveActiveChromeThemeInput): TerminalTheme | null {
|
||||
if (activeTabId === "vault" || activeTabId === "sftp") return null;
|
||||
|
||||
const resolveHostScope = (hostId: string): TerminalAppearanceHostScope => {
|
||||
const host = hostById.get(hostId) ?? null;
|
||||
return { host, isEphemeral: !host || !hostById.has(host.id) };
|
||||
};
|
||||
|
||||
const resolveHostTheme = (hostId: string): TerminalTheme => {
|
||||
if (followAppTerminalTheme) return currentTerminalTheme;
|
||||
if (resolveSessionAppearance) {
|
||||
return resolveSessionAppearance(resolveHostScope(hostId)).theme;
|
||||
}
|
||||
const host = hostById.get(hostId) ?? null;
|
||||
const themeId = resolveHostTerminalThemeId(host, currentTerminalTheme.id);
|
||||
const baseTheme = themeById.get(themeId) ?? currentTerminalTheme;
|
||||
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
|
||||
};
|
||||
|
||||
const resolveSessionTheme = (session: TerminalSession): TerminalTheme => resolveHostTheme(session.hostId);
|
||||
|
||||
if (isEditorTabId(activeTabId)) {
|
||||
const editorTabId = fromEditorTabId(activeTabId);
|
||||
const editorTab = editorTabs.find((tab) => tab.id === editorTabId);
|
||||
if (!editorTab) return null;
|
||||
return resolveHostTheme(editorTab.hostId);
|
||||
}
|
||||
|
||||
const logView = logViews.find((item) => item.id === activeTabId);
|
||||
if (logView) {
|
||||
const explicitThemeId = logView.log.themeId;
|
||||
return explicitThemeId ? themeById.get(explicitThemeId) ?? currentTerminalTheme : currentTerminalTheme;
|
||||
}
|
||||
|
||||
const workspace = workspaceById.get(activeTabId);
|
||||
if (workspace) {
|
||||
if (followAppTerminalTheme) return currentTerminalTheme;
|
||||
|
||||
if (workspace.viewMode === "focus") {
|
||||
const focusedSession = resolveWorkspaceTargetSessionFromMap(workspace, sessionById);
|
||||
return focusedSession ? resolveSessionTheme(focusedSession) : null;
|
||||
}
|
||||
|
||||
const workspaceSessions = collectSessionIds(workspace.root)
|
||||
.map((id) => sessionById.get(id))
|
||||
.filter(Boolean) as TerminalSession[];
|
||||
if (workspaceSessions.length === 0) return null;
|
||||
|
||||
const firstTheme = resolveSessionTheme(workspaceSessions[0]);
|
||||
const allSame = workspaceSessions.every((session) => resolveSessionTheme(session).id === firstTheme.id);
|
||||
if (allSame) return firstTheme;
|
||||
|
||||
const focusedSession = resolveWorkspaceTargetSessionFromMap(workspace, sessionById);
|
||||
return focusedSession ? resolveSessionTheme(focusedSession) : null;
|
||||
}
|
||||
|
||||
const session = sessionById.get(activeTabId);
|
||||
return session ? resolveSessionTheme(session) : null;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { shouldQueueKeyboardInteractiveRequest } from "./useAppStartupEffects.ts";
|
||||
|
||||
const sessions = [{ id: "terminal-1" }, { id: "terminal-2" }];
|
||||
|
||||
test("terminal-scoped keyboard-interactive requests are limited to owned sessions", () => {
|
||||
assert.equal(
|
||||
shouldQueueKeyboardInteractiveRequest({ scope: "terminal", sessionId: "terminal-1" }, sessions),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldQueueKeyboardInteractiveRequest({ scope: "terminal", sessionId: "foreign-terminal" }, sessions),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("external keyboard-interactive requests are not filtered by terminal session ids", () => {
|
||||
assert.equal(
|
||||
shouldQueueKeyboardInteractiveRequest({ scope: "external", sessionId: "sftp-conn-1" }, sessions),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldQueueKeyboardInteractiveRequest({ scope: "external", sessionId: "tunnel-1" }, sessions),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("disabled peer windows still queue sender-targeted external keyboard-interactive requests", () => {
|
||||
assert.equal(
|
||||
shouldQueueKeyboardInteractiveRequest({ scope: "external", sessionId: "sftp-conn-1" }, sessions),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("disabled peer windows can still queue owned terminal keyboard-interactive requests", () => {
|
||||
assert.equal(
|
||||
shouldQueueKeyboardInteractiveRequest({ scope: "terminal", sessionId: "terminal-1" }, sessions),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("legacy unscoped keyboard-interactive requests remain visible", () => {
|
||||
assert.equal(
|
||||
shouldQueueKeyboardInteractiveRequest({ sessionId: "legacy-conn" }, sessions),
|
||||
true,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { buildNumberShortcutTabTargets } from './tabShortcutTargets.ts';
|
||||
|
||||
test('number shortcut tabs include vault and sftp by default', () => {
|
||||
assert.deepEqual(
|
||||
buildNumberShortcutTabTargets({
|
||||
showSftpTab: true,
|
||||
shellOnlyTabNumberShortcuts: false,
|
||||
orderedTabs: ['session-1', 'workspace-1'],
|
||||
editorTabIds: ['editor:file-1'],
|
||||
}),
|
||||
['vault', 'sftp', 'session-1', 'workspace-1', 'editor:file-1'],
|
||||
);
|
||||
});
|
||||
|
||||
test('number shortcut tabs skip vault and sftp when shell-only mode is enabled', () => {
|
||||
assert.deepEqual(
|
||||
buildNumberShortcutTabTargets({
|
||||
showSftpTab: true,
|
||||
shellOnlyTabNumberShortcuts: true,
|
||||
orderedTabs: ['session-1', 'workspace-1'],
|
||||
editorTabIds: ['editor:file-1'],
|
||||
}),
|
||||
['session-1', 'workspace-1', 'editor:file-1'],
|
||||
);
|
||||
});
|
||||
|
||||
test('hidden sftp tab is omitted from default number shortcut targets', () => {
|
||||
assert.deepEqual(
|
||||
buildNumberShortcutTabTargets({
|
||||
showSftpTab: false,
|
||||
shellOnlyTabNumberShortcuts: false,
|
||||
orderedTabs: ['session-1'],
|
||||
editorTabIds: [],
|
||||
}),
|
||||
['vault', 'session-1'],
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/** Tab ids targeted by keyboard tab navigation shortcuts. */
|
||||
export function buildNumberShortcutTabTargets(params: {
|
||||
showSftpTab: boolean;
|
||||
shellOnlyTabNumberShortcuts: boolean;
|
||||
orderedTabs: readonly string[];
|
||||
editorTabIds: readonly string[];
|
||||
}): string[] {
|
||||
const workTabs = [...params.orderedTabs, ...params.editorTabIds];
|
||||
if (params.shellOnlyTabNumberShortcuts) {
|
||||
return workTabs;
|
||||
}
|
||||
const pinnedTabs = params.showSftpTab ? ['vault', 'sftp'] : ['vault'];
|
||||
return [...pinnedTabs, ...workTabs];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
test("active chrome theme applies top tab vars and clears them before vault restore transition", () => {
|
||||
const chromeThemeSource = readFileSync(new URL("../state/useActiveChromeTheme.ts", import.meta.url), "utf8");
|
||||
const syncSource = readFileSync(new URL("../state/activeChromeThemeSync.ts", import.meta.url), "utf8");
|
||||
const effectsSource = readFileSync(new URL("../../components/terminalLayer/useTerminalLayerEffects.ts", import.meta.url), "utf8");
|
||||
|
||||
assert.match(chromeThemeSource, /applyTopTabsChromeThemeVars\(theme\)/);
|
||||
assert.match(chromeThemeSource, /resolveReadableForegroundForHsl\(cursor\)/);
|
||||
const restoreBlock = chromeThemeSource.match(
|
||||
/clearTopTabsChromeThemeVars\(\);\s*runThemeTransition\(\(\) => \{\s*removeActiveChromeTheme\(\);/,
|
||||
)?.[0] ?? "";
|
||||
assert.notEqual(restoreBlock, "", "top tab vars must clear before the vault restore transition starts");
|
||||
assert.match(syncSource, /activeTabId === 'vault' \|\| activeTabId === 'sftp'\)[\s\S]*clearTopTabsChromeThemeVars\(\)/);
|
||||
assert.match(effectsSource, /if \(!isTerminalLayerVisible\) \{[\s\S]*clearTopTabsPreviewVars\(\)/);
|
||||
});
|
||||
|
||||
test("top tabs chrome theme keeps accent foreground in sync", () => {
|
||||
const source = readFileSync(new URL("./topTabsChromeTheme.ts", import.meta.url), "utf8");
|
||||
const supportSource = readFileSync(new URL("../../components/terminalLayer/TerminalLayerSupport.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(source, /--primary-foreground/);
|
||||
assert.match(source, /--accent-foreground/);
|
||||
assert.match(source, /resolveReadableForegroundForHsl\(accent\)/);
|
||||
assert.match(supportSource, /removeStylePropertyIfSet\(tabsRoot, '--primary-foreground'\)/);
|
||||
assert.match(supportSource, /removeStylePropertyIfSet\(tabsRoot, '--accent-foreground'\)/);
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { TerminalTheme } from '../../types';
|
||||
import { resolveReadableForegroundForHsl } from '../../domain/colorContrast';
|
||||
|
||||
function hexToHslToken(hex: string): string {
|
||||
const normalized = hex.startsWith('#') ? hex : `#${hex}`;
|
||||
const r = parseInt(normalized.slice(1, 3), 16) / 255;
|
||||
const g = parseInt(normalized.slice(3, 5), 16) / 255;
|
||||
const b = parseInt(normalized.slice(5, 7), 16) / 255;
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
||||
break;
|
||||
case g:
|
||||
h = ((b - r) / d + 2) / 6;
|
||||
break;
|
||||
default:
|
||||
h = ((r - g) / d + 4) / 6;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return `${Math.round(h * 3600) / 10} ${Math.round(s * 1000) / 10}% ${Math.round(l * 1000) / 10}%`;
|
||||
}
|
||||
|
||||
function adjustLightnessToken(hsl: string, delta: number): string {
|
||||
const parts = hsl.split(/\s+/);
|
||||
const newL = Math.max(0, Math.min(100, parseFloat(parts[2]) + delta));
|
||||
return `${parts[0]} ${parts[1]} ${Math.round(newL * 10) / 10}%`;
|
||||
}
|
||||
|
||||
function adjustSaturationToken(hsl: string, factor: number): string {
|
||||
const parts = hsl.split(/\s+/);
|
||||
const newS = Math.max(0, Math.min(100, parseFloat(parts[1]) * factor));
|
||||
return `${parts[0]} ${Math.round(newS * 10) / 10}% ${parts[2]}`;
|
||||
}
|
||||
|
||||
const setStylePropertyIfChanged = (element: HTMLElement, property: string, value: string) => {
|
||||
if (element.style.getPropertyValue(property) === value) return;
|
||||
element.style.setProperty(property, value);
|
||||
};
|
||||
|
||||
const removeStylePropertyIfSet = (element: HTMLElement, property: string) => {
|
||||
if (!element.style.getPropertyValue(property)) return;
|
||||
element.style.removeProperty(property);
|
||||
};
|
||||
|
||||
const TOP_TABS_THEME_PROPERTIES = [
|
||||
'--top-tabs-bg',
|
||||
'--top-tabs-fg',
|
||||
'--top-tabs-muted',
|
||||
'--top-tabs-active-bg',
|
||||
'--top-tabs-accent',
|
||||
'--background',
|
||||
'--foreground',
|
||||
'--accent',
|
||||
'--accent-foreground',
|
||||
'--primary',
|
||||
'--primary-foreground',
|
||||
'--secondary',
|
||||
'--border',
|
||||
'--muted-foreground',
|
||||
] as const;
|
||||
|
||||
let topTabsChromeThemeVarsApplied = false;
|
||||
|
||||
export function clearTopTabsChromeThemeVars(): void {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (!topTabsChromeThemeVarsApplied) return;
|
||||
const tabsRoot = document.querySelector<HTMLElement>('[data-top-tabs-root]');
|
||||
if (!tabsRoot) return;
|
||||
for (const property of TOP_TABS_THEME_PROPERTIES) {
|
||||
removeStylePropertyIfSet(tabsRoot, property);
|
||||
}
|
||||
topTabsChromeThemeVarsApplied = false;
|
||||
}
|
||||
|
||||
export function applyTopTabsChromeThemeVars(theme: TerminalTheme): void {
|
||||
if (typeof document === 'undefined') return;
|
||||
const tabsRoot = document.querySelector<HTMLElement>('[data-top-tabs-root]');
|
||||
if (!tabsRoot) return;
|
||||
|
||||
const bg = hexToHslToken(theme.colors.background);
|
||||
const fg = hexToHslToken(theme.colors.foreground);
|
||||
const accent = hexToHslToken(theme.colors.cursor);
|
||||
const accentForeground = resolveReadableForegroundForHsl(accent);
|
||||
const isDark = theme.type === 'dark';
|
||||
const secondary = adjustLightnessToken(bg, isDark ? 6 : -5);
|
||||
const border = adjustLightnessToken(bg, isDark ? 12 : -10);
|
||||
const mutedFg = adjustSaturationToken(adjustLightnessToken(fg, isDark ? -20 : 20), 0.5);
|
||||
|
||||
setStylePropertyIfChanged(tabsRoot, '--background', bg);
|
||||
setStylePropertyIfChanged(tabsRoot, '--foreground', fg);
|
||||
setStylePropertyIfChanged(tabsRoot, '--accent', accent);
|
||||
setStylePropertyIfChanged(tabsRoot, '--accent-foreground', accentForeground);
|
||||
setStylePropertyIfChanged(tabsRoot, '--primary', accent);
|
||||
setStylePropertyIfChanged(tabsRoot, '--primary-foreground', accentForeground);
|
||||
setStylePropertyIfChanged(tabsRoot, '--secondary', secondary);
|
||||
setStylePropertyIfChanged(tabsRoot, '--border', border);
|
||||
setStylePropertyIfChanged(tabsRoot, '--muted-foreground', mutedFg);
|
||||
setStylePropertyIfChanged(tabsRoot, '--top-tabs-bg', 'hsl(var(--secondary))');
|
||||
setStylePropertyIfChanged(tabsRoot, '--top-tabs-fg', 'hsl(var(--foreground))');
|
||||
setStylePropertyIfChanged(tabsRoot, '--top-tabs-muted', 'hsl(var(--muted-foreground))');
|
||||
setStylePropertyIfChanged(tabsRoot, '--top-tabs-active-bg', 'hsl(var(--background))');
|
||||
setStylePropertyIfChanged(tabsRoot, '--top-tabs-accent', 'hsl(var(--accent))');
|
||||
topTabsChromeThemeVarsApplied = true;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { usePortForwardingAutoStart } from '../state/usePortForwardingAutoStart';
|
||||
import { editorTabStore } from '../state/editorTabStore';
|
||||
import { netcattyBridge } from '../../infrastructure/services/netcattyBridge';
|
||||
import { localStorageAdapter } from '../../infrastructure/persistence/localStorageAdapter';
|
||||
import { toast } from '../../components/ui/toast';
|
||||
|
||||
type StartupEffectsContext = Record<string, any>;
|
||||
|
||||
type KeyboardInteractiveScope = "terminal" | "external";
|
||||
type KeyboardInteractiveRequestLike = {
|
||||
scope?: KeyboardInteractiveScope;
|
||||
sessionId?: string;
|
||||
};
|
||||
type SessionIdLike = { id: string };
|
||||
|
||||
export function shouldQueueKeyboardInteractiveRequest(
|
||||
request: KeyboardInteractiveRequestLike,
|
||||
sessions: SessionIdLike[],
|
||||
): boolean {
|
||||
if (request.scope !== "terminal") return true;
|
||||
if (!request.sessionId) return false;
|
||||
return sessions.some((session) => session.id === request.sessionId);
|
||||
}
|
||||
|
||||
export function useAppStartupEffects(ctx: StartupEffectsContext) {
|
||||
const {dismissUpdate, enabled = true, groupConfigs, hosts, identities,
|
||||
installUpdate, isVaultInitialized, keys, knownHosts, openSettingsWindow, portForwardingRules, proxyProfiles, sessions, setKeyboardInteractiveQueue,
|
||||
t, terminalSettings, updateState, workspaces,
|
||||
} = ctx;
|
||||
const sessionsRef = useRef(sessions);
|
||||
useEffect(() => {
|
||||
sessionsRef.current = sessions;
|
||||
}, [sessions]);
|
||||
|
||||
// Show toast notification when update is available (only when auto-download is idle)
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
// Skip "update available" toast if auto-download has already started or completed
|
||||
if (updateState.autoDownloadStatus !== 'idle') return;
|
||||
// Don't show automatic notification when auto-update is disabled
|
||||
if (localStorageAdapter.readString('netcatty_auto_update_enabled_v1') === 'false') return;
|
||||
if (updateState.hasUpdate && updateState.latestRelease) {
|
||||
const version = updateState.latestRelease.version;
|
||||
toast.info(
|
||||
t('update.available.message', { version }),
|
||||
{
|
||||
title: t('update.available.title'),
|
||||
duration: 8000, // Show longer for update notifications
|
||||
onClick: () => {
|
||||
void openSettingsWindow();
|
||||
// Dismiss the update so the toast doesn't re-fire on every render.
|
||||
// On unsupported platforms (where autoDownloadStatus stays 'idle')
|
||||
// this is the only way to suppress the notification for this version.
|
||||
// On supported platforms this toast only shows before auto-download
|
||||
// starts, and the Settings window's own useUpdateCheck will pick up
|
||||
// the download state via IPC events independently of the dismiss.
|
||||
dismissUpdate();
|
||||
},
|
||||
actionLabel: t('update.viewInSettings'),
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [enabled, updateState.hasUpdate, updateState.latestRelease, updateState.autoDownloadStatus, t, openSettingsWindow, dismissUpdate]);
|
||||
|
||||
// Track previous autoDownloadStatus so toast effects fire only on actual transitions,
|
||||
// not when unrelated deps (installUpdate, openSettingsWindow) change their reference.
|
||||
const prevAutoDownloadStatusRef = useRef(updateState.autoDownloadStatus);
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const prev = prevAutoDownloadStatusRef.current;
|
||||
prevAutoDownloadStatusRef.current = updateState.autoDownloadStatus;
|
||||
if (prev === updateState.autoDownloadStatus) return;
|
||||
|
||||
if (updateState.autoDownloadStatus === 'ready') {
|
||||
const version = updateState.latestRelease?.version ?? '';
|
||||
toast.info(
|
||||
t('update.readyToInstall.message', { version }),
|
||||
{
|
||||
title: t('update.readyToInstall.title'),
|
||||
duration: 0,
|
||||
actionLabel: t('update.restartNow'),
|
||||
onClick: () => installUpdate(),
|
||||
}
|
||||
);
|
||||
} else if (updateState.autoDownloadStatus === 'error') {
|
||||
toast.error(
|
||||
t('update.downloadFailed.message'),
|
||||
{
|
||||
title: t('update.downloadFailed.title'),
|
||||
actionLabel: t('update.viewInSettings'),
|
||||
onClick: () => void openSettingsWindow(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [enabled, updateState.autoDownloadStatus, updateState.latestRelease?.version, t, installUpdate, openSettingsWindow]);
|
||||
|
||||
// Auto-start port forwarding rules on app launch
|
||||
usePortForwardingAutoStart({
|
||||
enabled,
|
||||
isVaultInitialized,
|
||||
hosts,
|
||||
keys,
|
||||
identities,
|
||||
knownHosts,
|
||||
proxyProfiles,
|
||||
groupConfigs,
|
||||
terminalSettings,
|
||||
});
|
||||
|
||||
// Sync tray menu data + handle tray actions
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.updateTrayMenuData) return;
|
||||
|
||||
let cancelled = false;
|
||||
const timer = setTimeout(() => {
|
||||
if (cancelled) return;
|
||||
|
||||
const sessionsForTray = sessions.map((s) => {
|
||||
const ws = s.workspaceId ? workspaces.find((w) => w.id === s.workspaceId) : undefined;
|
||||
return {
|
||||
id: s.id,
|
||||
label: s.hostname,
|
||||
hostLabel: s.hostLabel,
|
||||
status: s.status,
|
||||
workspaceId: s.workspaceId,
|
||||
workspaceTitle: ws?.title,
|
||||
};
|
||||
});
|
||||
|
||||
const hostsForSystemMenu = hosts
|
||||
.filter((host: any) => typeof host?.id === "string" && host.id.length > 0)
|
||||
.map((host: any) => ({
|
||||
id: host.id,
|
||||
label: host.label,
|
||||
hostname: host.hostname,
|
||||
group: host.group,
|
||||
pinned: host.pinned,
|
||||
lastConnectedAt: host.lastConnectedAt,
|
||||
protocol: host.protocol,
|
||||
}));
|
||||
|
||||
void bridge.updateTrayMenuData({
|
||||
sessions: sessionsForTray,
|
||||
portForwardRules: portForwardingRules,
|
||||
hosts: hostsForSystemMenu,
|
||||
});
|
||||
}, 250);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [enabled, hosts, sessions, portForwardingRules, workspaces]);
|
||||
|
||||
// Quit guard: block app exit while any editor tab has unsaved changes.
|
||||
// Main process sends "app:query-dirty-editors"; we respond with the result.
|
||||
useEffect(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.onCheckDirtyEditors) return;
|
||||
const unsub = bridge.onCheckDirtyEditors(() => {
|
||||
// Always report SOMETHING so the main process doesn't time out for
|
||||
// 5 s on an unhandled exception. If we can't determine the state,
|
||||
// fail open — losing unsaved work is bad, but stranding the user
|
||||
// on a slow quit and then quitting anyway after the timeout is
|
||||
// exactly the same outcome.
|
||||
let hasDirty = false;
|
||||
try {
|
||||
hasDirty = editorTabStore.getTabs().some((tab) => tab.content !== tab.baselineContent);
|
||||
if (hasDirty) toast.warning(t('sftp.editor.quitBlockedByDirty'), 'SFTP');
|
||||
} catch (err) {
|
||||
console.error('[App] dirty-editors check failed:', err);
|
||||
}
|
||||
try {
|
||||
bridge.reportDirtyEditorsResult?.(hasDirty);
|
||||
} catch (err) {
|
||||
// Reporting itself shouldn't throw, but if the IPC bridge is in a
|
||||
// bad state we'd rather log than bubble out of the listener and
|
||||
// disable the quit guard for the rest of the session.
|
||||
console.error('[App] reportDirtyEditorsResult failed:', err);
|
||||
}
|
||||
});
|
||||
return unsub;
|
||||
}, [enabled, t]);
|
||||
|
||||
// Keyboard-interactive authentication (2FA/MFA) event listener
|
||||
useEffect(() => {
|
||||
const bridge = netcattyBridge.get();
|
||||
if (!bridge?.onKeyboardInteractive) return;
|
||||
|
||||
const unsubscribe = bridge.onKeyboardInteractive((request) => {
|
||||
if (!shouldQueueKeyboardInteractiveRequest(request, sessionsRef.current)) return;
|
||||
console.log('[App] Keyboard-interactive request received:', request);
|
||||
// Add to queue instead of replacing - supports multiple concurrent sessions
|
||||
setKeyboardInteractiveQueue(prev => [...prev, {
|
||||
requestId: request.requestId,
|
||||
sessionId: request.sessionId,
|
||||
name: request.name,
|
||||
instructions: request.instructions,
|
||||
prompts: request.prompts,
|
||||
hostname: request.hostname,
|
||||
savedPassword: request.savedPassword,
|
||||
}]);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [enabled, setKeyboardInteractiveQueue]);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
buildOrderedWorkTabIds,
|
||||
isHostTreeWorkTabSurface,
|
||||
isRootPageTabId,
|
||||
isTerminalContentTabSurface,
|
||||
reorderWorkTabIds,
|
||||
resolveWorkTabActiveHostId,
|
||||
resolveWorkTabHostTreeTheme,
|
||||
} from './workTabSurface';
|
||||
import type { EditorTab } from '../state/editorTabStore';
|
||||
import type { Host, TerminalSession, TerminalTheme, Workspace } from '../../types';
|
||||
|
||||
const makeTheme = (id: string, type: TerminalTheme['type'], background: string): TerminalTheme => ({
|
||||
id,
|
||||
name: id,
|
||||
type,
|
||||
colors: {
|
||||
background,
|
||||
foreground: type === 'dark' ? '#ffffff' : '#000000',
|
||||
cursor: '#888888',
|
||||
selection: '#555555',
|
||||
black: '#000000',
|
||||
red: '#ff0000',
|
||||
green: '#00ff00',
|
||||
yellow: '#ffff00',
|
||||
blue: '#0000ff',
|
||||
magenta: '#ff00ff',
|
||||
cyan: '#00ffff',
|
||||
white: '#ffffff',
|
||||
brightBlack: '#444444',
|
||||
brightRed: '#ff5555',
|
||||
brightGreen: '#55ff55',
|
||||
brightYellow: '#ffff55',
|
||||
brightBlue: '#5555ff',
|
||||
brightMagenta: '#ff55ff',
|
||||
brightCyan: '#55ffff',
|
||||
brightWhite: '#ffffff',
|
||||
},
|
||||
});
|
||||
|
||||
test('work tab order keeps custom positions and appends new tabs', () => {
|
||||
assert.deepEqual(
|
||||
buildOrderedWorkTabIds(['log-1', 'session-1'], ['session-1', 'workspace-1', 'log-1', 'editor:file-1']),
|
||||
['log-1', 'session-1', 'workspace-1', 'editor:file-1'],
|
||||
);
|
||||
});
|
||||
|
||||
test('work tab order removes duplicate ids before rendering', () => {
|
||||
assert.deepEqual(
|
||||
buildOrderedWorkTabIds(
|
||||
['session-2', 'session-1', 'session-2', 'session-1'],
|
||||
['session-1', 'session-2', 'session-3', 'session-3'],
|
||||
),
|
||||
['session-2', 'session-1', 'session-3'],
|
||||
);
|
||||
});
|
||||
|
||||
test('work tab order reorders with newly materialized tabs', () => {
|
||||
assert.deepEqual(
|
||||
reorderWorkTabIds(
|
||||
['session-1', 'session-2', 'session-3'],
|
||||
['session-1', 'session-2', 'session-3'],
|
||||
'session-1',
|
||||
'session-3',
|
||||
'after',
|
||||
),
|
||||
['session-2', 'session-3', 'session-1'],
|
||||
);
|
||||
});
|
||||
|
||||
test('root pages are not work tab surfaces', () => {
|
||||
assert.equal(isRootPageTabId('vault'), true);
|
||||
assert.equal(isRootPageTabId('sftp'), true);
|
||||
assert.equal(isRootPageTabId('session-1'), false);
|
||||
});
|
||||
|
||||
test('shared host tree is visible for editor, log, session, and workspace tabs', () => {
|
||||
const sessionIds = new Set(['session-1']);
|
||||
const workspaceIds = new Set(['workspace-1']);
|
||||
const logViewIds = new Set(['log-1']);
|
||||
const orderedTabs = ['session-1', 'workspace-1', 'editor:file-1', 'log-1'];
|
||||
|
||||
for (const activeTabId of orderedTabs) {
|
||||
assert.equal(isHostTreeWorkTabSurface({
|
||||
enabled: true,
|
||||
activeTabId,
|
||||
logViewIds,
|
||||
orderedTabs,
|
||||
sessionIds,
|
||||
workspaceIds,
|
||||
}), true);
|
||||
}
|
||||
});
|
||||
|
||||
test('shared host tree recognizes active log view before tab ordering catches up', () => {
|
||||
assert.equal(isHostTreeWorkTabSurface({
|
||||
enabled: true,
|
||||
activeTabId: 'log-1',
|
||||
logViewIds: new Set(['log-1']),
|
||||
orderedTabs: [],
|
||||
sessionIds: new Set(),
|
||||
workspaceIds: new Set(),
|
||||
}), true);
|
||||
});
|
||||
|
||||
test('terminal content surface is limited to sessions and workspaces', () => {
|
||||
const sessionIds = new Set(['session-1']);
|
||||
const workspaceIds = new Set(['workspace-1']);
|
||||
|
||||
assert.equal(isTerminalContentTabSurface({ activeTabId: 'session-1', sessionIds, workspaceIds }), true);
|
||||
assert.equal(isTerminalContentTabSurface({ activeTabId: 'workspace-1', sessionIds, workspaceIds }), true);
|
||||
assert.equal(isTerminalContentTabSurface({ activeTabId: 'editor:file-1', sessionIds, workspaceIds }), false);
|
||||
assert.equal(isTerminalContentTabSurface({ activeTabId: 'log-1', sessionIds, workspaceIds }), false);
|
||||
});
|
||||
|
||||
test('shared host tree resolves active host ids across work tab types', () => {
|
||||
const sessions = [
|
||||
{ id: 'session-1', hostId: 'host-1' },
|
||||
{ id: 'session-2', hostId: 'host-2', workspaceId: 'workspace-1' },
|
||||
] as TerminalSession[];
|
||||
const workspaces = [{
|
||||
id: 'workspace-1',
|
||||
focusedSessionId: 'session-2',
|
||||
root: { id: 'pane-2', type: 'pane', sessionId: 'session-2' },
|
||||
}] as Workspace[];
|
||||
const editorTabs = [
|
||||
{ id: 'file-1', hostId: 'host-3' },
|
||||
] as EditorTab[];
|
||||
|
||||
assert.equal(resolveWorkTabActiveHostId({ activeTabId: 'session-1', sessions, workspaces, editorTabs }), 'host-1');
|
||||
assert.equal(resolveWorkTabActiveHostId({ activeTabId: 'workspace-1', sessions, workspaces, editorTabs }), 'host-2');
|
||||
assert.equal(resolveWorkTabActiveHostId({ activeTabId: 'editor:file-1', sessions, workspaces, editorTabs }), 'host-3');
|
||||
assert.equal(resolveWorkTabActiveHostId({ activeTabId: 'log-1', sessions, workspaces, editorTabs }), null);
|
||||
});
|
||||
|
||||
test('shared host tree falls back to the first workspace session when focused session is missing', () => {
|
||||
const sessions = [
|
||||
{ id: 'session-1', hostId: 'host-1', workspaceId: 'workspace-1' },
|
||||
{ id: 'session-2', hostId: 'host-2', workspaceId: 'workspace-1' },
|
||||
] as TerminalSession[];
|
||||
const workspaces = [{
|
||||
id: 'workspace-1',
|
||||
focusedSessionId: 'missing-session',
|
||||
root: {
|
||||
id: 'split-1',
|
||||
type: 'split',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ id: 'pane-1', type: 'pane', sessionId: 'session-1' },
|
||||
{ id: 'pane-2', type: 'pane', sessionId: 'session-2' },
|
||||
],
|
||||
sizes: [0.5, 0.5],
|
||||
},
|
||||
}] as Workspace[];
|
||||
|
||||
assert.equal(resolveWorkTabActiveHostId({
|
||||
activeTabId: 'workspace-1',
|
||||
sessions,
|
||||
workspaces,
|
||||
editorTabs: [],
|
||||
}), 'host-1');
|
||||
});
|
||||
|
||||
test('shared host tree fallback prefers workspace tree order over sessions array order', () => {
|
||||
const sessions = [
|
||||
{ id: 'session-2', hostId: 'host-2', workspaceId: 'workspace-1' },
|
||||
{ id: 'session-1', hostId: 'host-1', workspaceId: 'workspace-1' },
|
||||
] as TerminalSession[];
|
||||
const workspaces = [{
|
||||
id: 'workspace-1',
|
||||
focusedSessionId: 'missing-session',
|
||||
root: {
|
||||
id: 'split-1',
|
||||
type: 'split',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ id: 'pane-1', type: 'pane', sessionId: 'session-1' },
|
||||
{ id: 'pane-2', type: 'pane', sessionId: 'session-2' },
|
||||
],
|
||||
sizes: [0.5, 0.5],
|
||||
},
|
||||
}] as Workspace[];
|
||||
|
||||
assert.equal(resolveWorkTabActiveHostId({
|
||||
activeTabId: 'workspace-1',
|
||||
sessions,
|
||||
workspaces,
|
||||
editorTabs: [],
|
||||
}), 'host-1');
|
||||
});
|
||||
|
||||
test('shared host tree uses the active host theme when follow-app terminal theme is off', () => {
|
||||
const currentTheme = makeTheme('app-dark', 'dark', '#111111');
|
||||
const hostTheme = makeTheme('host-light', 'light', '#fafafa');
|
||||
const host = {
|
||||
id: 'host-1',
|
||||
label: 'Host',
|
||||
hostname: 'host.local',
|
||||
username: 'root',
|
||||
tags: [],
|
||||
os: 'linux',
|
||||
theme: hostTheme.id,
|
||||
themeOverride: true,
|
||||
} as Host;
|
||||
|
||||
const resolved = resolveWorkTabHostTreeTheme({
|
||||
activeHostId: host.id,
|
||||
accentMode: 'theme',
|
||||
currentTerminalTheme: currentTheme,
|
||||
customAccent: '#8b5cf6',
|
||||
followAppTerminalTheme: false,
|
||||
hostById: new Map([[host.id, host]]),
|
||||
themeById: new Map([[currentTheme.id, currentTheme], [hostTheme.id, hostTheme]]),
|
||||
});
|
||||
|
||||
assert.equal(resolved.id, hostTheme.id);
|
||||
});
|
||||
|
||||
test('shared host tree uses the followed terminal theme when follow-app terminal theme is on', () => {
|
||||
const currentTheme = makeTheme('app-light', 'light', '#ffffff');
|
||||
const hostTheme = makeTheme('host-dark', 'dark', '#050505');
|
||||
const host = {
|
||||
id: 'host-1',
|
||||
label: 'Host',
|
||||
hostname: 'host.local',
|
||||
username: 'root',
|
||||
tags: [],
|
||||
os: 'linux',
|
||||
theme: hostTheme.id,
|
||||
themeOverride: true,
|
||||
} as Host;
|
||||
|
||||
const resolved = resolveWorkTabHostTreeTheme({
|
||||
activeHostId: host.id,
|
||||
accentMode: 'theme',
|
||||
currentTerminalTheme: currentTheme,
|
||||
customAccent: '#8b5cf6',
|
||||
followAppTerminalTheme: true,
|
||||
hostById: new Map([[host.id, host]]),
|
||||
themeById: new Map([[currentTheme.id, currentTheme], [hostTheme.id, hostTheme]]),
|
||||
});
|
||||
|
||||
assert.equal(resolved.id, currentTheme.id);
|
||||
});
|
||||
|
||||
test('shared host tree falls back to the current terminal theme without an active host', () => {
|
||||
const currentTheme = makeTheme('app-dark', 'dark', '#111111');
|
||||
|
||||
const resolved = resolveWorkTabHostTreeTheme({
|
||||
activeHostId: null,
|
||||
accentMode: 'theme',
|
||||
currentTerminalTheme: currentTheme,
|
||||
customAccent: '#8b5cf6',
|
||||
followAppTerminalTheme: false,
|
||||
hostById: new Map(),
|
||||
themeById: new Map([[currentTheme.id, currentTheme]]),
|
||||
});
|
||||
|
||||
assert.equal(resolved.id, currentTheme.id);
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
fromEditorTabId,
|
||||
isEditorTabId,
|
||||
} from '../state/activeTabStore';
|
||||
import { applyCustomAccentToTerminalTheme, resolveHostTerminalThemeId } from '../../domain/terminalAppearance';
|
||||
import { collectSessionIds } from '../../domain/workspace';
|
||||
import type { EditorTab } from '../state/editorTabStore';
|
||||
import type { Host, TerminalSession, TerminalTheme, Workspace } from '../../types';
|
||||
|
||||
function uniqueTabIds(tabIds: readonly string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const uniqueIds: string[] = [];
|
||||
for (const tabId of tabIds) {
|
||||
if (!tabId || seen.has(tabId)) continue;
|
||||
seen.add(tabId);
|
||||
uniqueIds.push(tabId);
|
||||
}
|
||||
return uniqueIds;
|
||||
}
|
||||
|
||||
export function isRootPageTabId(activeTabId: string): boolean {
|
||||
return activeTabId === 'vault' || activeTabId === 'sftp';
|
||||
}
|
||||
|
||||
export function buildOrderedWorkTabIds(
|
||||
tabOrder: readonly string[],
|
||||
allTabIds: readonly string[],
|
||||
): string[] {
|
||||
const uniqueAllTabIds = uniqueTabIds(allTabIds);
|
||||
const allTabIdSet = new Set(uniqueAllTabIds);
|
||||
const orderedIds = uniqueTabIds(tabOrder.filter((id) => allTabIdSet.has(id)));
|
||||
const orderedIdSet = new Set(orderedIds);
|
||||
const newIds = uniqueAllTabIds.filter((id) => !orderedIdSet.has(id));
|
||||
return [...orderedIds, ...newIds];
|
||||
}
|
||||
|
||||
export function reorderWorkTabIds(
|
||||
tabOrder: readonly string[],
|
||||
allTabIds: readonly string[],
|
||||
draggedId: string,
|
||||
targetId: string,
|
||||
position: 'before' | 'after' = 'before',
|
||||
): string[] {
|
||||
if (draggedId === targetId) return buildOrderedWorkTabIds(tabOrder, allTabIds);
|
||||
|
||||
const currentOrder = buildOrderedWorkTabIds(tabOrder, allTabIds);
|
||||
const draggedIndex = currentOrder.indexOf(draggedId);
|
||||
const targetIndex = currentOrder.indexOf(targetId);
|
||||
if (draggedIndex === -1 || targetIndex === -1) return [...tabOrder];
|
||||
|
||||
currentOrder.splice(draggedIndex, 1);
|
||||
|
||||
let nextTargetIndex = targetIndex;
|
||||
if (draggedIndex < targetIndex) {
|
||||
nextTargetIndex -= 1;
|
||||
}
|
||||
if (position === 'after') {
|
||||
nextTargetIndex += 1;
|
||||
}
|
||||
|
||||
currentOrder.splice(nextTargetIndex, 0, draggedId);
|
||||
return currentOrder;
|
||||
}
|
||||
|
||||
export function isHostTreeWorkTabSurface({
|
||||
enabled,
|
||||
activeTabId,
|
||||
logViewIds = new Set(),
|
||||
orderedTabs,
|
||||
sessionIds,
|
||||
workspaceIds,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
activeTabId: string;
|
||||
logViewIds?: ReadonlySet<string>;
|
||||
orderedTabs: readonly string[];
|
||||
sessionIds: ReadonlySet<string>;
|
||||
workspaceIds: ReadonlySet<string>;
|
||||
}): boolean {
|
||||
if (!enabled) return false;
|
||||
if (isRootPageTabId(activeTabId)) return false;
|
||||
return orderedTabs.includes(activeTabId)
|
||||
|| isEditorTabId(activeTabId)
|
||||
|| logViewIds.has(activeTabId)
|
||||
|| sessionIds.has(activeTabId)
|
||||
|| workspaceIds.has(activeTabId);
|
||||
}
|
||||
|
||||
export function isTerminalContentTabSurface({
|
||||
activeTabId,
|
||||
sessionIds,
|
||||
workspaceIds,
|
||||
}: {
|
||||
activeTabId: string;
|
||||
sessionIds: ReadonlySet<string>;
|
||||
workspaceIds: ReadonlySet<string>;
|
||||
}): boolean {
|
||||
return sessionIds.has(activeTabId) || workspaceIds.has(activeTabId);
|
||||
}
|
||||
|
||||
export function resolveWorkspaceTargetSession(
|
||||
workspace: Workspace,
|
||||
sessions: readonly TerminalSession[],
|
||||
): TerminalSession | undefined {
|
||||
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
||||
return resolveWorkspaceTargetSessionFromMap(workspace, sessionById);
|
||||
}
|
||||
|
||||
export function resolveWorkspaceTargetSessionFromMap(
|
||||
workspace: Workspace,
|
||||
sessionById: ReadonlyMap<string, TerminalSession>,
|
||||
): TerminalSession | undefined {
|
||||
const orderedSessionIds = collectSessionIds(workspace.root);
|
||||
const workspaceSessionIdSet = new Set(orderedSessionIds);
|
||||
const focusedSession = workspace.focusedSessionId
|
||||
? sessionById.get(workspace.focusedSessionId)
|
||||
: undefined;
|
||||
const validFocusedSession = focusedSession && workspaceSessionIdSet.has(focusedSession.id)
|
||||
? focusedSession
|
||||
: undefined;
|
||||
if (validFocusedSession) return validFocusedSession;
|
||||
for (const sessionId of orderedSessionIds) {
|
||||
const session = sessionById.get(sessionId);
|
||||
if (session) return session;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveWorkTabActiveHostId({
|
||||
activeTabId,
|
||||
editorTabs,
|
||||
sessions,
|
||||
workspaces,
|
||||
}: {
|
||||
activeTabId: string;
|
||||
editorTabs: readonly EditorTab[];
|
||||
sessions: readonly TerminalSession[];
|
||||
workspaces: readonly Workspace[];
|
||||
}): string | null {
|
||||
if (isEditorTabId(activeTabId)) {
|
||||
const editorId = fromEditorTabId(activeTabId);
|
||||
return editorTabs.find((tab) => tab.id === editorId)?.hostId ?? null;
|
||||
}
|
||||
|
||||
const activeSession = sessions.find((session) => session.id === activeTabId);
|
||||
if (activeSession) return activeSession.hostId ?? null;
|
||||
|
||||
const activeWorkspace = workspaces.find((workspace) => workspace.id === activeTabId);
|
||||
if (!activeWorkspace) return null;
|
||||
|
||||
const targetSession = resolveWorkspaceTargetSession(activeWorkspace, sessions);
|
||||
return targetSession?.hostId ?? null;
|
||||
}
|
||||
|
||||
export function resolveWorkTabHostTreeTheme({
|
||||
activeHostId,
|
||||
accentMode,
|
||||
currentTerminalTheme,
|
||||
customAccent,
|
||||
followAppTerminalTheme,
|
||||
hostById,
|
||||
themeById,
|
||||
}: {
|
||||
activeHostId: string | null;
|
||||
accentMode: 'theme' | 'custom';
|
||||
currentTerminalTheme: TerminalTheme;
|
||||
customAccent: string;
|
||||
followAppTerminalTheme: boolean;
|
||||
hostById: ReadonlyMap<string, Host>;
|
||||
themeById: ReadonlyMap<string, TerminalTheme>;
|
||||
}): TerminalTheme {
|
||||
if (!activeHostId || followAppTerminalTheme) return currentTerminalTheme;
|
||||
|
||||
const host = hostById.get(activeHostId) ?? null;
|
||||
const themeId = resolveHostTerminalThemeId(host, currentTerminalTheme.id);
|
||||
const baseTheme = themeById.get(themeId) ?? currentTerminalTheme;
|
||||
return applyCustomAccentToTerminalTheme(baseTheme, accentMode, customAccent);
|
||||
}
|
||||
@@ -14,11 +14,19 @@ const I18nContext = createContext<I18nContextValue | null>(null);
|
||||
|
||||
const interpolate = (template: string, values?: InterpolationValues): string => {
|
||||
if (!values) return template;
|
||||
return template.replace(/\{(\w+)\}/g, (_match, key: string) => {
|
||||
const replaceDoubleBraceToken = (match: string, key: string) => {
|
||||
const v = values[key];
|
||||
if (v === null || v === undefined) return match;
|
||||
return String(v);
|
||||
};
|
||||
const replaceSingleBraceToken = (_match: string, key: string) => {
|
||||
const v = values[key];
|
||||
if (v === null || v === undefined) return '';
|
||||
return String(v);
|
||||
});
|
||||
};
|
||||
return template
|
||||
.replace(/\{\{(\w+)\}\}/g, replaceDoubleBraceToken)
|
||||
.replace(/\{(\w+)\}/g, replaceSingleBraceToken);
|
||||
};
|
||||
|
||||
const resolveMessage = (resolvedLocale: string, key: string): string | undefined => {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import en from "../locales/en.ts";
|
||||
import ru from "../locales/ru.ts";
|
||||
import zhCN from "../locales/zh-CN.ts";
|
||||
|
||||
const strategyKeys = [
|
||||
"cloudSync.strategy.title",
|
||||
"cloudSync.strategy.desc",
|
||||
"cloudSync.strategy.smartMerge",
|
||||
"cloudSync.strategy.smartMergeDesc",
|
||||
"cloudSync.strategy.preferCloud",
|
||||
"cloudSync.strategy.preferCloudDesc",
|
||||
"cloudSync.strategy.preferLocal",
|
||||
"cloudSync.strategy.preferLocalDesc",
|
||||
] as const;
|
||||
|
||||
test("cloud sync strategy copy exists in every bundled locale", () => {
|
||||
for (const [locale, messages] of Object.entries({ en, ru, zhCN })) {
|
||||
for (const key of strategyKeys) {
|
||||
assert.equal(
|
||||
typeof messages[key],
|
||||
"string",
|
||||
`${locale} is missing ${key}`,
|
||||
);
|
||||
assert.notEqual(messages[key], "", `${locale} has empty ${key}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
+15
-2093
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,492 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const enAiMessages: Messages = {
|
||||
// AI Settings
|
||||
'ai.agentSettings': 'Agent Settings',
|
||||
'ai.chat.preparing': 'Preparing…',
|
||||
'ai.chat.compactingContext': 'Compacting earlier context…',
|
||||
'ai.chat.compactingStep': 'Trimming context for next step…',
|
||||
'ai.chat.compactionRetry': 'Request was too large. Compacting context and retrying…',
|
||||
'ai.chat.compactionBanner': 'Context compacted: {before}K → {after}K tokens',
|
||||
'ai.title': 'AI',
|
||||
'ai.description': 'Configure AI providers, agents, and safety settings',
|
||||
'ai.providers': 'Providers',
|
||||
'ai.agents': 'Agents',
|
||||
'ai.providers.empty': 'No providers configured. Add a provider to get started.',
|
||||
'ai.providers.add': 'Add Provider',
|
||||
'ai.providers.active': 'Active',
|
||||
'ai.providers.apiKeyConfigured': 'API key configured',
|
||||
'ai.providers.noApiKey': 'No API key',
|
||||
'ai.providers.configure': 'Configure',
|
||||
'ai.providers.remove': 'Remove',
|
||||
'ai.providers.name': 'Display Name',
|
||||
'ai.providers.name.placeholder': 'e.g. My Provider',
|
||||
'ai.providers.style': 'Protocol style',
|
||||
'ai.providers.style.anthropic': 'Anthropic-compatible',
|
||||
'ai.providers.style.openai': 'OpenAI-compatible',
|
||||
'ai.providers.style.google': 'Google-compatible',
|
||||
'ai.providers.style.inherited': 'auto',
|
||||
'ai.providers.style.help': 'Selects which API format requests use. Override when a third-party endpoint speaks a different dialect than its provider type suggests.',
|
||||
'ai.providers.icon.change': 'Change icon',
|
||||
'ai.providers.icon.upload': 'Upload image',
|
||||
'ai.providers.icon.reset': 'Reset',
|
||||
'ai.providers.icon.close': 'Close',
|
||||
'ai.providers.icon.uploadedNote': 'Custom icon (64×64 WebP)',
|
||||
'ai.providers.icon.errorType': 'Please choose an image file.',
|
||||
'ai.providers.apiKey': 'API Key',
|
||||
'ai.providers.apiKey.placeholder': 'Enter API key',
|
||||
'ai.providers.apiKey.decrypting': 'Decrypting...',
|
||||
'ai.providers.baseUrl': 'Base URL',
|
||||
'ai.providers.skipTLSVerify': 'Skip TLS certificate verification (for self-signed certs)',
|
||||
'ai.providers.defaultModel': 'Default Model',
|
||||
'ai.providers.defaultModel.placeholder': 'e.g. gpt-4o, claude-sonnet-4-20250514',
|
||||
'ai.providers.contextWindow': 'Context window',
|
||||
'ai.providers.contextWindow.placeholder': 'e.g. 128000',
|
||||
'ai.providers.contextWindow.help': 'Leave blank to use the model list value when available, otherwise Netcatty uses a safe default.',
|
||||
'ai.providers.contextWindow.error': 'Enter a positive whole number, or leave it blank.',
|
||||
'ai.providers.refreshModels': 'Refresh models',
|
||||
'ai.providers.searchModel': 'Search or type model ID...',
|
||||
'ai.providers.filterModels': 'Filter models...',
|
||||
'ai.providers.loadingModels': 'Loading models...',
|
||||
'ai.providers.noMatchingModels': 'No matching models',
|
||||
'ai.providers.clickToLoadModels': 'Click to load models',
|
||||
'ai.providers.showingModels': 'Showing first 100 of {count} models. Type to filter.',
|
||||
'ai.providers.advancedParams': 'Advanced Parameters',
|
||||
'ai.providers.advancedParams.hint': 'Leave blank to use provider defaults.',
|
||||
'ai.providers.advancedParams.maxTokens.placeholder': 'e.g. 4096',
|
||||
'ai.providers.advancedParams.default': 'Provider default',
|
||||
|
||||
// AI Codex
|
||||
'ai.codex': 'Codex',
|
||||
'ai.codex.title': 'Codex CLI',
|
||||
'ai.codex.description': 'Connect OpenAI Codex. Sign in with ChatGPT here, or enable an OpenAI-compatible provider API key and custom endpoint in Settings.',
|
||||
'ai.codex.detecting': 'Detecting...',
|
||||
'ai.codex.notFound': 'Not found',
|
||||
'ai.codex.awaitingLogin': 'Awaiting login',
|
||||
'ai.codex.connectedChatGPT': 'Connected via ChatGPT',
|
||||
'ai.codex.connectedApiKey': 'Connected via API key',
|
||||
'ai.codex.connectedCustomConfig': 'Connected via ~/.codex/config.toml',
|
||||
'ai.codex.customConfigIncomplete': 'Custom config detected (env var missing)',
|
||||
'ai.codex.customConfigHint': 'Using custom provider "{provider}" configured in ~/.codex/config.toml — no ChatGPT login needed.',
|
||||
'ai.codex.customConfigMissingEnvKey': 'Warning: {envKey} is not set in your shell environment. Export it (or launch netcatty from a shell that has it) so Codex can authenticate.',
|
||||
'ai.codex.notConnected': 'Not connected',
|
||||
'ai.codex.statusUnknown': 'Status unknown',
|
||||
'ai.codex.path': 'Path:',
|
||||
'ai.codex.notFoundHint': 'Could not find codex in PATH. Install it or specify the executable path below.',
|
||||
'ai.codex.customPathPlaceholder': 'e.g. /usr/local/bin/codex',
|
||||
'ai.codex.check': 'Check',
|
||||
'ai.codex.resetPath': 'Reset',
|
||||
'ai.codex.openLogin': 'Open Login',
|
||||
'ai.codex.logout': 'Logout',
|
||||
'ai.codex.connectChatGPT': 'Connect ChatGPT',
|
||||
'ai.codex.refreshStatus': 'Refresh Status',
|
||||
|
||||
// AI Claude Code
|
||||
'ai.claude.title': 'Claude Code',
|
||||
'ai.claude.description': "Anthropic's agentic coding assistant. Requires the system Claude Code CLI.",
|
||||
'ai.claude.detecting': 'Detecting...',
|
||||
'ai.claude.detected': 'Detected',
|
||||
'ai.claude.notFound': 'Not found',
|
||||
'ai.claude.path': 'Path:',
|
||||
'ai.claude.notFoundHint': 'Could not find claude in PATH. Install it or specify the executable path below.',
|
||||
'ai.claude.customPathPlaceholder': 'e.g. /usr/local/bin/claude',
|
||||
'ai.claude.configSection': 'Authentication & config (optional)',
|
||||
'ai.claude.configDir': 'Config directory',
|
||||
'ai.claude.configDir.placeholder': '~/.claude (leave blank for default)',
|
||||
'ai.claude.configDir.hint': 'Sets CLAUDE_CONFIG_DIR — point at a folder where you have run `claude` login (contains settings.json + credentials).',
|
||||
'ai.claude.settings': 'Settings file',
|
||||
'ai.claude.settings.placeholder': '~/team-settings.json (path, or inline {"model":"..."})',
|
||||
'ai.claude.settings.hint': 'Optional. A settings.json path or inline JSON, passed to the SDK as `settings`. Additive to — and independent of — the config directory above (merged on top, not a replacement).',
|
||||
'ai.claude.envVars': 'Environment variables',
|
||||
'ai.claude.envVars.placeholder': 'ANTHROPIC_BASE_URL=https://...\nANTHROPIC_MODEL=...',
|
||||
'ai.claude.envVars.hint': 'One KEY=VALUE per line, passed to the Claude agent. Stored locally in plaintext — for API keys / credentials, prefer the config directory above (a `claude` login).',
|
||||
'ai.claude.check': 'Check',
|
||||
'ai.claude.resetPath': 'Reset',
|
||||
|
||||
// AI GitHub Copilot CLI
|
||||
'ai.copilot.title': 'GitHub Copilot CLI',
|
||||
'ai.copilot.description': 'Uses the GitHub Copilot CLI. Once detected, it can be selected as an external coding agent.',
|
||||
'ai.copilot.detecting': 'Detecting...',
|
||||
'ai.copilot.detected': 'Detected',
|
||||
'ai.copilot.notFound': 'Not found',
|
||||
'ai.copilot.path': 'Path:',
|
||||
'ai.copilot.notFoundHint': 'Could not find copilot in PATH. Install it or specify the executable path below.',
|
||||
'ai.copilot.customPathPlaceholder': 'e.g. /usr/local/bin/copilot',
|
||||
'ai.copilot.check': 'Check',
|
||||
'ai.copilot.resetPath': 'Reset',
|
||||
|
||||
// AI Cursor SDK
|
||||
'ai.cursor.title': 'Cursor',
|
||||
'ai.cursor.description': 'Uses the Cursor SDK.',
|
||||
'ai.cursor.detecting': 'Detecting...',
|
||||
'ai.cursor.detected': 'Available',
|
||||
'ai.cursor.notFound': 'Unavailable',
|
||||
'ai.cursor.path': 'Runtime:',
|
||||
'ai.cursor.notFoundHint': 'Enter an API key to enable Cursor.',
|
||||
'ai.cursor.notInstalledHint': 'Cursor SDK was not detected.',
|
||||
'ai.cursor.installStatus': 'Cursor SDK',
|
||||
'ai.cursor.installed': 'Detected',
|
||||
'ai.cursor.notInstalled': 'Not detected',
|
||||
'ai.cursor.apiKeyStatus': 'API Key',
|
||||
'ai.cursor.apiKeyConfigured': 'Configured',
|
||||
'ai.cursor.apiKeyMissing': 'Missing',
|
||||
'ai.cursor.apiKeyFromEnv': 'From environment',
|
||||
'ai.cursor.apiKey': 'API Key',
|
||||
'ai.cursor.apiKeyPlaceholder': 'Enter Cursor API key',
|
||||
'ai.cursor.apiKeyPlaceholder.env': 'Using CURSOR_API_KEY; enter a key to override',
|
||||
'ai.cursor.apiKeyEnvHint': 'Cursor can use CURSOR_API_KEY from your shell. Save a key here only if you want Netcatty to override it.',
|
||||
'ai.cursor.apiKeyOverrideHint': 'Netcatty will use the saved key here before CURSOR_API_KEY.',
|
||||
'ai.cursor.saveApiKey': 'Save',
|
||||
'ai.cursor.saved': 'Saved',
|
||||
'ai.cursor.showApiKey': 'Show API key',
|
||||
'ai.cursor.hideApiKey': 'Hide API key',
|
||||
'ai.cursor.customPathPlaceholder': 'e.g. /usr/local/bin/cursor',
|
||||
'ai.cursor.check': 'Check',
|
||||
|
||||
// AI CodeBuddy Code
|
||||
'ai.codebuddy.title': 'CodeBuddy Code',
|
||||
'ai.codebuddy.description': 'Uses CodeBuddy Code via the official Agent SDK (`@tencent-ai/agent-sdk`). Once detected, it can be selected as an external coding agent.',
|
||||
'ai.codebuddy.detecting': 'Detecting...',
|
||||
'ai.codebuddy.detected': 'Detected',
|
||||
'ai.codebuddy.notFound': 'Not found',
|
||||
'ai.codebuddy.path': 'Path:',
|
||||
'ai.codebuddy.notFoundHint': 'Could not find codebuddy in PATH. Install it or specify the executable path below.',
|
||||
'ai.codebuddy.customPathPlaceholder': 'e.g. /usr/local/bin/codebuddy',
|
||||
'ai.codebuddy.check': 'Check',
|
||||
'ai.codebuddy.resetPath': 'Reset',
|
||||
'ai.codebuddy.configSection': 'Authentication & config (optional)',
|
||||
'ai.codebuddy.internetEnv': 'Internet Environment',
|
||||
'ai.codebuddy.internetEnv.default': 'Default (overseas)',
|
||||
'ai.codebuddy.internetEnv.internal': 'Internal',
|
||||
'ai.codebuddy.internetEnv.ioa': 'IOA',
|
||||
'ai.codebuddy.internetEnv.hint': 'Sets CODEBUDDY_INTERNET_ENVIRONMENT — choose Internal or IOA for restricted network environments.',
|
||||
'ai.codebuddy.envVars': 'Environment variables',
|
||||
'ai.codebuddy.envVars.placeholder': 'CODEBUDDY_API_KEY=...\nCODEBUDDY_AUTH_TOKEN=...\nOTHER_VAR=...',
|
||||
'ai.codebuddy.envVars.hint': 'One KEY=VALUE per line, passed to the CodeBuddy agent. Set CODEBUDDY_API_KEY or CODEBUDDY_AUTH_TOKEN here for authentication. Stored locally in plaintext.',
|
||||
|
||||
// AI OpenCode
|
||||
'ai.opencode.title': 'OpenCode',
|
||||
'ai.opencode.description': 'Uses OpenCode via the official SDK. Configure providers and keys in OpenCode, then select it as an external coding agent.',
|
||||
'ai.opencode.detecting': 'Detecting...',
|
||||
'ai.opencode.detected': 'Detected',
|
||||
'ai.opencode.notFound': 'Not found',
|
||||
'ai.opencode.path': 'Path:',
|
||||
'ai.opencode.notFoundHint': 'Could not find opencode in PATH. Install it or specify the executable path below.',
|
||||
'ai.opencode.customPathPlaceholder': 'e.g. /usr/local/bin/opencode',
|
||||
'ai.opencode.check': 'Check',
|
||||
'ai.opencode.resetPath': 'Reset',
|
||||
|
||||
// AI Default Agent
|
||||
'ai.defaultAgent': 'Default Agent',
|
||||
'ai.defaultAgent.description': 'Agent to use when starting a new AI session',
|
||||
'ai.defaultAgent.catty': 'Catty (Built-in)',
|
||||
'ai.toolAccess.title': 'Tool Access',
|
||||
'ai.toolAccess.mode': 'Netcatty Access Mode',
|
||||
'ai.toolAccess.description': 'Choose how external agents access Netcatty sessions. MCP exposes the built-in server, while Skills + CLI points agents to the local Netcatty skill and CLI commands.',
|
||||
'ai.toolAccess.mode.mcp': 'MCP',
|
||||
'ai.toolAccess.mode.skills': 'Skills + CLI',
|
||||
|
||||
// External MCP (productized catalog MCP for Codex / Claude Code / Cursor)
|
||||
'ai.externalMcp.title': 'External MCP',
|
||||
'ai.externalMcp.description': 'Expose Netcatty as an MCP server for external clients such as Codex, Claude Code, Cursor, and Grok. Uses the same catalog tools as in-app agents (terminal, SFTP, Vault, port forwarding). Keep Netcatty running while clients are connected.',
|
||||
'ai.externalMcp.sessionsExposed': 'Sessions in scope: {count}',
|
||||
'ai.externalMcp.mode': 'Availability mode',
|
||||
'ai.externalMcp.mode.temporary': 'Temporary',
|
||||
'ai.externalMcp.mode.persistent': 'Always on',
|
||||
'ai.externalMcp.mode.description': 'Temporary mode auto-disables after idle timeout. Always-on restores External MCP when Netcatty starts.',
|
||||
'ai.externalMcp.idleTimeout': 'Idle timeout',
|
||||
'ai.externalMcp.idleTimeout.description': 'In temporary mode, disable External MCP after this many minutes with no MCP operations.',
|
||||
'ai.externalMcp.idleTimeout.minutes': 'min',
|
||||
'ai.externalMcp.usage.title': 'How to use',
|
||||
'ai.externalMcp.usage.keepRunning': '1. Turn on External MCP and keep Netcatty running.',
|
||||
'ai.externalMcp.usage.localhost': '2. Clients connect via the local launcher (127.0.0.1 only). Discovery is removed when you disable the switch.',
|
||||
'ai.externalMcp.usage.permissions': '3. Write operations follow Settings → AI → Safety (observer / confirm / auto) and the command blocklist.',
|
||||
'ai.externalMcp.usage.capabilities': '4. Full catalog tools are available: terminal, SFTP, Vault, and port forwarding. Secrets (passwords / private keys) are never returned.',
|
||||
'ai.externalMcp.help.ariaLabel': 'External MCP help',
|
||||
'ai.externalMcp.security': 'Security',
|
||||
'ai.externalMcp.security.description': 'Listens on 127.0.0.1 with a rotating token, reuses AI Permission Mode for writes, and removes discovery when disabled. No OAuth — this is a local desktop bridge.',
|
||||
'ai.externalMcp.permissionMode': 'Current permission mode: {mode}',
|
||||
'ai.externalMcp.permissionMode.label': 'Write permission mode',
|
||||
'ai.externalMcp.permissionMode.hint': 'Same setting as Settings → AI → Safety. Auto runs Netcatty write tools without Netcatty approval prompts; Confirm asks each time. External clients (Codex / Claude / Grok) may still show their own tool approval UI.',
|
||||
'ai.externalMcp.permissionMode.unknown': 'Unknown',
|
||||
'ai.externalMcp.discovery': 'Discovery',
|
||||
'ai.externalMcp.launcher': 'Launcher',
|
||||
'ai.externalMcp.unavailable': 'Unavailable',
|
||||
'ai.externalMcp.bridgeUnavailable': 'External MCP bridge unavailable',
|
||||
'ai.externalMcp.copy': 'Copy',
|
||||
'ai.externalMcp.copied': 'Copied',
|
||||
'ai.externalMcp.copyFailed': 'Copy failed. Try copying manually.',
|
||||
'ai.externalMcp.refresh': 'Refresh',
|
||||
'ai.externalMcp.clientConfiguration': 'Client configuration',
|
||||
'ai.externalMcp.clientConfiguration.description': 'Pick a client to one-click install, or copy CLI / config snippets.',
|
||||
'ai.externalMcp.client.codex': 'Codex',
|
||||
'ai.externalMcp.client.claude': 'Claude Code',
|
||||
'ai.externalMcp.client.grok': 'Grok',
|
||||
'ai.externalMcp.client.cursor': 'Cursor',
|
||||
'ai.externalMcp.cliCommand': 'CLI command',
|
||||
'ai.externalMcp.configSnippet': 'Config snippet',
|
||||
'ai.externalMcp.addToCodex': 'Add to Codex',
|
||||
'ai.externalMcp.addToClaude': 'Add to Claude Code',
|
||||
'ai.externalMcp.addToGrok': 'Add to Grok',
|
||||
'ai.externalMcp.codexAdded': 'Codex MCP entry added. Restart Codex or open a new Codex session.',
|
||||
'ai.externalMcp.claudeAdded': 'Claude Code MCP entry added. Restart Claude Code or open a new Claude Code session.',
|
||||
'ai.externalMcp.grokAdded': 'Grok MCP entry added. Restart Grok or open a new Grok session.',
|
||||
'ai.externalMcp.installCodex': 'Install Codex separately, then click Refresh.',
|
||||
'ai.externalMcp.installClaude': 'Install Claude Code separately, then click Refresh.',
|
||||
'ai.externalMcp.installGrok': 'Install the Grok CLI separately, then click Refresh.',
|
||||
'ai.externalMcp.conflict.description': 'A netcatty-external entry already exists and points elsewhere. Remove or edit it manually.',
|
||||
'ai.externalMcp.enableForLauncher': 'Enable External MCP to get a usable launcher path.',
|
||||
'ai.externalMcp.cursor.title': 'Cursor / other clients',
|
||||
'ai.externalMcp.cursor.description': 'Merge this into your MCP config (for example ~/.cursor/mcp.json). Do not replace the whole file if you already have other servers.',
|
||||
'ai.externalMcp.status.unavailable': 'Unavailable',
|
||||
'ai.externalMcp.status.disabled': 'Disabled',
|
||||
'ai.externalMcp.status.running': 'Running',
|
||||
'ai.externalMcp.status.starting': 'Starting',
|
||||
'ai.externalMcp.status.error': 'Error',
|
||||
'ai.externalMcp.status.configured': 'Configured',
|
||||
'ai.externalMcp.status.notConfigured': 'Not configured',
|
||||
'ai.externalMcp.status.checking': 'Checking',
|
||||
'ai.externalMcp.status.codexNotFound': 'Codex not found',
|
||||
'ai.externalMcp.status.claudeNotFound': 'Claude Code not found',
|
||||
'ai.externalMcp.status.grokNotFound': 'Grok not found',
|
||||
'ai.externalMcp.status.conflict': 'Conflict',
|
||||
'ai.userSkills.title': 'User Skills',
|
||||
'ai.userSkills.description': 'Open the Netcatty skills folder to add your own skill directories. Netcatty scans these skills automatically and injects only lightweight indexes unless a skill clearly matches the current request.',
|
||||
'ai.userSkills.openFolder': 'Open Skills Folder',
|
||||
'ai.userSkills.reload': 'Reload Skills',
|
||||
'ai.userSkills.location': 'Location',
|
||||
'ai.userSkills.loading': 'Scanning user skills...',
|
||||
'ai.userSkills.summary': '{ready} ready, {warnings} warnings',
|
||||
'ai.userSkills.empty': 'No user skills found yet. Open the folder to add skill directories with a SKILL.md file.',
|
||||
'ai.userSkills.unavailable': 'User skills are unavailable in this environment.',
|
||||
'ai.userSkills.status.ready': 'Ready',
|
||||
'ai.userSkills.status.warning': 'Warning',
|
||||
|
||||
// AI Quick Messages
|
||||
'ai.quickMessages.title': 'Quick Messages',
|
||||
'ai.quickMessages.description': 'Create reusable prompts you can insert from the AI chat with / or the quick-message button. Unlike user skills, quick messages fill the composer with text.',
|
||||
'ai.quickMessages.add': 'Add Quick Message',
|
||||
'ai.quickMessages.createTitle': 'New Quick Message',
|
||||
'ai.quickMessages.editTitle': 'Edit Quick Message',
|
||||
'ai.quickMessages.name': 'Name',
|
||||
'ai.quickMessages.name.placeholder': 'e.g. Check disk space',
|
||||
'ai.quickMessages.slug': 'Command',
|
||||
'ai.quickMessages.slug.placeholder': 'disk-check',
|
||||
'ai.quickMessages.descriptionField': 'Description (optional)',
|
||||
'ai.quickMessages.descriptionField.placeholder': 'Short hint about what this prompt does',
|
||||
'ai.quickMessages.content': 'Message content',
|
||||
'ai.quickMessages.content.placeholder': 'Full prompt text to insert when selected...',
|
||||
'ai.quickMessages.empty': 'No quick messages yet. Add a few prompts you use often.',
|
||||
'ai.quickMessages.confirmDelete': 'Delete quick message "{name}"?',
|
||||
'ai.quickMessages.error.nameRequired': 'Name is required.',
|
||||
'ai.quickMessages.error.invalidSlug': 'Command may only contain lowercase letters, numbers, and hyphens.',
|
||||
'ai.quickMessages.error.contentRequired': 'Message content is required.',
|
||||
'ai.quickMessages.error.slugTaken': 'This command is already used by another quick message.',
|
||||
'ai.quickMessages.error.slugConflictsWithSkill': 'This command conflicts with user skill "/{slug}". Choose another.',
|
||||
'ai.quickMessages.error.maxItems': 'You can save at most {max} quick messages.',
|
||||
|
||||
// AI Chat
|
||||
'ai.chat.noProvider': 'No AI provider is configured. Go to **Settings → AI → Providers** to add and enable a provider.',
|
||||
'ai.chat.toolDenied': 'Action was rejected by the user.',
|
||||
'ai.chat.toolApproved': 'Approved',
|
||||
'ai.chat.toolApprovalHint': 'Enter once · Esc reject',
|
||||
'ai.chat.approve': 'Approve',
|
||||
'ai.chat.approveOnce': 'Once',
|
||||
'ai.chat.alwaysAllow': 'Always',
|
||||
'ai.chat.slashStopDesc': 'Stop the current AI turn and cancel in-flight tools',
|
||||
'ai.chat.reject': 'Reject',
|
||||
'ai.chat.toolLabel': 'Tool',
|
||||
'ai.chat.targetLabel': 'Target',
|
||||
'ai.chat.permissionRequired': 'Permission Required',
|
||||
'ai.chat.permissionDescription': 'The AI agent wants to execute a tool call that requires your approval.',
|
||||
'ai.chat.commandBlocked': 'This command is blocked by your security policy and cannot be executed.',
|
||||
'ai.chat.recommendAllow': 'Allow',
|
||||
'ai.chat.recommendConfirm': 'Confirm',
|
||||
'ai.chat.recommendDeny': 'Deny',
|
||||
'ai.chat.exportConversation': 'Export conversation',
|
||||
'ai.chat.exportAs': 'Export As',
|
||||
'ai.chat.exportMarkdown': 'Markdown',
|
||||
'ai.chat.exportJSON': 'JSON',
|
||||
'ai.chat.exportPlainText': 'Plain Text',
|
||||
'ai.chat.thinking': 'Thinking',
|
||||
'ai.chat.thoughtFor': 'Thought for {duration}',
|
||||
'ai.chat.thought': 'Thought',
|
||||
'ai.chat.agents': 'Agents',
|
||||
'ai.chat.detectedOnMachine': 'Detected on this machine',
|
||||
'ai.chat.rescan': 'Re-scan',
|
||||
'ai.chat.permObserver': 'Observer',
|
||||
'ai.chat.permConfirm': 'Confirm',
|
||||
'ai.chat.permAuto': 'Auto',
|
||||
'ai.chat.permObserverDesc': 'Read only',
|
||||
'ai.chat.permConfirmDesc': 'Ask before actions',
|
||||
'ai.chat.permAutoDesc': 'Execute freely',
|
||||
'ai.chat.emptyHint': 'Ask about your servers, run commands, or get help with configurations.',
|
||||
'ai.chat.placeholder': 'Message {agent} — @ to include context, / for commands',
|
||||
'ai.chat.placeholderDefault': 'Message Catty Agent...',
|
||||
'ai.chat.noModel': 'No model',
|
||||
'ai.chat.noProviderModel': 'No default model — set one in Settings → AI → Providers.',
|
||||
'ai.chat.selectProvider': 'Select provider',
|
||||
'ai.chat.recent': 'Recent',
|
||||
'ai.chat.viewAll': 'View All',
|
||||
'ai.chat.untitled': 'Untitled',
|
||||
'ai.chat.justNow': 'Just now',
|
||||
'ai.chat.minutesAgo': '{n}m ago',
|
||||
'ai.chat.hoursAgo': '{n}h ago',
|
||||
'ai.chat.daysAgo': '{n}d ago',
|
||||
'ai.chat.newChat': 'New Chat',
|
||||
'ai.chat.allSessions': 'All Sessions',
|
||||
'ai.chat.loadEarlierMessages': 'Load earlier messages ({n} more)',
|
||||
'ai.chat.usedTools': 'Tools used: {n}',
|
||||
'ai.chat.loadMoreSessions': 'Load more sessions ({n} more)',
|
||||
'ai.chat.noSessions': 'No previous sessions',
|
||||
'ai.chat.retryHint': 'You can retry by sending your message again.',
|
||||
'ai.chat.approvalTimeout': 'Tool approval timed out after 5 minutes. You can retry by sending your message again.',
|
||||
'ai.chat.menuHosts': 'Hosts',
|
||||
'ai.chat.menuContext': 'Context',
|
||||
'ai.chat.menuFiles': 'Files',
|
||||
'ai.chat.menuImage': 'Image',
|
||||
'ai.chat.menuMentionHost': 'Mention Host',
|
||||
'ai.chat.menuUserSkills': 'User Skills',
|
||||
'ai.chat.menuSlashCommands': 'Slash Commands',
|
||||
'ai.chat.slashCommands': 'Slash commands',
|
||||
'ai.chat.slashQuickMessages': 'Quick messages',
|
||||
'ai.chat.slashUserSkills': 'User skills',
|
||||
'ai.chat.quickMessages': 'Slash commands',
|
||||
'ai.chat.slashNoResults': 'No matching commands',
|
||||
'ai.chat.slashEmptyHint': 'Add prompts in Settings → AI → Quick Messages.',
|
||||
|
||||
// AI Chat Shortcuts
|
||||
'ai.chatShortcuts.title': 'Chat Shortcuts',
|
||||
'ai.chatShortcuts.selectionAction': 'Show Add to Conversation when selecting terminal text',
|
||||
'ai.chatShortcuts.selectionAction.description': 'Show a small AI button next to selected terminal text.',
|
||||
|
||||
// AI Error
|
||||
'ai.codex.bridgeError': 'Codex main-process handlers are not loaded yet. Fully restart Netcatty, or restart the Electron dev process, then try again.',
|
||||
|
||||
// AI Web Search
|
||||
'ai.webSearch.title': 'Web Search',
|
||||
'ai.webSearch.enable': 'Enable Web Search',
|
||||
'ai.webSearch.enable.description': 'Allow the AI agent to search the web for current information.',
|
||||
'ai.webSearch.provider': 'Search Provider',
|
||||
'ai.webSearch.provider.description': 'Choose a web search API provider.',
|
||||
'ai.webSearch.apiKey': 'API Key',
|
||||
'ai.webSearch.apiKey.description': 'API key for the selected search provider.',
|
||||
'ai.webSearch.apiKey.placeholder': 'Enter API key...',
|
||||
'ai.webSearch.apiHost': 'API Host',
|
||||
'ai.webSearch.apiHost.description': 'Custom API endpoint. Leave default unless you use a proxy.',
|
||||
'ai.webSearch.apiHost.searxngDescription': 'URL of your SearXNG instance (required).',
|
||||
'ai.webSearch.maxResults': 'Max Results',
|
||||
'ai.webSearch.maxResults.description': 'Maximum number of search results to return (1-20).',
|
||||
|
||||
// AI Safety Settings
|
||||
'ai.safety.title': 'Safety',
|
||||
'ai.safety.permissionMode': 'Permission Mode',
|
||||
'ai.safety.permissionMode.description': 'Controls how the AI interacts with your Netcatty terminal sessions. Observer mode blocks write operations that go through Netcatty. External agent CLIs may still have their own local tools and approval flow.',
|
||||
'ai.safety.permissionMode.observer': 'Observer - Read only, no actions',
|
||||
'ai.safety.permissionMode.confirm': 'Confirm - Ask before actions',
|
||||
'ai.safety.permissionMode.auto': 'Auto - Execute freely',
|
||||
'ai.safety.commandTimeout': 'Command Timeout',
|
||||
'ai.safety.commandTimeout.description': 'Maximum seconds a command can run before being terminated through Netcatty execution.',
|
||||
'ai.safety.commandTimeout.unit': 'sec',
|
||||
'ai.safety.maxIterations': 'Max Iterations',
|
||||
'ai.safety.maxIterations.description': 'Maximum number of AI tool-use loops to prevent runaway execution. External agents may have their own internal iteration limits that take precedence.',
|
||||
'ai.safety.blocklist': 'Command Blocklist',
|
||||
'ai.safety.blocklist.description': 'Regex patterns to block dangerous commands executed through Netcatty.',
|
||||
'ai.safety.blocklist.placeholder': 'Regex pattern...',
|
||||
'ai.safety.blocklist.reset': 'Reset to defaults',
|
||||
'ai.safety.blocklist.add': 'Add pattern',
|
||||
'ai.safety.grants.title': 'Permission memory',
|
||||
'ai.safety.grants.heading': 'Confirm-mode allow rules',
|
||||
'ai.safety.grants.description': 'Confirm mode asks before running an operation. Saved rules automatically allow matching operations across all terminal sessions/nodes, and can be edited manually.',
|
||||
'ai.safety.grants.empty': 'No saved rules yet. Approve a tool with “Always allow”, or add one manually.',
|
||||
'ai.safety.grants.capability': 'Capability',
|
||||
'ai.safety.grants.sessionPattern': 'Session pattern',
|
||||
'ai.safety.grants.commandPattern': 'Command pattern (optional)',
|
||||
'ai.safety.grants.note': 'Note (optional)',
|
||||
'ai.safety.grants.add': 'Add rule',
|
||||
'ai.safety.grants.remove': 'Remove',
|
||||
'ai.safety.grants.export': 'Export JSON',
|
||||
'ai.safety.grants.import': 'Import JSON',
|
||||
'ai.safety.note': 'These safety settings are enforced for actions that go through Netcatty. External agent CLIs may also expose local tools that are governed by the agent itself.',
|
||||
|
||||
// Unified tooltips for terminal workspace and top tabs (issue #954)
|
||||
'terminal.layer.addTerminal': 'Add Terminal',
|
||||
'terminal.layer.switchToSplitView': 'Switch to Split View',
|
||||
'terminal.layer.sftp': 'SFTP',
|
||||
'terminal.layer.scripts': 'Scripts',
|
||||
'terminal.layer.history': 'History',
|
||||
'terminal.layer.theme': 'Theme',
|
||||
'terminal.layer.notes': 'Notes',
|
||||
'terminal.layer.aiChat': 'AI Chat',
|
||||
'terminal.layer.movePanelLeft': 'Move panel to left',
|
||||
'terminal.layer.movePanelRight': 'Move panel to right',
|
||||
'terminal.layer.closePanel': 'Close panel',
|
||||
'terminal.layer.hostTree.search': 'Search hosts...',
|
||||
'terminal.layer.hostTree.searchButton': 'Search',
|
||||
'terminal.layer.hostTree.tagsButton': 'Filter by tags',
|
||||
'terminal.layer.hostTree.newGroup': 'New group',
|
||||
'terminal.layer.hostTree.localShell': 'Local shell',
|
||||
'terminal.layer.hostTree.tagsEmpty': 'No tags available',
|
||||
'terminal.layer.hostTree.clearTags': 'Clear selection',
|
||||
'terminal.layer.hostTree.collapse': 'Collapse host list',
|
||||
'terminal.layer.hostTree.expand': 'Expand host list',
|
||||
'terminal.layer.hostTree.empty': 'No hosts found',
|
||||
'terminal.layer.hostTree.details.host': 'Host',
|
||||
'terminal.layer.hostTree.details.user': 'User',
|
||||
'terminal.layer.hostTree.details.port': 'Port',
|
||||
'terminal.layer.hostTree.details.protocol': 'Protocol',
|
||||
'terminal.layer.hostTree.details.group': 'Group',
|
||||
'terminal.layer.hostTree.details.tags': 'Tags',
|
||||
'terminal.layer.hostTree.details.lastConnected': 'Last connected',
|
||||
'topTabs.openQuickSwitcher': 'Open quick switcher',
|
||||
'topTabs.moreTabs': 'More tabs',
|
||||
'topTabs.aiAssistant': 'AI Assistant',
|
||||
'topTabs.windowOpacity': 'Window opacity',
|
||||
'topTabs.toggleTheme': 'Toggle theme',
|
||||
'topTabs.openSettings': 'Open Settings',
|
||||
'ai.chat.sessionHistory': 'Session history',
|
||||
'ai.chat.attach': 'Attach',
|
||||
'ai.chat.terminalSelectionAttachment': 'Terminal selection',
|
||||
'ai.chat.terminalSelectionLines': 'lines: {count}',
|
||||
'ai.chat.collapse': 'Collapse',
|
||||
'ai.chat.expand': 'Expand',
|
||||
'ai.chat.enableAgent': 'Enable {name}',
|
||||
'ai.chat.artifact.noteFallback': 'Vault note',
|
||||
'ai.chat.artifact.openNotes': 'Open Notes',
|
||||
'ai.chat.artifact.openHosts': 'Open Hosts',
|
||||
'ai.chat.artifact.notesSummary': '{count} notes in Vault',
|
||||
'ai.chat.artifact.hostsSummary': '{count} hosts in Vault',
|
||||
'ai.chat.artifact.hostsAdded': 'Added {count} hosts',
|
||||
'ai.chat.artifact.hostsPreview': 'Preview {count} hosts',
|
||||
'ai.chat.artifact.failed': 'Vault operation failed',
|
||||
'ai.chat.artifact.unavailableTitle': 'Unavailable',
|
||||
'ai.chat.artifact.noteMissing': 'This note is no longer in your Vault.',
|
||||
'ai.chat.artifact.hostMissing': 'This host is no longer in your Vault.',
|
||||
'ai.chat.artifact.snippetMissing': 'This snippet or script is no longer in your Vault.',
|
||||
'ai.chat.artifact.openSnippets': 'Open Snippets',
|
||||
'ai.chat.artifact.snippetsSummary': '{count} snippets in Vault',
|
||||
'ai.chat.artifact.scriptsSummary': '{count} scripts in Vault',
|
||||
'ai.chat.artifact.snippetFallback': 'Vault snippet',
|
||||
'ai.chat.artifact.scriptFallback': 'Automation script',
|
||||
'ai.chat.artifact.scriptLanguage': '{language} script',
|
||||
'ai.chat.artifact.snippetDeleted': 'Snippet deleted',
|
||||
'ai.chat.artifact.scriptDeleted': 'Script deleted',
|
||||
'ai.chat.artifact.snippetRan': 'Snippet executed',
|
||||
'ai.chat.artifact.scriptStarted': 'Script run started',
|
||||
'ai.chat.artifact.scriptRunStatus': 'Script run {status}',
|
||||
'ai.chat.artifact.scriptRunsSummary': '{count} script runs',
|
||||
'ai.chat.artifact.scriptRunStopped': 'Script run stopped',
|
||||
'ai.chat.artifact.scriptRunPaused': 'Script run paused',
|
||||
'ai.chat.artifact.scriptRunResumed': 'Script run resumed',
|
||||
'ai.chat.artifact.scriptReference': 'nct API reference',
|
||||
'zmodem.waitingForRemote': 'Waiting for remote...',
|
||||
'zmodem.uploading': 'Uploading',
|
||||
'zmodem.downloading': 'Downloading',
|
||||
'zmodem.cancelTransfer': 'Cancel transfer (Ctrl+C)',
|
||||
'zmodem.overwrite.title': 'Remote file already exists',
|
||||
'zmodem.overwrite.applyToRest': 'Apply to remaining conflicts',
|
||||
'zmodem.overwrite.overwrite': 'Overwrite',
|
||||
'zmodem.overwrite.skip': 'Skip',
|
||||
'zmodem.overwrite.cancel': 'Cancel',
|
||||
'settings.shortcuts.resetToDefault': 'Reset to default',
|
||||
};
|
||||
@@ -0,0 +1,802 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const enCoreMessages: Messages = {
|
||||
// Common
|
||||
'common.save': 'Save',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.close': 'Close',
|
||||
'common.reset': 'Reset',
|
||||
'common.zoomIn': 'Zoom in',
|
||||
'common.zoomOut': 'Zoom out',
|
||||
'common.settings': 'Settings',
|
||||
'common.search': 'Search',
|
||||
'common.searchPlaceholder': 'Search...',
|
||||
'common.connect': 'Connect',
|
||||
'common.terminal': 'Terminal',
|
||||
'common.create': 'Create',
|
||||
'common.import': 'Import',
|
||||
'common.generate': 'Generate',
|
||||
'common.delete': 'Delete',
|
||||
'common.edit': 'Edit',
|
||||
'common.clear': 'Clear',
|
||||
'common.optional': 'Optional',
|
||||
'common.selectPlaceholder': 'Select...',
|
||||
'common.add': 'Add',
|
||||
'common.rename': 'Rename',
|
||||
'common.refresh': 'Refresh',
|
||||
'common.continue': 'Continue',
|
||||
'common.enabled': 'Enabled',
|
||||
'common.disabled': 'Disabled',
|
||||
'common.error': 'Error',
|
||||
'common.validation': 'Validation',
|
||||
'common.unknownError': 'Unknown error',
|
||||
'common.noResultsFound': 'No results found',
|
||||
'common.back': 'Back',
|
||||
'common.apply': 'Apply',
|
||||
'common.use': 'Use',
|
||||
'common.useGlobal': 'Use global',
|
||||
'common.saveChanges': 'Save Changes',
|
||||
'common.advanced': 'Advanced',
|
||||
'common.left': 'Left',
|
||||
'common.right': 'Right',
|
||||
'common.more': 'More',
|
||||
'common.selectAHost': 'Select a host',
|
||||
'common.selectAHostPlaceholder': 'Select a host...',
|
||||
'sort.manual': 'Manual order',
|
||||
'sort.az': 'A-z',
|
||||
'sort.za': 'Z-a',
|
||||
'sort.newest': 'Newest to oldest',
|
||||
'sort.oldest': 'Oldest to newest',
|
||||
'sort.group': 'By group',
|
||||
'field.label': 'Label',
|
||||
'field.type': 'Type',
|
||||
'auth.keyType': 'Type {type}',
|
||||
'auth.showAllKeys': 'Show all keys',
|
||||
|
||||
// Dialogs / prompts
|
||||
'confirm.deleteHost': 'Delete Host "{name}"?',
|
||||
'confirm.deleteIdentity': 'Delete Identity "{name}"?',
|
||||
'confirm.removeProvider': 'Remove provider "{name}"?',
|
||||
'confirm.closeBusyTerminal.title': 'Confirm close',
|
||||
'confirm.closeBusyTerminal.message': 'Process "{command}" is still running and will be terminated.',
|
||||
'confirm.closeBusyTerminal.messageWithMore': 'Process "{command}" and {count} other running process(es) will be terminated.',
|
||||
'confirm.closeBusyTerminal.cancel': 'Cancel',
|
||||
'confirm.closeBusyTerminal.close': 'Close',
|
||||
'dialog.createWorkspace.title': 'Create Workspace',
|
||||
'dialog.renameWorkspace.title': 'Rename workspace',
|
||||
'dialog.renameSession.title': 'Rename session',
|
||||
'field.name': 'Name',
|
||||
'field.selectHosts': 'Select Hosts',
|
||||
'placeholder.workspaceName': 'Workspace name',
|
||||
'placeholder.sessionName': 'Session name',
|
||||
'placeholder.searchHosts': 'Search hosts...',
|
||||
'toast.settingsUnavailable': 'Settings window is unavailable on this platform.',
|
||||
'credentials.protectionUnavailable.title': 'Credential Protection Unavailable',
|
||||
'credentials.protectionUnavailable.message': 'Saved passwords and keys cannot be auto-decrypted on this device. Re-enter credentials before connecting.',
|
||||
'credentials.protectionUnavailable.action': 'Open Settings',
|
||||
|
||||
// Settings shell
|
||||
'settings.title': 'Settings',
|
||||
'settings.tab.application': 'Application',
|
||||
'settings.tab.appearance': 'Appearance',
|
||||
'settings.tab.terminal': 'Terminal',
|
||||
'settings.tab.shortcuts': 'Shortcuts',
|
||||
'settings.tab.syncCloud': 'Sync & Cloud',
|
||||
'settings.tab.system': 'System',
|
||||
|
||||
// Settings > System
|
||||
'settings.system.title': 'System',
|
||||
'settings.system.description': 'System information and temporary file management.',
|
||||
'settings.system.tempDirectory': 'Temporary Files',
|
||||
'settings.system.location': 'Location',
|
||||
'settings.system.fileCount': 'Files',
|
||||
'settings.system.totalSize': 'Size',
|
||||
'settings.system.openFolder': 'Open folder',
|
||||
'settings.system.refresh': 'Refresh',
|
||||
'settings.system.clearTempFiles': 'Clear temp files',
|
||||
'settings.system.clearing': 'Clearing...',
|
||||
'settings.system.clearResult': 'Deleted {deleted} file(s), {failed} failed.',
|
||||
'settings.system.tempDirectoryHint': 'Temporary files are created when opening remote files with external applications. They are automatically cleaned up when SFTP sessions close.',
|
||||
'settings.system.credentials.title': 'Credential Protection',
|
||||
'settings.system.credentials.status': 'Status',
|
||||
'settings.system.credentials.checking': 'Checking...',
|
||||
'settings.system.credentials.available': 'Available (OS keychain ready)',
|
||||
'settings.system.credentials.unavailable': 'Unavailable (cannot decrypt saved credentials)',
|
||||
'settings.system.credentials.unknown': 'Unknown (not supported in this environment)',
|
||||
'settings.system.credentials.unavailableHint': 'Credentials encrypted on another user profile or machine cannot be decrypted here. Re-enter and save credentials on this device.',
|
||||
'settings.system.credentials.portabilityHint': 'Cloud Sync is portable because it uses your master key encryption. Local safeStorage encryption is device/user scoped.',
|
||||
|
||||
// Settings > System > Network Proxy
|
||||
'settings.system.networkProxy.title': 'Network Proxy',
|
||||
'settings.system.networkProxy.description': 'HTTP(S) proxy for cloud sync and AI providers. This is separate from SSH ProxyJump / ProxyCommand profiles.',
|
||||
'settings.system.networkProxy.mode': 'Proxy mode',
|
||||
'settings.system.networkProxy.mode.system': 'Use system proxy',
|
||||
'settings.system.networkProxy.mode.direct': 'Direct (no proxy)',
|
||||
'settings.system.networkProxy.mode.custom': 'Custom proxy',
|
||||
'settings.system.networkProxy.url': 'Proxy URL',
|
||||
'settings.system.networkProxy.url.placeholder': 'http://127.0.0.1:7890',
|
||||
'settings.system.networkProxy.url.desc': 'Supports http://, https://, and socks5:// host:port URLs. Proxy credentials in the URL are not supported.',
|
||||
'settings.system.networkProxy.bypass': 'Bypass list',
|
||||
'settings.system.networkProxy.bypass.placeholder': '<local>,localhost,127.0.0.1',
|
||||
'settings.system.networkProxy.bypass.desc': 'Comma-separated hosts that skip the proxy. Use <local> for loopback and LAN names.',
|
||||
'settings.system.networkProxy.hint': 'Affects Google Drive / OneDrive / GitHub sync login and AI provider requests. Does not change SSH connections.',
|
||||
|
||||
// Settings > System > Crash Logs
|
||||
'settings.system.crashLogs.title': 'Crash Logs',
|
||||
'settings.system.crashLogs.description': 'View error logs from the main process to help diagnose unexpected behavior.',
|
||||
'settings.system.crashLogs.noLogs': 'No crash logs found.',
|
||||
'settings.system.crashLogs.entries': '{count} entries',
|
||||
'settings.system.crashLogs.clear': 'Clear all logs',
|
||||
'settings.system.crashLogs.cleared': 'Cleared {count} log file(s).',
|
||||
'settings.system.crashLogs.source': 'Source',
|
||||
'settings.system.crashLogs.time': 'Time',
|
||||
'settings.system.crashLogs.message': 'Message',
|
||||
'settings.system.crashLogs.stack': 'Stack Trace',
|
||||
'settings.system.crashLogs.hint': 'Crash logs are retained for 30 days and automatically rotated.',
|
||||
'settings.system.crashLogs.collapse': 'Collapse',
|
||||
'settings.system.crashLogs.expand': 'Show details',
|
||||
|
||||
// Settings > System > Software Update
|
||||
'settings.update.title': 'Software Update',
|
||||
'settings.update.currentVersion': 'Current version',
|
||||
'settings.update.checkForUpdates': 'Check for Updates',
|
||||
'settings.update.checking': 'Checking...',
|
||||
'settings.update.upToDate': 'You are using the latest version.',
|
||||
'settings.update.available': 'New version {version} is available.',
|
||||
'settings.update.download': 'Download Update',
|
||||
'settings.update.downloading': 'Downloading... {percent}%',
|
||||
'settings.update.readyToInstall': 'Update downloaded and ready to install.',
|
||||
'settings.update.restartNow': 'Restart to Update',
|
||||
'settings.update.error': 'Failed to check for updates.',
|
||||
'settings.update.downloadError': 'Download failed.',
|
||||
'settings.update.manualDownload': 'Download from GitHub',
|
||||
'settings.update.manualDownloadHint': 'Auto-update is not available on this platform. Download the latest version from GitHub.',
|
||||
'settings.update.hint': 'Netcatty checks for updates from GitHub Releases.',
|
||||
'settings.update.lastCheckedJustNow': 'just now',
|
||||
'settings.update.lastCheckedMinutesAgo': '{n} min ago',
|
||||
'settings.update.lastCheckedHoursAgo': '{n} hr ago',
|
||||
'settings.update.lastCheckedPrefix': 'Last checked: ',
|
||||
'settings.update.autoUpdateEnabled': 'Automatic Updates',
|
||||
'settings.update.autoUpdateEnabledDesc': 'Automatically check and download updates when available.',
|
||||
|
||||
// Settings > Session Restore
|
||||
'settings.sessionRestore.title': 'Session Restore',
|
||||
'settings.sessionRestore.restorePreviousSession': 'Restore previous tabs and workspaces',
|
||||
'settings.sessionRestore.restorePreviousSessionDesc': 'Reopen terminal tabs and workspace layouts from the last app session.',
|
||||
'settings.sessionRestore.restoreTerminalCwd': 'Restore terminal working directory',
|
||||
'settings.sessionRestore.restoreTerminalCwdDesc': 'When reconnecting a restored terminal, try to return to its last known directory without blocking the connection.',
|
||||
'terminal.restore.placeholder.title': 'Restored session',
|
||||
'terminal.restore.placeholder.desc': 'This terminal is disconnected. Reconnect when you are ready to start a new shell for this tab.',
|
||||
'terminal.restore.placeholder.reconnect': 'Reconnect',
|
||||
'terminal.restore.cwdLog': 'Restoring working directory: {cwd}',
|
||||
|
||||
// Settings > Session Logs
|
||||
'settings.sessionLogs.title': 'Session Logs',
|
||||
'settings.sessionLogs.description': 'Configure session log export and auto-save settings.',
|
||||
'settings.sessionLogs.autoSave': 'Auto-Save',
|
||||
'settings.sessionLogs.enableAutoSave': 'Enable auto-save',
|
||||
'settings.sessionLogs.enableAutoSaveDesc': 'Automatically save session logs when terminal sessions end.',
|
||||
'settings.sessionLogs.directory': 'Save Directory',
|
||||
'settings.sessionLogs.noDirectory': 'No directory selected',
|
||||
'settings.sessionLogs.browse': 'Browse',
|
||||
'settings.sessionLogs.openFolder': 'Open folder',
|
||||
'settings.sessionLogs.directoryHint': 'Logs will be organized by host in subdirectories.',
|
||||
'settings.sessionLogs.format': 'Log Format',
|
||||
'settings.sessionLogs.formatDesc': 'Choose the format for saved log files.',
|
||||
'settings.sessionLogs.formatTxt': 'Plain Text (.txt)',
|
||||
'settings.sessionLogs.formatRaw': 'Raw with ANSI (.log)',
|
||||
'settings.sessionLogs.formatHtml': 'HTML (.html)',
|
||||
'settings.sessionLogs.timestamps': 'Add timestamps',
|
||||
'settings.sessionLogs.timestampsDesc': 'Prefix each line in plain text and HTML logs with the local time.',
|
||||
'settings.sessionLogs.hint': 'Session logs capture all terminal output for troubleshooting and auditing purposes.',
|
||||
|
||||
// Settings > SSH Debug Logs
|
||||
'settings.sshDebugLogs.title': 'SSH Debug Logs',
|
||||
'settings.sshDebugLogs.enable': 'Enable SSH debug logs',
|
||||
'settings.sshDebugLogs.enableDesc': 'Record connection, auth, handshake, disconnect, and error reasons without saving terminal output.',
|
||||
'settings.sshDebugLogs.location': 'Log Location',
|
||||
'settings.sshDebugLogs.status': 'Status',
|
||||
'settings.sshDebugLogs.statusOn': 'On',
|
||||
'settings.sshDebugLogs.statusOff': 'Off',
|
||||
'settings.sshDebugLogs.size': 'Size',
|
||||
'settings.sshDebugLogs.hint': 'When enabled, newly started SSH connections write diagnostic events for bastion, auth, and unexpected disconnect troubleshooting.',
|
||||
|
||||
// Settings > Global Hotkey (Quake Mode)
|
||||
'settings.globalHotkey.title': 'Global Hotkey',
|
||||
'settings.globalHotkey.toggleWindow': 'Toggle Window',
|
||||
'settings.globalHotkey.toggleWindowDesc': 'Press a key combination to set a global shortcut for showing/hiding the window.',
|
||||
'settings.globalHotkey.notSet': 'Not set',
|
||||
'settings.globalHotkey.reset': 'Reset to default',
|
||||
'settings.globalHotkey.closeToTray': 'Close to System Tray',
|
||||
'settings.globalHotkey.closeToTrayDesc': 'When enabled, closing the window will minimize to the system tray instead of quitting.',
|
||||
'settings.globalHotkey.enabled': 'Enable Global Hotkey',
|
||||
'settings.globalHotkey.enabledDesc': 'Register system-wide keyboard shortcuts. When disabled, all global hotkeys are unregistered.',
|
||||
'settings.globalHotkey.hint': 'Global hotkey works system-wide to quickly show or hide the window (Quake-style terminal).',
|
||||
|
||||
// Tray Panel
|
||||
'tray.openMainWindow': 'Open Main Window',
|
||||
'tray.sessions': 'Sessions',
|
||||
'tray.portForwarding': 'Port Forwarding',
|
||||
'tray.status.connected': 'Connected',
|
||||
'tray.status.connecting': 'Connecting',
|
||||
'tray.status.disconnected': 'Disconnected',
|
||||
'tray.status.active': 'Active',
|
||||
'tray.status.inactive': 'Inactive',
|
||||
'tray.status.error': 'Error',
|
||||
'tray.recentHosts': 'Recent Hosts',
|
||||
'tray.empty.title': 'Nothing here yet',
|
||||
'tray.empty.subtitle': 'Go connect to a server, they miss you 🚀',
|
||||
'tray.quit': 'Quit Netcatty',
|
||||
|
||||
// Vault Sidebar
|
||||
'vault.sidebar.collapse': 'Collapse sidebar',
|
||||
'vault.sidebar.expand': 'Expand sidebar',
|
||||
'vault.sidebar.resize': 'Resize sidebar',
|
||||
|
||||
// Settings > Application
|
||||
'settings.application.checkUpdates': 'Check for updates',
|
||||
'settings.application.reportProblem': 'Report a problem',
|
||||
'settings.application.reportProblem.subtitle': 'Generate a pre-filled GitHub issue',
|
||||
'settings.application.community': 'Community',
|
||||
'settings.application.community.subtitle': 'On GitHub Discussions',
|
||||
'settings.application.github': 'GitHub',
|
||||
'settings.application.github.subtitle': 'Source code',
|
||||
'settings.application.whatsNew': "What's new",
|
||||
'settings.application.whatsNew.subtitle': 'Show release notes',
|
||||
'settings.application.openExternal.failedTitle': 'Cannot open link',
|
||||
'settings.application.openExternal.failedBody': 'The link could not be opened in either the system browser or the built-in browser window.',
|
||||
'settings.vault.title': 'Vault',
|
||||
'settings.vault.showRecentHosts': 'Show recently connected hosts',
|
||||
'settings.vault.showRecentHostsDesc': 'Display a section of recently connected hosts at the top of the vault',
|
||||
'settings.vault.showOnlyUngroupedHostsInRoot': 'Only show ungrouped hosts at root',
|
||||
'settings.vault.showOnlyUngroupedHostsInRootDesc': 'When enabled, the root host list only shows hosts without a group. Open a group from the sidebar to see grouped hosts.',
|
||||
'settings.vault.showSftpTab': 'Show SFTP tab',
|
||||
'settings.vault.showSftpTabDesc': 'Display the standalone SFTP view in the top tab bar. When hidden, use the in-session SFTP side panel instead.',
|
||||
'settings.vault.showHostTreeSidebar': 'Show host list sidebar',
|
||||
'settings.vault.showHostTreeSidebarDesc': 'Display the host list sidebar and its top-bar toggle on terminal and editor tabs.',
|
||||
|
||||
// Update notifications
|
||||
'update.available.title': 'Update Available',
|
||||
'update.available.message': 'A new version {version} is available. Click to download.',
|
||||
'update.checking': 'Checking for updates...',
|
||||
'update.upToDate.title': 'Up to Date',
|
||||
'update.upToDate.message': 'You are running the latest version ({version}).',
|
||||
'update.error': 'Failed to check for updates',
|
||||
'update.downloadNow': 'Download Now',
|
||||
'update.viewInSettings': 'View in Settings',
|
||||
'update.readyToInstall.title': 'Update Ready',
|
||||
'update.readyToInstall.message': 'Version {version} downloaded and ready to install.',
|
||||
'update.restartNow': 'Restart Now',
|
||||
'update.downloadFailed.title': 'Update Failed',
|
||||
'update.downloadFailed.message': 'Failed to download update. You can download it manually.',
|
||||
'update.needsSave.title': 'Unsaved Changes',
|
||||
'update.needsSave.message': 'Save your open editors first, then click Restart Now again to install the update.',
|
||||
'update.openReleases': 'Open Releases',
|
||||
'update.remindLater': 'Remind Later',
|
||||
'update.skipVersion': 'Skip This Version',
|
||||
|
||||
// Settings > Appearance
|
||||
'settings.appearance.uiTheme': 'UI Theme',
|
||||
'settings.appearance.theme': 'Theme',
|
||||
'settings.appearance.theme.desc': 'Choose light, dark, or follow system preference',
|
||||
'settings.appearance.theme.light': 'Light',
|
||||
'settings.appearance.theme.dark': 'Dark',
|
||||
'settings.appearance.theme.system': 'System',
|
||||
'settings.appearance.accentColor': 'Accent Color',
|
||||
'settings.appearance.customColor': 'Custom color',
|
||||
'settings.appearance.accentColor.mode': 'Use custom accent',
|
||||
'settings.appearance.accentColor.mode.desc': 'Override the theme accent color',
|
||||
'settings.appearance.accentColor.custom': 'Custom accent',
|
||||
'settings.appearance.themeColor': 'Theme Color',
|
||||
'settings.appearance.themeColor.desc': 'Pick a preset palette for each theme',
|
||||
'settings.appearance.themeColor.light': 'Light palette',
|
||||
'settings.appearance.themeColor.dark': 'Dark palette',
|
||||
'settings.appearance.customCss': 'Custom CSS',
|
||||
'settings.appearance.customCss.desc':
|
||||
'Add custom CSS to personalize the app appearance. Changes apply immediately. Major UI regions expose a [data-section="..."] attribute you can target — e.g. snippets-panel, host-details-panel, group-details-panel, serial-host-details-panel, ai-chat-panel, vault-sidebar, vault-main, vault-hosts-header, vault-host-list, vault-view, terminal-workspace, terminal-workspace-sidebar (focus-mode terminal list), terminal-host-tree-sidebar, terminal-host-tree-sidebar-content, terminal-host-tree-sidebar-row, terminal-side-panel (SFTP/Scripts/Theme/AI panel, available while open), terminal-side-panel-tabs, terminal-side-panel-content, terminal-sftp-panel, terminal-sftp-host-header, terminal-sftp-pane, terminal-sftp-toolbar, terminal-sftp-path, terminal-sftp-filter-bar, terminal-sftp-list, terminal-sftp-list-header, terminal-sftp-list-row, terminal-sftp-tree, terminal-sftp-tree-row, terminal-sftp-transfer-queue, terminal-sftp-transfer-row, terminal-split-pane, terminal-split-resizer, top-tabs, top-tabs-host-tree-toggle, top-tabs-quick-switcher-toggle.',
|
||||
'settings.appearance.customCss.placeholder':
|
||||
'/* Examples — use !important to beat Tailwind utility specificity */\n\n/* Hide the host-list toggle in the top tab bar */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* Hide the plus button that opens the quick switcher */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* Border around the SFTP / side panel (does not linger after closing) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* Change the whole side panel background, not only the top tabs */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* Style selected SFTP file rows */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* Thicker split dividers */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* Highlight the focused split pane */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* Or use Settings → Terminal → Workspace Focus Indicator → Border on focused pane */',
|
||||
'settings.appearance.language': 'Language',
|
||||
'settings.appearance.language.desc': 'Choose the UI language',
|
||||
'settings.appearance.uiFont': 'Interface Font',
|
||||
'settings.appearance.uiFont.desc': 'Choose the font for the application interface',
|
||||
'settings.appearance.windowOpacity': 'Window Opacity',
|
||||
'settings.appearance.windowOpacity.desc': 'Adjust the transparency of the entire application window. Lower values also fade terminal text. Some Linux desktop environments may not support this.',
|
||||
'settings.appearance.appIcon': 'App Icon',
|
||||
'settings.appearance.appIcon.desc': 'Choose a dock/taskbar icon style to make Netcatty easier to spot when switching apps. Applies at runtime; the installed app bundle icon is unchanged.',
|
||||
'settings.appearance.appIcon.original': 'Original',
|
||||
'settings.appearance.appIcon.bright': 'Bright',
|
||||
'settings.appearance.appIcon.dark': 'Dark',
|
||||
'settings.appearance.appIcon.colorful': 'Colorful',
|
||||
'settings.appearance.appIcon.highContrast': 'High contrast',
|
||||
'settings.appearance.appIcon.group.classic': 'Colored backgrounds',
|
||||
'settings.appearance.appIcon.group.white': 'White background',
|
||||
'settings.appearance.appIcon.group.special': 'Hidden',
|
||||
'settings.appearance.appIcon.whiteNavy': 'White · Navy cat',
|
||||
'settings.appearance.appIcon.whiteSky': 'White · Sky cat',
|
||||
'settings.appearance.appIcon.whiteRose': 'White · Rose cat',
|
||||
'settings.appearance.appIcon.whiteEmerald': 'White · Emerald cat',
|
||||
'settings.appearance.appIcon.whiteAmber': 'White · Amber cat',
|
||||
'settings.appearance.appIcon.whiteViolet': 'White · Violet cat',
|
||||
'settings.appearance.appIcon.rainbow': 'Rainbow cat',
|
||||
// Settings > Terminal
|
||||
'settings.terminal.section.theme': 'Terminal Theme',
|
||||
'settings.terminal.themeModal.title': 'Select Theme',
|
||||
'settings.terminal.themeModal.darkThemes': 'Dark Themes',
|
||||
'settings.terminal.themeModal.lightThemes': 'Light Themes',
|
||||
'settings.terminal.theme.selectButton': 'Select Theme',
|
||||
'settings.terminal.theme.followApp': 'Follow Application Theme',
|
||||
'settings.terminal.theme.followApp.desc': 'Automatically match the terminal background to the current app theme for a seamless look.',
|
||||
'settings.terminal.theme.darkTheme': 'Dark mode terminal theme',
|
||||
'settings.terminal.theme.lightTheme': 'Light mode terminal theme',
|
||||
'settings.terminal.theme.auto': 'Auto (match app theme)',
|
||||
'settings.terminal.theme.autoDesc': 'Follows the active UI theme preset',
|
||||
'settings.terminal.section.font': 'Font',
|
||||
'settings.terminal.section.cursor': 'Cursor',
|
||||
'settings.terminal.section.keyboard': 'Keyboard',
|
||||
'settings.terminal.section.accessibility': 'Accessibility',
|
||||
'settings.terminal.section.behavior': 'Behavior',
|
||||
'settings.terminal.section.sidePanel': 'Side Panel',
|
||||
'settings.terminal.section.scrollback': 'Scrollback',
|
||||
'settings.terminal.section.keywordHighlight': 'Keyword highlighting',
|
||||
'settings.terminal.font.family': 'Font',
|
||||
'settings.terminal.font.family.desc': 'Terminal font family',
|
||||
'settings.terminal.font.cjk': 'CJK font',
|
||||
'settings.terminal.font.cjk.desc': 'Font used for Chinese / Japanese / Korean characters; "Auto" picks one based on the primary font',
|
||||
'settings.terminal.font.cjk.option.auto': 'Auto · paired with the primary font',
|
||||
'settings.terminal.font.cjk.option.sarasaSC': 'Sarasa Mono SC (Iosevka + Source Han SC)',
|
||||
'settings.terminal.font.cjk.option.sarasaTC': 'Sarasa Mono TC (Iosevka + Source Han TC)',
|
||||
'settings.terminal.font.cjk.option.mapleCN': 'Maple Mono CN',
|
||||
'settings.terminal.font.cjk.option.sourceHan': 'Source Han Mono SC',
|
||||
'settings.terminal.font.cjk.option.notoCJK': 'Noto Sans Mono CJK SC',
|
||||
'settings.terminal.font.cjk.option.lxgwWenkai': 'LXGW WenKai Mono',
|
||||
'settings.terminal.font.cjk.option.simSun': 'SimSun',
|
||||
'settings.terminal.font.cjk.option.legacy': '{font} · not recommended (proportional font)',
|
||||
'settings.terminal.font.size': 'Font size',
|
||||
'settings.terminal.font.size.desc': 'Terminal text size',
|
||||
'settings.terminal.font.weight': 'Font weight',
|
||||
'settings.terminal.font.weight.desc': 'Weight for regular text (100-900)',
|
||||
'settings.terminal.font.weight.thin': 'Thin',
|
||||
'settings.terminal.font.weight.extraLight': 'Extra Light',
|
||||
'settings.terminal.font.weight.light': 'Light',
|
||||
'settings.terminal.font.weight.normal': 'Normal',
|
||||
'settings.terminal.font.weight.medium': 'Medium',
|
||||
'settings.terminal.font.weight.semiBold': 'Semi Bold',
|
||||
'settings.terminal.font.weight.bold': 'Bold',
|
||||
'settings.terminal.font.weight.extraBold': 'Extra Bold',
|
||||
'settings.terminal.font.weight.black': 'Black',
|
||||
'settings.terminal.font.weightBold': 'Bold font weight',
|
||||
'settings.terminal.font.weightBold.desc': 'Weight for bold text (100-900)',
|
||||
'settings.terminal.font.smoothing': 'Font smoothing',
|
||||
'settings.terminal.font.smoothing.desc': 'Use native macOS font anti-aliasing',
|
||||
'settings.terminal.font.linePadding': 'Line padding',
|
||||
'settings.terminal.font.linePadding.desc': 'Additional space between lines (0-10)',
|
||||
'settings.terminal.font.emulationType': 'Terminal emulation type',
|
||||
'settings.terminal.cursor.style': 'Cursor style',
|
||||
'settings.terminal.cursor.style.block': 'Block',
|
||||
'settings.terminal.cursor.style.bar': 'Bar',
|
||||
'settings.terminal.cursor.style.underline': 'Underline',
|
||||
'settings.terminal.cursor.blink': 'Cursor blink',
|
||||
'settings.terminal.keyboard.altAsMeta': 'Use Option as Meta key',
|
||||
'settings.terminal.keyboard.altAsMeta.desc':
|
||||
'Use Option (Alt) as the Meta key instead of for special characters',
|
||||
'settings.terminal.keyboard.optionArrowWordJump': 'Option+←/→ jumps by word',
|
||||
'settings.terminal.keyboard.optionArrowWordJump.desc':
|
||||
'Send Meta-b / Meta-f on Option+Left/Right so the shell moves by word, instead of the default ^[[1;3D / ^[[1;3C',
|
||||
'settings.terminal.accessibility.minimumContrastRatio': 'Minimum contrast ratio',
|
||||
'settings.terminal.sidePanel.autoOpen': 'Open side panel with terminal',
|
||||
'settings.terminal.sidePanel.autoOpen.desc': 'Automatically open a side panel when a terminal connects.',
|
||||
'settings.terminal.sidePanel.autoOpenPane': 'Default side panel',
|
||||
'settings.terminal.sidePanel.autoOpenPane.desc': 'Choose which panel opens first.',
|
||||
'settings.terminal.sidePanel.pane.sftp': 'SFTP',
|
||||
'settings.terminal.sidePanel.pane.scripts': 'Scripts',
|
||||
'settings.terminal.sidePanel.pane.history': 'History',
|
||||
'settings.terminal.sidePanel.pane.theme': 'Theme',
|
||||
'settings.terminal.sidePanel.pane.system': 'System',
|
||||
'settings.terminal.sidePanel.pane.notes': 'Notes',
|
||||
'settings.terminal.sidePanel.pane.ai': 'AI',
|
||||
'settings.terminal.accessibility.minimumContrastRatio.desc':
|
||||
'Adjust colors to meet contrast requirements (1 = disabled, 21 = max)',
|
||||
'settings.terminal.behavior.rightClick': 'Right-click behavior',
|
||||
'settings.terminal.behavior.rightClick.desc': 'Action when right-clicking in terminal',
|
||||
'settings.terminal.behavior.rightClick.menu': 'Show menu',
|
||||
'settings.terminal.behavior.rightClick.paste': 'Paste',
|
||||
'settings.terminal.behavior.rightClick.selectWord': 'Select word',
|
||||
'settings.terminal.behavior.copyOnSelect': 'Copy on select',
|
||||
'settings.terminal.behavior.copyOnSelect.desc': 'Automatically copy selected text. In tmux/vim with mouse mode, hold Option on macOS or Shift on Windows/Linux to select',
|
||||
'settings.terminal.behavior.middleClickPaste': 'Middle-click paste',
|
||||
'settings.terminal.behavior.middleClickPaste.desc':
|
||||
'Paste clipboard content on middle-click',
|
||||
'settings.terminal.behavior.middleClick': 'Middle-click behavior',
|
||||
'settings.terminal.behavior.middleClick.desc': 'Action when middle-clicking in terminal',
|
||||
'settings.terminal.behavior.middleClick.menu': 'Show menu',
|
||||
'settings.terminal.behavior.middleClick.paste': 'Paste',
|
||||
'settings.terminal.behavior.middleClick.disabled': 'Do nothing',
|
||||
'settings.terminal.behavior.wordSeparators': 'Word separators',
|
||||
'settings.terminal.behavior.wordSeparators.desc':
|
||||
'Characters treated as word boundaries for double-click selection. Spaces count too; add =, comma, or : for strings like uid=name.',
|
||||
'settings.terminal.behavior.bracketedPaste': 'Bracketed paste mode',
|
||||
'settings.terminal.behavior.bracketedPaste.desc':
|
||||
'Wrap pasted text with escape sequences so the shell can distinguish paste from typed input. Disable if you see ^[[200~ artifacts.',
|
||||
'settings.terminal.behavior.shiftEnterNewline': 'Shift+Enter text',
|
||||
'settings.terminal.behavior.shiftEnterNewline.desc':
|
||||
'Send configured text instead of normal Enter when pressing Shift+Enter in the terminal.',
|
||||
'settings.terminal.behavior.shiftEnterNewlineText': 'Text to send',
|
||||
'settings.terminal.behavior.shiftEnterNewlineText.desc':
|
||||
'Use \\n for newline, \\t for tab, and \\\\ for backslash.',
|
||||
'settings.terminal.behavior.clearWipesScrollback': '`clear` wipes scrollback',
|
||||
'settings.terminal.behavior.clearWipesScrollback.desc':
|
||||
'Make `clear` also wipe the scrollback buffer (POSIX default). Disable to keep history visible after `clear`.',
|
||||
'settings.terminal.behavior.preserveSelectionOnInput': 'Keep selection while typing',
|
||||
'settings.terminal.behavior.preserveSelectionOnInput.desc':
|
||||
'Don\'t clear mouse-selected text when typing — useful for selecting a path then pasting it after a command prefix like `sz `.',
|
||||
'settings.terminal.behavior.forcePromptNewLine': 'Prompt on a new line',
|
||||
'settings.terminal.behavior.forcePromptNewLine.desc':
|
||||
'When the final line of command output is not terminated by a newline, move the recognized shell prompt to the next visual line.',
|
||||
'settings.terminal.behavior.dynamicTabTitle': 'Dynamic tab title',
|
||||
'settings.terminal.behavior.dynamicTabTitle.desc':
|
||||
'Choose when session tabs should follow shell-reported window titles.',
|
||||
'settings.terminal.behavior.dynamicTabTitle.off': 'Disabled',
|
||||
'settings.terminal.behavior.dynamicTabTitle.agent': 'Agents only',
|
||||
'settings.terminal.behavior.dynamicTabTitle.all': 'All sessions',
|
||||
'settings.terminal.behavior.osc52Clipboard': 'OSC-52 clipboard',
|
||||
'settings.terminal.behavior.osc52Clipboard.desc':
|
||||
'Allow remote programs (tmux, vim, etc.) to access the local clipboard via OSC-52 escape sequences.',
|
||||
'settings.terminal.behavior.osc52Clipboard.off': 'Disabled',
|
||||
'settings.terminal.behavior.osc52Clipboard.writeOnly': 'Write only',
|
||||
'settings.terminal.behavior.osc52Clipboard.readWrite': 'Read & Write',
|
||||
'settings.terminal.behavior.osc52Clipboard.prompt': 'Write + Prompt on Read',
|
||||
'terminal.osc52.readPrompt.title': 'Clipboard Read Request',
|
||||
'terminal.osc52.readPrompt.desc': 'A remote program is requesting to read your clipboard. Allow?',
|
||||
'terminal.osc52.readPrompt.allow': 'Allow',
|
||||
'terminal.osc52.readPrompt.deny': 'Deny',
|
||||
'settings.terminal.behavior.scrollOnInput': 'Scroll on input',
|
||||
'settings.terminal.behavior.scrollOnInput.desc': 'Scroll terminal to bottom when typing',
|
||||
'settings.terminal.behavior.scrollOnOutput': 'Scroll on output',
|
||||
'settings.terminal.behavior.scrollOnOutput.desc':
|
||||
'Scroll terminal to bottom when new output arrives',
|
||||
'settings.terminal.behavior.scrollOnKeyPress': 'Scroll on key press',
|
||||
'settings.terminal.behavior.scrollOnKeyPress.desc':
|
||||
'Scroll terminal to bottom when pressing a key (e.g., Enter)',
|
||||
'settings.terminal.behavior.scrollOnPaste': 'Scroll on paste',
|
||||
'settings.terminal.behavior.scrollOnPaste.desc':
|
||||
'Scroll terminal to bottom when pasting text',
|
||||
'settings.terminal.behavior.smoothScrolling': 'Smooth scrolling',
|
||||
'settings.terminal.behavior.smoothScrolling.desc':
|
||||
'Animate terminal viewport scrolling instead of jumping instantly',
|
||||
'settings.terminal.behavior.linkModifier': 'Link modifier key',
|
||||
'settings.terminal.behavior.linkModifier.desc': 'Hold this key to click on links in terminal',
|
||||
'settings.terminal.behavior.linkModifier.none': 'None (click directly)',
|
||||
'settings.terminal.behavior.linkModifier.ctrl': 'Ctrl',
|
||||
'settings.terminal.behavior.linkModifier.alt': 'Alt / Option',
|
||||
'settings.terminal.behavior.linkModifier.meta': 'Cmd / Win',
|
||||
'settings.terminal.scrollback.desc': 'Limit number of terminal rows. Set to 0 for no limit.',
|
||||
'settings.terminal.scrollback.rows': 'Number of rows *',
|
||||
'settings.terminal.section.startupCommand': 'Startup command',
|
||||
'settings.terminal.startupCommandDelay.label': 'Startup command delay (ms)',
|
||||
'settings.terminal.startupCommandDelay.desc': 'How long to wait after connecting before sending the startup command. Also used between lines when the startup command has multiple lines. Increase for slow connections.',
|
||||
'settings.terminal.keywordHighlight.title': 'Keyword highlighting',
|
||||
'settings.terminal.keywordHighlight.resetColors': 'Reset to default colors',
|
||||
'settings.terminal.keywordHighlight.resetDefaults': 'Reset built-ins to defaults',
|
||||
'settings.terminal.keywordHighlight.resetBuiltIn': 'Restore default label and patterns',
|
||||
'settings.terminal.keywordHighlight.addCustom': 'Add Custom Rule',
|
||||
'settings.terminal.keywordHighlight.editCustom': 'Edit Rule',
|
||||
'settings.terminal.keywordHighlight.editBuiltIn': 'Edit Built-in Rule',
|
||||
'settings.terminal.keywordHighlight.labelField': 'Label & Color',
|
||||
'settings.terminal.keywordHighlight.labelPlaceholder': 'Label (e.g., Down)',
|
||||
'settings.terminal.keywordHighlight.patternField': 'Regex Patterns',
|
||||
'settings.terminal.keywordHighlight.patternPlaceholder': 'One regex per line (e.g., \\bdown\\b)',
|
||||
'settings.terminal.keywordHighlight.patternHint': 'One regex per line. Patterns are matched case-insensitively with the global flag.',
|
||||
'settings.terminal.keywordHighlight.invalidPattern': 'Invalid regex pattern',
|
||||
'settings.terminal.keywordHighlight.preview': 'Preview',
|
||||
'settings.terminal.section.localShell': 'Local Shell',
|
||||
'settings.terminal.localShell.shell': 'Shell executable',
|
||||
'settings.terminal.localShell.shell.desc': 'Path to the shell executable (e.g., /bin/zsh, pwsh.exe). Leave empty for system default.',
|
||||
'settings.terminal.localShell.shell.placeholder': 'System default',
|
||||
'settings.terminal.localShell.shell.detected': 'Detected',
|
||||
'settings.terminal.localShell.shell.notFound': 'Shell executable not found',
|
||||
'settings.terminal.localShell.shell.isDirectory': 'Path is a directory, not an executable',
|
||||
'settings.terminal.localShell.shell.default': 'System Default',
|
||||
'settings.terminal.localShell.shell.custom': 'Custom...',
|
||||
'settings.terminal.localShell.shell.customPath': 'Shell executable path',
|
||||
'settings.terminal.localShell.shell.customArgs': 'Launch arguments',
|
||||
'settings.terminal.localShell.shell.customArgs.placeholder': 'e.g. --login -i',
|
||||
'settings.terminal.localShell.shell.customArgs.desc': 'Arguments passed to the shell. Some shells need them to work — e.g. msys2 bash requires --login -i to load the environment.',
|
||||
'settings.terminal.localShell.shell.commonPaths': 'Common paths',
|
||||
'settings.terminal.localShell.shell.pathValid': 'Path valid',
|
||||
'settings.terminal.localShell.startDir': 'Starting directory',
|
||||
'settings.terminal.localShell.startDir.desc': 'Directory to start in when opening a local terminal. Leave empty for home directory.',
|
||||
'settings.terminal.localShell.startDir.placeholder': 'Home directory',
|
||||
'settings.terminal.localShell.startDir.notFound': 'Directory not found',
|
||||
'settings.terminal.localShell.startDir.isFile': 'Path is a file, not a directory',
|
||||
'settings.terminal.section.connection': 'Connection',
|
||||
'settings.terminal.connection.verifyHostKeys': 'Verify SSH host keys',
|
||||
'settings.terminal.connection.verifyHostKeys.desc': 'Ask before connecting to a new or changed SSH host key. Turn this off only for trusted private networks.',
|
||||
'settings.terminal.connection.sshAutoReconnectEnabled': 'Automatically reconnect SSH sessions',
|
||||
'settings.terminal.connection.sshAutoReconnectEnabled.desc': 'When turned on, established SSH sessions that drop unexpectedly try to reconnect every 5 seconds until the tab is closed or the connection succeeds.',
|
||||
'settings.terminal.connection.keepaliveInterval': 'Keepalive Interval',
|
||||
'settings.terminal.connection.keepaliveInterval.desc': 'How often (in seconds) to send SSH-level keepalive packets. Set to 0 to disable globally — note that individual hosts can override this in their own settings.',
|
||||
'settings.terminal.connection.keepaliveCountMax': 'Max unanswered keepalives',
|
||||
'settings.terminal.connection.keepaliveCountMax.desc': 'Unanswered keepalives before the connection is declared dead. Higher values are more forgiving of brief network glitches and SSH servers that respond slowly.',
|
||||
'settings.terminal.connection.x11Display': 'X11 display',
|
||||
'settings.terminal.connection.x11Display.desc': 'Optional local display address for X11 forwarding. Leave empty to use the system default.',
|
||||
'settings.terminal.connection.x11Display.placeholder': 'Auto (:0 or DISPLAY)',
|
||||
'settings.terminal.section.serverStats': 'Server Stats (Linux)',
|
||||
'settings.terminal.section.systemManager': 'System Manager',
|
||||
'settings.terminal.systemManager.processRefreshInterval': 'Process list refresh',
|
||||
'settings.terminal.systemManager.processRefreshInterval.desc': 'How often to refresh the process list in the system manager side panel.',
|
||||
'settings.terminal.systemManager.tmuxRefreshInterval': 'tmux session refresh',
|
||||
'settings.terminal.systemManager.tmuxRefreshInterval.desc': 'How often to refresh the tmux session list.',
|
||||
'settings.terminal.systemManager.dockerListRefreshInterval': 'Docker container list refresh',
|
||||
'settings.terminal.systemManager.dockerListRefreshInterval.desc': 'How often to refresh the Docker container list.',
|
||||
'settings.terminal.systemManager.dockerStatsRefreshInterval': 'Docker stats refresh',
|
||||
'settings.terminal.systemManager.dockerStatsRefreshInterval.desc': 'How often to refresh Docker container CPU/memory/network stats.',
|
||||
'settings.terminal.serverStats.show': 'Show Server Stats',
|
||||
'settings.terminal.serverStats.show.desc': 'Display CPU, memory, and disk usage in the terminal statusbar (Linux servers only).',
|
||||
'settings.terminal.serverStats.refreshInterval': 'Refresh Interval',
|
||||
'settings.terminal.serverStats.refreshInterval.desc': 'How often to refresh server stats.',
|
||||
'settings.terminal.serverStats.seconds': 'seconds',
|
||||
|
||||
// Settings > Terminal > Rendering
|
||||
'settings.terminal.section.rendering': 'Rendering',
|
||||
'settings.terminal.rendering.renderer': 'Renderer',
|
||||
'settings.terminal.rendering.renderer.desc': 'Choose the terminal rendering technology. Auto will use DOM on low-memory devices. Changes take effect on new terminal sessions.',
|
||||
'settings.terminal.rendering.auto': 'Auto',
|
||||
'settings.terminal.rendering.hibernateHiddenTabs': 'Hibernate hidden tabs',
|
||||
'settings.terminal.rendering.hibernateHiddenTabs.desc': 'Dispose the terminal renderer for off-screen tabs to save memory while keeping the SSH session connected. Skipped during file transfers.',
|
||||
'settings.terminal.rendering.hibernateHiddenTabsDelay': 'Hibernate delay',
|
||||
'settings.terminal.rendering.hibernateHiddenTabsDelay.desc': 'How long a tab must stay off-screen before its renderer is released (5–600 seconds).',
|
||||
'settings.terminal.rendering.hibernateSkipAltScreen': 'Skip hibernate on full-screen apps',
|
||||
'settings.terminal.rendering.hibernateSkipAltScreen.desc': 'Keep the terminal renderer alive while vim, htop, or agent TUIs own the alternate screen buffer.',
|
||||
'settings.terminal.rendering.hibernateKeepRendererCount': 'Keep hidden renderers',
|
||||
'settings.terminal.rendering.hibernateKeepRendererCount.desc': 'How many off-screen tabs keep their renderer alive (WebGL suspended) before full hibernate.',
|
||||
'settings.terminal.rendering.hibernateReplayChunkBytes': 'Wake replay chunk size',
|
||||
'settings.terminal.rendering.hibernateReplayChunkBytes.desc': 'Bytes replayed per animation frame when restoring a hibernated terminal (4–64 KB).',
|
||||
'settings.terminal.rendering.hibernatePreferWasmSerialize': 'Prefer WASM serialize',
|
||||
'settings.terminal.rendering.hibernatePreferWasmSerialize.desc': 'Use WASM terminal serialization when available; falls back to the JS serializer.',
|
||||
|
||||
// Settings > Terminal > Workspace Focus Indicator
|
||||
'settings.terminal.section.workspaceFocus': 'Workspace Focus Indicator',
|
||||
'settings.terminal.workspaceFocus.style': 'Focus indicator style',
|
||||
'settings.terminal.workspaceFocus.style.desc': 'How to indicate which pane is focused in split view.',
|
||||
'settings.terminal.workspaceFocus.dim': 'Dim unfocused panes',
|
||||
'settings.terminal.workspaceFocus.border': 'Border on focused pane',
|
||||
|
||||
// Settings > Terminal > Autocomplete
|
||||
'settings.terminal.section.autocomplete': 'Autocomplete',
|
||||
'settings.terminal.autocomplete.enabled': 'Enable autocomplete',
|
||||
'settings.terminal.autocomplete.enabled.desc': 'Show command suggestions based on history and command specs as you type.',
|
||||
'settings.terminal.autocomplete.ghostText': 'Ghost text',
|
||||
'settings.terminal.autocomplete.ghostText.desc': 'Show inline gray suggestion text after the cursor (like fish shell).',
|
||||
'settings.terminal.autocomplete.popupMenu': 'Popup menu',
|
||||
'settings.terminal.autocomplete.popupMenu.desc': 'Show a floating list of multiple suggestions.',
|
||||
|
||||
// Settings > Shortcuts
|
||||
'settings.shortcuts.section.scheme': 'Hotkey Scheme',
|
||||
'settings.shortcuts.scheme.label': 'Keyboard shortcuts',
|
||||
'settings.shortcuts.scheme.desc': 'Choose which keyboard layout to use for shortcuts',
|
||||
'settings.shortcuts.scheme.disabled': 'Disabled',
|
||||
'settings.shortcuts.scheme.mac': 'Mac (Cmd)',
|
||||
'settings.shortcuts.scheme.pc': 'PC (Ctrl)',
|
||||
'settings.shortcuts.disableTerminalFontZoom.label': 'Disable terminal zoom',
|
||||
'settings.shortcuts.disableTerminalFontZoom.desc': 'Turn off terminal font zoom shortcuts, including Cmd/Ctrl + mouse wheel.',
|
||||
'settings.shortcuts.shellOnlyTabNumberShortcuts.label': 'Tab shortcuts skip pinned tabs',
|
||||
'settings.shortcuts.shellOnlyTabNumberShortcuts.desc': 'When enabled, Cmd/Ctrl+[1...9] and Ctrl+Tab switch only work tabs (terminals, workspaces, editors), not the pinned Vault or SFTP tabs.',
|
||||
'settings.shortcuts.section.custom': 'Custom Shortcuts',
|
||||
'settings.shortcuts.resetAll': 'Reset All',
|
||||
'settings.shortcuts.recording': 'Press keys...',
|
||||
'settings.shortcuts.none': 'None',
|
||||
'settings.shortcuts.setDisabled': 'Set to disabled',
|
||||
'settings.shortcuts.category.tabs': 'Tabs',
|
||||
'settings.shortcuts.category.terminal': 'Terminal',
|
||||
'settings.shortcuts.category.navigation': 'Navigation',
|
||||
'settings.shortcuts.category.app': 'App',
|
||||
'settings.shortcuts.category.sftp': 'SFTP',
|
||||
|
||||
// Context menus / common actions
|
||||
'action.newHost': 'New Host',
|
||||
'action.newSubfolder': 'New Subfolder',
|
||||
'action.copyPublicKey': 'Copy Public Key',
|
||||
'action.keyExport': 'Key Export',
|
||||
'action.edit': 'Edit',
|
||||
'action.delete': 'Delete',
|
||||
'action.duplicate': 'Duplicate',
|
||||
'action.open': 'Open',
|
||||
'action.copy': 'Copy',
|
||||
'action.run': 'Run',
|
||||
'action.start': 'Start',
|
||||
'action.stop': 'Stop',
|
||||
'action.remove': 'Remove',
|
||||
'action.convertToHost': 'Convert to Host',
|
||||
|
||||
// Sync
|
||||
'sync.cloudSync': 'Cloud Sync',
|
||||
'sync.settings': 'Sync Settings',
|
||||
'sync.active': 'Cloud Sync Active',
|
||||
'sync.pending': 'Changes pending sync',
|
||||
'sync.syncing': 'Syncing...',
|
||||
'sync.error': 'Sync Error',
|
||||
'sync.notConfigured': 'Not Configured',
|
||||
'sync.failed': 'Sync failed',
|
||||
'sync.connected': 'Connected',
|
||||
'sync.syncNow': 'Sync Now',
|
||||
'sync.recentActivity': 'Recent activity',
|
||||
'sync.history.uploaded': 'Uploaded',
|
||||
'sync.history.downloaded': 'Downloaded',
|
||||
'sync.history.resolved': 'Resolved',
|
||||
'sync.toast.completedMessage': 'Sync completed successfully',
|
||||
'sync.toast.errorTitle': 'Sync Error',
|
||||
'sync.autoSync.failedTitle': 'Sync failed',
|
||||
'sync.autoSync.inspectFailedTitle': 'Sync paused',
|
||||
'sync.autoSync.inspectFailedMessage': 'Could not reach the cloud to check for changes. Auto-sync will retry when data changes or the app is restarted.',
|
||||
'sync.autoSync.syncedTitle': 'Synced from cloud',
|
||||
'sync.autoSync.syncedMessage': 'Your data has been updated from the cloud.',
|
||||
'sync.autoSync.noProvider': 'No cloud provider connected. Open Settings → Sync & Cloud to connect one.',
|
||||
'sync.autoSync.alreadySyncing': 'Sync is already in progress.',
|
||||
'sync.autoSync.restoreInProgress': 'A vault restore is in progress in another window. Please wait for it to finish.',
|
||||
'sync.autoSync.interruptedApplyTitle': 'Sync paused — previous restore interrupted',
|
||||
'sync.autoSync.interruptedApplyMessage': 'A previous restore did not finish cleanly, so the local vault may be inconsistent. Open Settings → Sync & Cloud → Restore and apply a protective backup before auto-sync resumes.',
|
||||
'sync.autoSync.vaultLocked': 'Vault is locked. Open Settings → Sync & Cloud to unlock.',
|
||||
'sync.autoSync.conflictDetected': 'Sync conflict detected. Open Settings → Sync & Cloud to resolve.',
|
||||
'sync.autoSync.syncFailed': 'Sync failed',
|
||||
'sync.autoSync.restoredTitle': 'Vault restored',
|
||||
'sync.autoSync.restoredMessage': 'Your vault has been restored from the cloud.',
|
||||
'sync.autoSync.keptLocalTitle': 'Kept local vault',
|
||||
'sync.autoSync.keptLocalMessage': 'Your empty local vault was kept. Cloud data was not applied.',
|
||||
'sync.autoSync.emptyVaultConflict.title': 'Empty Vault Detected',
|
||||
'sync.autoSync.emptyVaultConflict.description': 'Your local vault is empty, but the cloud has data. This usually happens after an update or storage reset. What would you like to do?',
|
||||
'sync.autoSync.emptyVaultConflict.cloudLabel': 'Cloud',
|
||||
'sync.autoSync.emptyVaultConflict.restore': 'Restore from Cloud',
|
||||
'sync.autoSync.emptyVaultConflict.restoreDesc': 'Recommended — recover your hosts, keys, and snippets from the cloud backup',
|
||||
'sync.autoSync.emptyVaultConflict.keepEmpty': 'Keep Empty',
|
||||
'sync.autoSync.emptyVaultConflict.keepEmptyDesc': 'Start fresh with an empty vault',
|
||||
'sync.autoSync.emptyVaultConflict.cloudSummary': '{hosts} hosts, {keys} keys, {snippets} snippets, {notes} notes, {proxyProfiles} proxies',
|
||||
'sync.autoSync.emptyVaultManual': 'Cannot sync: the local vault is empty. Restore from a local backup or enable Force Push in the sync panel first.',
|
||||
|
||||
'sync.blocked.title': 'Sync paused',
|
||||
'sync.blocked.reason.bulkShrink': 'Would delete {lost} of {baseCount} {entityType} from cloud ({percent}% reduction).',
|
||||
'sync.blocked.reason.largeShrink': 'Would delete {lost} {entityType} from cloud.',
|
||||
'sync.blocked.detail': 'This is usually caused by a degraded local state (keychain failure, partial data load). Restore from a local backup, or force-push if you truly meant to remove these entries.',
|
||||
'sync.blocked.restoreButton': 'Restore from local backup',
|
||||
'sync.blocked.forcePushButton': 'Force push anyway',
|
||||
|
||||
'sync.forcePush.title': 'Confirm force push',
|
||||
'sync.forcePush.body': 'You are about to remove {lost} {entityType} from the cloud. This cannot be undone. Proceed?',
|
||||
'sync.forcePush.confirm': 'Yes, push anyway',
|
||||
'sync.forcePush.cancel': 'Cancel',
|
||||
|
||||
'sync.entityType.hosts': 'hosts',
|
||||
'sync.entityType.keys': 'keys',
|
||||
'sync.entityType.identities': 'identities',
|
||||
'sync.entityType.proxyProfiles': 'proxy profiles',
|
||||
'sync.entityType.snippets': 'snippets',
|
||||
'sync.entityType.customGroups': 'groups',
|
||||
'sync.entityType.snippetPackages': 'snippet packages',
|
||||
'sync.entityType.knownHosts': 'known-host entries',
|
||||
'sync.entityType.portForwardingRules': 'port-forwarding rules',
|
||||
'sync.entityType.groupConfigs': 'group configs',
|
||||
|
||||
'sync.credentialsUnavailable': 'This device cannot decrypt some saved credentials. Re-enter credentials locally before syncing.',
|
||||
'time.never': 'Never',
|
||||
'time.justNow': 'Just now',
|
||||
'time.minutesAgo': '{minutes}m ago',
|
||||
|
||||
// Vault navigation
|
||||
'vault.nav.hosts': 'Hosts',
|
||||
'vault.nav.keychain': 'Keychain',
|
||||
'vault.nav.proxies': 'Proxies',
|
||||
'vault.nav.portForwarding': 'Port Forwarding',
|
||||
'vault.nav.snippets': 'Scripts',
|
||||
'vault.nav.notes': 'Notes',
|
||||
'vault.nav.knownHosts': 'Known Hosts',
|
||||
'vault.nav.logs': 'Logs',
|
||||
|
||||
'proxyProfiles.action.add': 'Add Proxy',
|
||||
'proxyProfiles.search.placeholder': 'Search proxies…',
|
||||
'proxyProfiles.section.proxies': 'Proxies',
|
||||
'proxyProfiles.count.items': '{count} items',
|
||||
'proxyProfiles.empty.title': 'No Proxies',
|
||||
'proxyProfiles.empty.desc': 'Create reusable HTTP, SOCKS5, or command proxies and select them from host details.',
|
||||
'proxyProfiles.usage': '{count} linked',
|
||||
'proxyProfiles.copyName': '{name} Copy',
|
||||
'proxyProfiles.panel.newTitle': 'New Proxy',
|
||||
'proxyProfiles.field.name': 'Proxy name',
|
||||
'proxyProfiles.error.required': 'Name and proxy details are required.',
|
||||
'proxyProfiles.error.port': 'Port must be between 1 and 65535.',
|
||||
'proxyProfiles.viewMode': 'Proxy view mode',
|
||||
'proxyProfiles.delete.title': 'Delete proxy?',
|
||||
'proxyProfiles.delete.desc': 'Deleting "{name}" will unlink it from {count} host or group settings.',
|
||||
|
||||
'vault.groups.title': 'Groups',
|
||||
'vault.groups.total': '{count} total',
|
||||
'vault.groups.hostsCount': '{count} Hosts',
|
||||
'vault.groups.newSubgroup': 'New Subgroup',
|
||||
'vault.groups.rename': 'Rename Group',
|
||||
'vault.groups.unnamed': 'Unnamed Group',
|
||||
'vault.groups.delete': 'Delete Group',
|
||||
'vault.groups.createSubfolder': 'Create Subfolder',
|
||||
'vault.groups.createRoot': 'Create Root Group',
|
||||
'vault.groups.createDialog.desc': 'Create a new group for organizing hosts.',
|
||||
'vault.groups.renameDialogTitle': 'Rename Group',
|
||||
'vault.groups.renameDialog.desc': 'Rename an existing group.',
|
||||
'vault.groups.deleteDialogTitle': 'Delete Group',
|
||||
'vault.groups.deleteDialog.desc': 'This will permanently delete the group and move all hosts to the root level.',
|
||||
'vault.groups.deleteDialog.managedDesc': 'This is a managed SSH config group. Deleting it will also delete all hosts and unlink from the source file.',
|
||||
'vault.groups.deleteDialog.deleteHosts': 'Also delete all hosts in this group',
|
||||
'vault.groups.ungrouped': 'Ungrouped',
|
||||
'vault.groups.field.name': 'Group Name',
|
||||
'vault.groups.placeholder.example': 'e.g. Production',
|
||||
'vault.groups.parentLabel': 'Parent',
|
||||
'vault.groups.pathLabel': 'Path',
|
||||
'vault.groups.settings': 'Group Settings',
|
||||
'vault.groups.details': 'Group Details',
|
||||
'vault.groups.details.general': 'General',
|
||||
'vault.groups.details.ssh': 'SSH',
|
||||
'vault.groups.details.telnet': 'Telnet',
|
||||
'vault.groups.details.advanced': 'Advanced',
|
||||
'vault.groups.details.appearance': 'Appearance',
|
||||
'vault.groups.details.mosh': 'Mosh',
|
||||
'vault.groups.details.parentGroup': 'Parent Group',
|
||||
'vault.groups.details.none': 'None',
|
||||
'vault.groups.details.inherited': 'Inherited from group',
|
||||
'vault.groups.details.addProtocol': 'Add Protocol',
|
||||
'vault.groups.details.removeProtocol': 'Remove Protocol',
|
||||
'vault.groups.details.fontFamily': 'Font Family',
|
||||
'vault.groups.details.fontSize': 'Font Size',
|
||||
'vault.groups.errors.required': 'Group name is required.',
|
||||
'vault.groups.errors.invalidChars': "Group name cannot include '/' or '\\\\'.",
|
||||
'vault.groups.errors.duplicatePath': 'A group with this name already exists at this location.',
|
||||
|
||||
'vault.deleteConfirm.title': 'Delete "{name}"?',
|
||||
'vault.deleteConfirm.desc': 'This action cannot be undone.',
|
||||
'vault.deleteConfirm.packageDesc': 'This will remove the script package. Scripts inside it will stay saved and move out of the package.',
|
||||
'vault.deleteConfirm.noteGroupDesc': 'This will remove the folder. Notes inside it will stay saved and move out of the folder.',
|
||||
'vault.deleteConfirm.portForwardingDesc': 'This will delete the port forwarding rule.',
|
||||
|
||||
'vault.managedSource.unmanage': 'Unmanage',
|
||||
'vault.managedSource.unmanageSuccess': 'Successfully unmanaged group',
|
||||
|
||||
'vault.hosts.header.entries': '{count} entries',
|
||||
'vault.hosts.header.live': '{count} live',
|
||||
|
||||
// Vault hosts header/actions
|
||||
'vault.hosts.search.placeholder': 'Find a host or ssh user@hostname / ssh -p 2222 user@hostname...',
|
||||
'vault.hosts.connect': 'Connect',
|
||||
'vault.view.grid': 'Grid',
|
||||
'vault.view.list': 'List',
|
||||
'vault.view.tree': 'Tree',
|
||||
'vault.tree.expandAll': 'Expand All',
|
||||
'vault.tree.collapseAll': 'Collapse All',
|
||||
'vault.hosts.newHost': 'New Host',
|
||||
'vault.hosts.newGroup': 'New Group',
|
||||
'vault.hosts.import': 'Import',
|
||||
'vault.hosts.export': 'Export',
|
||||
'vault.hosts.export.toast.success': 'Exported {count} hosts to CSV',
|
||||
'vault.hosts.export.toast.successWithSkipped': 'Exported {count} hosts to CSV ({skipped} unsupported hosts skipped)',
|
||||
'vault.hosts.export.toast.noHosts': 'No hosts to export',
|
||||
'vault.hosts.allHosts': 'All hosts',
|
||||
'vault.hosts.pinned': 'Pinned',
|
||||
'vault.hosts.recentlyConnected': 'Recently Connected',
|
||||
'vault.hosts.pinToTop': 'Pin to Top',
|
||||
'vault.hosts.unpin': 'Unpin',
|
||||
'vault.hosts.copyCredentials': 'Copy Credentials',
|
||||
'vault.hosts.copyCredentials.toast.success': 'Credentials copied to clipboard',
|
||||
'vault.hosts.copyCredentials.toast.noPassword': 'No password saved for this host',
|
||||
'vault.hosts.multiSelect': 'Multi-select',
|
||||
'vault.hosts.selected': '{count} selected',
|
||||
'vault.hosts.selectAll': 'Select All',
|
||||
'vault.hosts.deselectAll': 'Deselect All',
|
||||
'vault.hosts.deleteSelected': 'Delete ({count})',
|
||||
'vault.hosts.deleteMultiple.success': 'Deleted {count} hosts',
|
||||
'vault.hosts.connectSelected': 'Connect ({count})',
|
||||
'vault.hosts.connectMultiple.success': 'Connecting {count} hosts',
|
||||
'vault.hosts.moveToGroup.success': 'Moved {host} to {group}',
|
||||
'vault.hosts.errors.nameRequired': 'Host name is required.',
|
||||
'vault.hosts.empty.title': 'Set up your hosts',
|
||||
'vault.hosts.empty.desc': 'Save hosts to quickly connect to your servers, VMs, and containers.',
|
||||
'deepLink.ssh.invalid': 'Invalid ssh:// link',
|
||||
'deepLink.telnet.invalid': 'Invalid telnet:// link',
|
||||
'deepLink.jms.invalid': 'Invalid JumpServer link',
|
||||
'deepLink.jms.unsupported': 'Unsupported JumpServer protocol: {{protocol}}',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
export const enScriptsMessages = {
|
||||
'scripts.meta.name': 'Name',
|
||||
'scripts.meta.language': 'Language',
|
||||
'scripts.meta.description': 'Description',
|
||||
'scripts.meta.descriptionPlaceholder': 'Optional notes about this script',
|
||||
'scripts.meta.trigger': 'Trigger',
|
||||
'scripts.meta.triggerPattern': 'Output pattern (regex)',
|
||||
'scripts.meta.code': 'Script',
|
||||
'scripts.trigger.manual': 'Manual run',
|
||||
'scripts.trigger.onConnect': 'Run on connect',
|
||||
'scripts.trigger.onOutput': 'Run on output match',
|
||||
'scripts.trigger.onOutputHint': 'Fires when server output matches the pattern (user keystroke echo is ignored). With no target hosts configured, listens on the current connected session; with targets, only those hosts. Disabled in alternate-screen apps such as vim or htop. Suppressed while another script is running on this session; rechecked after it finishes. For in-flow waits, use nct.screen.waitForText or nct.screen.waitForRegex in script code.',
|
||||
'scripts.actions.save': 'Save',
|
||||
'scripts.actions.runNow': 'Run now',
|
||||
'scripts.actions.openEditor': 'Open editor',
|
||||
'scripts.actions.openEditorHint': 'Edit script in a larger window',
|
||||
'scripts.editor.modalTitle': 'Script editor',
|
||||
'scripts.editor.modalSubtitle': 'Edit metadata and script code in a larger workspace.',
|
||||
'scripts.editor.lineCount': '{count} lines',
|
||||
'scripts.editor.resize': 'Resize editor',
|
||||
'scripts.targets.hint': 'Automatic triggers apply to selected hosts, or to all hosts when that option is enabled.',
|
||||
'scripts.targets.connectOrderHint': 'Run order for connect scripts is configured per host under Host details → Automation.',
|
||||
'scripts.actions.runNowHint': 'Run on selected targets, or on all connectable hosts when that option is enabled.',
|
||||
'scripts.actions.runParallel': 'Run on all tabs (parallel)',
|
||||
'scripts.actions.runSequential': 'Run on all tabs (sequential)',
|
||||
'scripts.actions.skippedConnectingSessions': '{count} tab(s) still connecting and were skipped',
|
||||
'scripts.actions.noRunnableHosts': 'No connectable hosts match this script\'s targets',
|
||||
'scripts.sidePanel.library': 'Library',
|
||||
'scripts.sidePanel.running': 'Running',
|
||||
'scripts.sidePanel.newScript': 'New script',
|
||||
'scripts.running.empty': 'No scripts are running in this session.',
|
||||
'scripts.running.unnamed': 'Untitled script',
|
||||
'scripts.running.status.running': 'Running',
|
||||
'scripts.running.status.paused': 'Paused',
|
||||
'scripts.running.status.completed': 'Completed',
|
||||
'scripts.running.status.failed': 'Failed',
|
||||
'scripts.running.waitingFor': 'Waiting for {pattern}',
|
||||
'scripts.running.waitingForLabel': 'Waiting for',
|
||||
'scripts.running.waitingForShellPrompt': 'shell prompt (# or $)',
|
||||
'scripts.running.lastSent': 'Sent: {command}',
|
||||
'scripts.recording.start': 'Start recording',
|
||||
'scripts.recording.active': 'Stop recording',
|
||||
'scripts.recording.startHint': 'Record actions in the focused terminal and generate nct script code',
|
||||
'scripts.recording.unavailableHint': 'Recording is not available here — use the scripts sidebar on the right',
|
||||
'scripts.recording.activeHint': 'Recording this terminal. Type commands as usual; click Stop or use the REC control in the toolbar to finish and save.',
|
||||
'scripts.recording.started': 'Recording started — operate in the terminal',
|
||||
'scripts.recording.noSession': 'Connect a terminal first (single tab or workspace)',
|
||||
'scripts.recording.alreadyActive': 'Another terminal is already recording',
|
||||
'scripts.recording.stop': 'Stop recording',
|
||||
'scripts.recording.pause': 'Pause recording',
|
||||
'scripts.recording.resume': 'Resume recording',
|
||||
'scripts.recording.saveTitle': 'Save recorded script',
|
||||
'scripts.recording.namePlaceholder': 'Script name',
|
||||
'scripts.recording.packagePlaceholder': 'Save to folder',
|
||||
'scripts.recording.rootPackage': 'Root',
|
||||
'scripts.recording.save': 'Save',
|
||||
'scripts.recording.saveAndEdit': 'Save and edit',
|
||||
'scripts.recording.helpTitle': 'How to record a script',
|
||||
'scripts.recording.helpIntro': 'Recording turns what you do in the terminal into a reusable automation script — useful for deploys, health checks, and other repetitive tasks.',
|
||||
'scripts.recording.helpStep1': 'Connect to a host. A standalone terminal tab or a terminal inside a workspace both work — open the scripts sidebar on the right.',
|
||||
'scripts.recording.helpStep2': 'Click Start recording. A red REC badge in the terminal toolbar means recording is active.',
|
||||
'scripts.recording.helpStep3': 'Type commands in the terminal as you normally would. Each Enter press is captured as one step.',
|
||||
'scripts.recording.helpStep4': 'When you are done, click Stop recording, or use the stop control next to REC in the terminal toolbar.',
|
||||
'scripts.recording.helpStep5': 'Name the script and save. Choose Save and edit if you want to fine-tune the generated code in the script editor.',
|
||||
'scripts.recording.helpTipsTitle': 'Tips',
|
||||
'scripts.recording.helpTip1': 'Pauses longer than 1 second between actions are recorded as wait time so playback is not too fast.',
|
||||
'scripts.recording.helpTip2': 'After each command, recording waits for a shell prompt (such as $ or #) before the next step.',
|
||||
'scripts.recording.helpTip3': 'Password input is marked sensitive and is not stored in plain text in the script.',
|
||||
'scripts.recording.helpTip4': 'For vim, menus, or other interactive flows, record the main commands first, then refine the script manually.',
|
||||
'scripts.dialog.title': 'Script',
|
||||
'scripts.dialog.ok': 'OK',
|
||||
'scripts.dialog.required': 'Required',
|
||||
'scripts.dialog.numberInvalid': 'Enter a valid number',
|
||||
'scripts.dialog.numberMin': 'Must be at least {min}',
|
||||
'scripts.dialog.numberMax': 'Must be at most {max}',
|
||||
'scripts.dialog.numberStep': 'Must use increments of {step}',
|
||||
'scripts.dialog.waitForTimeoutTitle': 'Wait timed out',
|
||||
'scripts.dialog.retry': 'Retry',
|
||||
'scripts.dialog.skip': 'Skip',
|
||||
'scripts.dialog.abort': 'Abort',
|
||||
'scripts.running.stepProgress': 'Step {current} / {total}',
|
||||
'scripts.running.determinateProgress': '{label} {current}/{total}',
|
||||
'scripts.running.progressFallback': 'Progress',
|
||||
'scripts.running.operationsCount': '{count} operations',
|
||||
'scripts.running.opsPrefix': '',
|
||||
'scripts.running.opsSuffix': ' operations',
|
||||
'scripts.running.elapsedLabel': 'Elapsed',
|
||||
'scripts.running.lastSentLabel': 'Sent:',
|
||||
'scripts.running.elapsed': '{elapsed}',
|
||||
'scripts.running.completedSummary': 'Completed · {count} operations · {elapsed}',
|
||||
'scripts.running.dismissHint': 'Tap close to dismiss',
|
||||
'scripts.running.dismiss': 'Close',
|
||||
'scripts.running.viewLogs': 'View logs',
|
||||
'scripts.running.logTitle': '{name} · Run log',
|
||||
'scripts.running.logEmpty': 'No log output yet',
|
||||
'scripts.running.pause': 'Pause',
|
||||
'scripts.running.resume': 'Resume',
|
||||
'scripts.running.stop': 'Stop',
|
||||
'scripts.recording.saved': 'Script saved',
|
||||
'scripts.recording.savedNamed': 'Saved "{name}"',
|
||||
'scripts.observer.blocked': 'Observer mode blocks scripts that write to the terminal.',
|
||||
'vault.section.scripts': 'Scripts',
|
||||
'vault.nav.scripts': 'Scripts',
|
||||
'snippets.action.newScript': 'New automation script',
|
||||
'hostDetails.section.automation': 'Automation',
|
||||
'hostDetails.automation.loginScript': 'Login script',
|
||||
'hostDetails.automation.loginScriptPlaceholder': 'Select a script',
|
||||
'hostDetails.automation.none': 'None',
|
||||
'hostDetails.automation.outputTriggers': 'Output triggers',
|
||||
'hostDetails.automation.addTrigger': 'Add trigger',
|
||||
'hostDetails.automation.triggerPatternPlaceholder': 'Regex pattern',
|
||||
'hostDetails.automation.linkedScripts': 'Linked scripts',
|
||||
'hostDetails.automation.linkedScriptsEmpty': 'No scripts linked to this host yet.',
|
||||
'hostDetails.automation.linkScriptPlaceholder': 'Link existing script…',
|
||||
'hostDetails.automation.unlink': 'Unlink',
|
||||
'hostDetails.automation.equivalenceHint': 'Links add this host to the script\'s target list in Scripts. Both views stay in sync.',
|
||||
'hostDetails.automation.queueHint': 'Scripts run in order when this host connects. Global scripts run first, then this queue.',
|
||||
'hostDetails.automation.globalScripts': 'Global connect scripts',
|
||||
'hostDetails.automation.globalScriptsHint': 'Runs first on every connect. Reorder in Scripts library sort order.',
|
||||
'hostDetails.automation.connectQueue': 'This host\'s run queue',
|
||||
'hostDetails.automation.connectQueueEmpty': 'No connect scripts queued for this host yet.',
|
||||
'hostDetails.automation.addToQueuePlaceholder': 'Add script to queue…',
|
||||
'hostDetails.automation.moveUp': 'Move up',
|
||||
'hostDetails.automation.moveDown': 'Move down',
|
||||
'hostDetails.automation.removeFromQueue': 'Remove from queue',
|
||||
'hostDetails.automation.dragHandle': 'Drag to reorder',
|
||||
'hostDetails.automation.queueDragHint': 'Drag items to reorder the connect queue.',
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const enSystemManagerMessages: Messages = {
|
||||
'terminal.layer.system': 'System',
|
||||
|
||||
'systemManager.noSession': 'No active terminal session.',
|
||||
'systemManager.notConnected': 'Connect to a host to manage processes and services.',
|
||||
'systemManager.empty': 'No data available.',
|
||||
'systemManager.tabs.overview': 'Overview',
|
||||
'systemManager.tabs.processes': 'Processes',
|
||||
'systemManager.tabs.tmux': 'tmux',
|
||||
'systemManager.tabs.docker': 'Docker',
|
||||
'systemManager.popup.loading': 'Opening terminal…',
|
||||
'systemManager.popup.startupFailed': 'The startup command did not complete successfully. Check that the target is still available and try again.',
|
||||
|
||||
'systemManager.errors.loadProcesses': 'Failed to load processes',
|
||||
'systemManager.errors.loadTmux': 'Failed to load tmux sessions',
|
||||
'systemManager.errors.loadTmuxWindows': 'Failed to load tmux windows',
|
||||
'systemManager.errors.loadTmuxPanes': 'Failed to load tmux panes',
|
||||
'systemManager.errors.loadTmuxClients': 'Failed to load tmux clients',
|
||||
'systemManager.errors.actionFailed': 'Action failed',
|
||||
'systemManager.errors.loadDocker': 'Failed to load containers',
|
||||
'systemManager.errors.loadDockerStats': 'Failed to load container stats',
|
||||
'systemManager.errors.loadDockerImages': 'Failed to load images',
|
||||
'systemManager.errors.loadOverview': 'Failed to load system overview',
|
||||
'systemManager.errors.sshChannelUnavailable': 'The server refused to open a new execution channel. Try again later, or reconnect this host.',
|
||||
|
||||
'systemManager.overview.empty': 'No system overview data yet.',
|
||||
'systemManager.overview.loading': 'Loading system overview…',
|
||||
'systemManager.overview.memory': 'Memory',
|
||||
'systemManager.overview.disk': 'Disk',
|
||||
'systemManager.overview.network': 'Network',
|
||||
'systemManager.overview.rx': 'RX',
|
||||
'systemManager.overview.tx': 'TX',
|
||||
'systemManager.overview.cores': '{{count}} cores',
|
||||
'systemManager.overview.load': 'Load',
|
||||
'systemManager.overview.uptime': 'Uptime',
|
||||
'systemManager.overview.duration.daysHours': '{{days}}d {{hours}}h',
|
||||
'systemManager.overview.duration.hoursMinutes': '{{hours}}h {{minutes}}m',
|
||||
'systemManager.overview.duration.minutes': '{{minutes}}m',
|
||||
'systemManager.overview.system': 'System',
|
||||
'systemManager.overview.kernel': 'Kernel',
|
||||
'systemManager.overview.swap': 'Swap',
|
||||
'systemManager.overview.latency': 'Latency',
|
||||
'systemManager.overview.cpuCores': 'CPU cores',
|
||||
'systemManager.overview.disks': 'Disks',
|
||||
'systemManager.overview.interfaces': 'Network interfaces',
|
||||
'systemManager.overview.topProcesses': 'Top memory processes',
|
||||
'systemManager.overview.noData': 'No data',
|
||||
'systemManager.overview.noDisks': 'No disk data',
|
||||
'systemManager.overview.noInterfaces': 'No interface data',
|
||||
'systemManager.overview.noTopProcesses': 'No process data',
|
||||
|
||||
'systemManager.processes.search': 'Search processes…',
|
||||
'systemManager.processes.command': 'Command',
|
||||
'systemManager.processes.user': 'User',
|
||||
'systemManager.processes.term': 'Terminate',
|
||||
'systemManager.processes.kill': 'Kill',
|
||||
'systemManager.processes.stop': 'Stop (SIGSTOP)',
|
||||
'systemManager.processes.cont': 'Continue (SIGCONT)',
|
||||
'systemManager.processes.hup': 'Hang up (SIGHUP)',
|
||||
'systemManager.processes.renice': 'Renice',
|
||||
'systemManager.processes.renicePrompt': 'Nice value (-20 to 19)',
|
||||
'systemManager.processes.reniceInvalid': 'Nice value must be between -20 and 19',
|
||||
'systemManager.processes.confirmKill': 'Send SIGKILL to process {{pid}}?',
|
||||
'systemManager.processes.confirmSignal': 'Send SIG{{signal}} to process {{pid}}?',
|
||||
'systemManager.processes.filter.all': 'All',
|
||||
'systemManager.processes.filter.running': 'Running',
|
||||
'systemManager.processes.ppid': 'Parent PID',
|
||||
'systemManager.processes.rss': 'RSS',
|
||||
'systemManager.processes.vsz': 'Virtual size',
|
||||
'systemManager.processes.elapsed': 'Elapsed',
|
||||
'systemManager.processes.stat': 'State',
|
||||
'systemManager.processes.meta': '{{count}} process(es)',
|
||||
'systemManager.processes.loading': 'Loading processes…',
|
||||
'systemManager.processes.loadingMore': 'Loading more processes…',
|
||||
'systemManager.processes.state.running': 'Running',
|
||||
'systemManager.processes.state.sleeping': 'Sleeping',
|
||||
'systemManager.processes.state.stopped': 'Stopped',
|
||||
'systemManager.processes.state.zombie': 'Zombie',
|
||||
'systemManager.processes.sort.cpu': 'CPU',
|
||||
'systemManager.processes.sort.mem': 'MEM',
|
||||
'systemManager.processes.sort.pid': 'PID',
|
||||
'systemManager.processes.sort.command': 'Command',
|
||||
'systemManager.processes.sort.user': 'User',
|
||||
|
||||
'systemManager.common.dismiss': 'Dismiss',
|
||||
'systemManager.common.checkingAvailability': 'Checking availability…',
|
||||
'systemManager.common.loading': 'Loading…',
|
||||
'systemManager.common.loadingDetails': 'Loading details…',
|
||||
'systemManager.common.loadingStats': 'Loading stats…',
|
||||
|
||||
'systemManager.tmux.new': 'New',
|
||||
'systemManager.tmux.search': 'Search sessions…',
|
||||
'systemManager.tmux.newSessionTitle': 'New tmux session',
|
||||
'systemManager.tmux.newSessionDesc': 'Name the session and optionally run a script on start.',
|
||||
'systemManager.tmux.newSessionTabCustom': 'Custom command',
|
||||
'systemManager.tmux.newSessionTabSnippet': 'From snippet',
|
||||
'systemManager.tmux.pickSnippet': 'From snippets',
|
||||
'systemManager.tmux.pickSnippetEmpty': 'No snippets yet — add some in the Scripts panel or Vault.',
|
||||
'systemManager.tmux.selectedSnippet': 'Using snippet: {{label}}',
|
||||
'systemManager.tmux.newSessionName': 'Session name',
|
||||
'systemManager.tmux.newSessionCommand': 'Start command',
|
||||
'systemManager.tmux.newSessionCommandPlaceholder': 'e.g. htop or npm run dev (optional)',
|
||||
'systemManager.tmux.newSessionCommandHint': 'Leave empty for a default shell session.',
|
||||
'systemManager.tmux.creating': 'Creating…',
|
||||
'systemManager.tmux.newSessionPlaceholder': 'my-session',
|
||||
'systemManager.tmux.newSessionRequired': 'Enter a session name first',
|
||||
'systemManager.tmux.empty': 'No tmux sessions',
|
||||
'systemManager.tmux.attach': 'Attach',
|
||||
'systemManager.tmux.attached': 'Attached',
|
||||
'systemManager.tmux.detached': 'Detached',
|
||||
'systemManager.tmux.windows': '{{count}} window(s)',
|
||||
'systemManager.tmux.created': 'Created',
|
||||
'systemManager.tmux.activity': 'Activity',
|
||||
'systemManager.tmux.rename': 'Rename',
|
||||
'systemManager.tmux.detach': 'Detach all',
|
||||
'systemManager.tmux.killSession': 'Kill session',
|
||||
'systemManager.tmux.killServer': 'Kill server',
|
||||
'systemManager.tmux.loadingDetails': 'Loading details…',
|
||||
'systemManager.tmux.clients': 'Attached clients',
|
||||
'systemManager.tmux.windowList': 'Windows',
|
||||
'systemManager.tmux.newWindow': 'New window',
|
||||
'systemManager.tmux.newWindowPlaceholder': 'Window name (optional)',
|
||||
'systemManager.tmux.noWindows': 'No windows',
|
||||
'systemManager.tmux.unavailable': 'tmux is not available on this host',
|
||||
'systemManager.docker.unavailable': 'Docker is not available on this host',
|
||||
'systemManager.tmux.windowsMismatch': 'Session reports {{count}} window(s) but list-windows returned none',
|
||||
'systemManager.tmux.lastCommand': 'last command: {{command}}',
|
||||
'systemManager.tmux.noPanes': 'No panes',
|
||||
'systemManager.tmux.panes': '{{count}} pane(s)',
|
||||
'systemManager.tmux.active': 'active',
|
||||
'systemManager.tmux.unnamedWindow': 'Unnamed window',
|
||||
'systemManager.tmux.unnamedPane': 'Unnamed pane',
|
||||
'systemManager.tmux.attachWindow': 'Attach to window',
|
||||
'systemManager.tmux.selectWindow': 'Select window',
|
||||
'systemManager.tmux.killWindow': 'Kill window',
|
||||
'systemManager.tmux.killPane': 'Kill pane',
|
||||
'systemManager.tmux.splitHorizontal': 'Split horizontal',
|
||||
'systemManager.tmux.splitVertical': 'Split vertical',
|
||||
'systemManager.tmux.sendKeys': 'Send keys',
|
||||
'systemManager.tmux.sendKeysTo': 'Send keys to window {{window}} pane {{pane}}',
|
||||
'systemManager.tmux.sendKeysPlaceholder': 'Command or text…',
|
||||
'systemManager.tmux.renameSessionPrompt': 'Rename session',
|
||||
'systemManager.tmux.renameWindowPrompt': 'Rename window',
|
||||
'systemManager.tmux.windowName': 'Window name',
|
||||
'systemManager.tmux.confirmKillSession': 'Kill tmux session "{{name}}"?',
|
||||
'systemManager.tmux.confirmDetachSession': 'Detach all clients from "{{name}}"?',
|
||||
'systemManager.tmux.confirmKillWindow': 'Kill window "{{name}}"?',
|
||||
'systemManager.tmux.confirmKillPane': 'Kill pane #{{index}}?',
|
||||
'systemManager.tmux.confirmKillServer': 'Kill tmux server? All sessions will be terminated.',
|
||||
'systemManager.tmux.meta': '{{count}} session(s)',
|
||||
|
||||
'systemManager.docker.title': 'Containers',
|
||||
'systemManager.docker.subTabs.containers': 'Containers',
|
||||
'systemManager.docker.subTabs.images': 'Images',
|
||||
'systemManager.docker.empty': 'No containers found',
|
||||
'systemManager.docker.imagesEmpty': 'No images found',
|
||||
'systemManager.docker.search': 'Search containers…',
|
||||
'systemManager.docker.searchImages': 'Search images…',
|
||||
'systemManager.docker.filter.all': 'All',
|
||||
'systemManager.docker.filter.running': 'Running',
|
||||
'systemManager.docker.filter.stopped': 'Stopped',
|
||||
'systemManager.docker.filter.paused': 'Paused',
|
||||
'systemManager.docker.shell': 'Shell',
|
||||
'systemManager.docker.logs': 'Logs',
|
||||
'systemManager.docker.details': 'Details',
|
||||
'systemManager.docker.inspect': 'Inspect',
|
||||
'systemManager.docker.imageInspect': 'Image inspect',
|
||||
'systemManager.docker.confirmRemove': 'Remove this container?',
|
||||
'systemManager.docker.confirmKill': 'Force kill this container?',
|
||||
'systemManager.docker.confirmRemoveImage': 'Remove image "{{name}}"?',
|
||||
'systemManager.docker.confirmPrune': 'Remove dangling images?',
|
||||
'systemManager.docker.confirmPruneAll': 'Remove all unused images?',
|
||||
'systemManager.docker.pause': 'Pause',
|
||||
'systemManager.docker.unpause': 'Unpause',
|
||||
'systemManager.docker.restart': 'Restart',
|
||||
'systemManager.docker.kill': 'Kill',
|
||||
'systemManager.docker.renamePrompt': 'Container name',
|
||||
'systemManager.docker.prune': 'Prune',
|
||||
'systemManager.docker.pruneAll': 'Prune all',
|
||||
'systemManager.docker.tag': 'Tag',
|
||||
'systemManager.docker.tagRepoPrompt': 'Repository name',
|
||||
'systemManager.docker.tagNamePrompt': 'Tag name',
|
||||
'systemManager.docker.meta': '{{count}} container(s)',
|
||||
'systemManager.docker.imagesMeta': '{{count}} image(s)',
|
||||
'systemManager.docker.start': 'Start',
|
||||
'systemManager.docker.stop': 'Stop',
|
||||
|
||||
'systemManager.inspect.status': 'Status',
|
||||
'systemManager.inspect.image': 'Image',
|
||||
'systemManager.inspect.created': 'Created',
|
||||
'systemManager.inspect.started': 'Started',
|
||||
'systemManager.inspect.restartPolicy': 'Restart policy',
|
||||
'systemManager.inspect.command': 'Command',
|
||||
'systemManager.inspect.ports': 'Ports',
|
||||
'systemManager.inspect.networks': 'Networks',
|
||||
'systemManager.inspect.mounts': 'Mounts',
|
||||
'systemManager.inspect.env': 'Environment',
|
||||
'systemManager.inspect.labels': 'Labels',
|
||||
'systemManager.inspect.tags': 'Tags',
|
||||
'systemManager.inspect.digests': 'Digests',
|
||||
'systemManager.inspect.size': 'Size',
|
||||
'systemManager.inspect.platform': 'Platform',
|
||||
'systemManager.inspect.workdir': 'Working dir',
|
||||
'systemManager.inspect.exposedPorts': 'Exposed ports',
|
||||
'systemManager.inspect.showRaw': 'JSON',
|
||||
'systemManager.inspect.hideRaw': 'Hide JSON',
|
||||
};
|
||||
@@ -0,0 +1,780 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const enTerminalMessages: Messages = {
|
||||
'terminal.sudoHint.pressEnter': 'Press Enter to paste sudo password',
|
||||
// Terminal toolbar / search / context menu / auth
|
||||
'terminal.toolbar.openSftp': 'Open SFTP',
|
||||
'terminal.toolbar.availableAfterConnect': 'Available after connect',
|
||||
'terminal.toolbar.sendYmodem': 'Send with YMODEM',
|
||||
'terminal.toolbar.receiveYmodem': 'Receive with YMODEM',
|
||||
'terminal.toolbar.sftp': 'SFTP',
|
||||
'terminal.toolbar.more': 'More actions',
|
||||
'terminal.toolbar.scripts': 'Scripts',
|
||||
'terminal.toolbar.history': 'Command history',
|
||||
'terminal.toolbar.configureOsc7': 'Configure directory tracking',
|
||||
'history.scope.label': 'History scope',
|
||||
'history.tab.host': 'Host',
|
||||
'history.tab.global': 'Global',
|
||||
'history.searchPlaceholder': 'Search history...',
|
||||
'history.loading': 'Loading remote history...',
|
||||
'history.meta.count': '{count} commands',
|
||||
'history.empty.noSession': 'Open a remote session to view its command history.',
|
||||
'history.empty.unsupportedProtocol': 'Command history is only available for SSH/Mosh/ET sessions.',
|
||||
'history.empty.noHistory': 'No command history found on this host.',
|
||||
'history.empty.noGlobalHistory': 'No global command history yet. Commands you run will appear here.',
|
||||
'history.action.refresh': 'Refresh',
|
||||
'history.action.retry': 'Retry',
|
||||
'history.action.paste': 'Paste to terminal',
|
||||
'history.action.run': 'Run in terminal',
|
||||
'history.action.saveAsSnippet': 'Save as snippet',
|
||||
'terminal.toolbar.library': 'Library',
|
||||
'terminal.toolbar.noSnippets': 'No snippets available',
|
||||
'terminal.toolbar.terminalSettings': 'Terminal settings',
|
||||
'terminal.toolbar.searchTerminal': 'Search terminal',
|
||||
'terminal.toolbar.search': 'Search',
|
||||
'terminal.toolbar.startSessionLog': 'Start session log',
|
||||
'terminal.toolbar.stopSessionLog': 'Stop session log',
|
||||
'terminal.toolbar.timestampsEnable': 'Show timestamps',
|
||||
'terminal.toolbar.timestampsDisable': 'Hide timestamps',
|
||||
'terminal.toolbar.broadcast': 'Broadcast',
|
||||
'terminal.toolbar.broadcastEnable': 'Enable Broadcast Mode',
|
||||
'terminal.toolbar.broadcastDisable': 'Disable Broadcast Mode',
|
||||
'terminal.toolbar.composeBar': 'Compose Bar',
|
||||
'terminal.composeBar.placeholder': 'Type command here, press Enter to send...',
|
||||
'terminal.composeBar.send': 'Send',
|
||||
'terminal.composeBar.close': 'Close compose bar',
|
||||
'terminal.composeBar.broadcasting': 'Broadcasting to all sessions',
|
||||
'terminal.composeBar.resize': 'Resize compose bar height',
|
||||
'terminal.composeBar.manageSnippets': 'Manage quick snippets',
|
||||
'terminal.composeBar.searchSnippets': 'Search snippets...',
|
||||
'terminal.composeBar.noPinnedSnippets': 'Pin snippets with + for quick access',
|
||||
'terminal.composeBar.noMatchingSnippets': 'No matching snippets',
|
||||
'terminal.composeBar.pinnedCount': '{count} pinned',
|
||||
'terminal.composeBar.unpinSnippet': 'Remove {label} from quick bar',
|
||||
'terminal.composeBar.snippetClickHint': 'Click to insert · Shift+Click to send',
|
||||
'terminal.toolbar.focus': 'Focus',
|
||||
'terminal.toolbar.focusMode': 'Focus Mode',
|
||||
'terminal.toolbar.detach': 'Detach to standalone tab',
|
||||
'terminal.toolbar.encoding': 'Terminal Encoding',
|
||||
'terminal.toolbar.encoding.utf8': 'UTF-8',
|
||||
'terminal.toolbar.encoding.gb18030': 'GB18030',
|
||||
'terminal.toolbar.closeSession': 'Close session',
|
||||
'terminal.toolbar.hostHighlight.title': 'Host Keyword Highlighting',
|
||||
'terminal.toolbar.hostHighlight.noRules': 'No custom highlight rules defined for this host',
|
||||
'terminal.toolbar.hostHighlight.addRule': 'Add New Rule',
|
||||
'terminal.toolbar.hostHighlight.labelPlaceholder': 'Label (e.g., Error)',
|
||||
'terminal.toolbar.hostHighlight.patternPlaceholder': 'Regex pattern (e.g., \\bfailed\\b)',
|
||||
'terminal.toolbar.hostHighlight.invalidPattern': 'Invalid regex pattern',
|
||||
'terminal.toolbar.hostHighlight.clearAll': 'Clear All',
|
||||
'terminal.toolbar.hostHighlight.changeColor': 'Change highlight color for',
|
||||
'terminal.toolbar.hostHighlight.selectColor': 'Select color for new rule',
|
||||
'terminal.statusbar.copyHostname.label': 'Copy host address',
|
||||
'terminal.statusbar.copyHostname.tooltip': 'Copy host address ({hostname})',
|
||||
'terminal.statusbar.copyHostname.toast': 'Copied host address: {hostname}',
|
||||
'terminal.statusbar.copyHostname.error': 'Failed to copy host address to clipboard',
|
||||
'terminal.serverStats.cpu': 'CPU Usage',
|
||||
'terminal.serverStats.cpuCores': 'CPU Core Usage',
|
||||
'terminal.serverStats.memory': 'Memory Usage',
|
||||
'terminal.serverStats.memoryDetails': 'Memory Details',
|
||||
'terminal.serverStats.memUsed': 'Used',
|
||||
'terminal.serverStats.memBuffers': 'Buffers',
|
||||
'terminal.serverStats.memCached': 'Cache',
|
||||
'terminal.serverStats.memFree': 'Free',
|
||||
'terminal.serverStats.swap': 'Swap',
|
||||
'terminal.serverStats.swapUsed': 'Swap Used',
|
||||
'terminal.serverStats.swapFree': 'Swap Free',
|
||||
'terminal.serverStats.swapTotal': 'Total',
|
||||
'terminal.serverStats.topProcesses': 'Top Processes by Memory',
|
||||
'terminal.serverStats.disk': 'Disk Usage (Root)',
|
||||
'terminal.serverStats.diskDetails': 'Mounted Disks',
|
||||
'terminal.serverStats.network': 'Network Speed',
|
||||
'terminal.serverStats.latency': 'Latency',
|
||||
'terminal.serverStats.networkDetails': 'Network Interfaces',
|
||||
'terminal.serverStats.noData': 'No data available',
|
||||
'terminal.dragDrop.localTitle': 'Drop to Insert Paths',
|
||||
'terminal.dragDrop.localMessage': 'File paths will be inserted into the terminal',
|
||||
'terminal.dragDrop.remoteTitle': 'Drop to Upload Files',
|
||||
'terminal.dragDrop.remoteZmodemMessage': 'Files will be uploaded via ZMODEM (PTY)',
|
||||
'terminal.dragDrop.remoteSftpMessage': 'Files will be uploaded via SFTP',
|
||||
'terminal.dragDrop.noFiles': 'No files to upload',
|
||||
'terminal.dragDrop.notConnected': 'Cannot drop files - terminal is not connected',
|
||||
'terminal.dragDrop.errorTitle': 'Drop Error',
|
||||
'terminal.dragDrop.errorMessage': 'Failed to process dropped files',
|
||||
'terminal.search.placeholder': 'Search...',
|
||||
'terminal.search.noResults': 'No results',
|
||||
'terminal.search.prevMatch': 'Previous match (Shift+Enter)',
|
||||
'terminal.search.nextMatch': 'Next match (Enter)',
|
||||
'terminal.menu.copy': 'Copy',
|
||||
'terminal.menu.paste': 'Paste',
|
||||
'terminal.menu.uploadClipboardImage': 'Upload clipboard image',
|
||||
'terminal.menu.addSelectionToAI': 'Add to Conversation',
|
||||
'terminal.menu.pasteSelection': 'Paste Selection',
|
||||
'terminal.menu.selectAll': 'Select All',
|
||||
'terminal.menu.reconnect': 'Reconnect',
|
||||
'terminal.menu.sendYmodem': 'Send with YMODEM',
|
||||
'terminal.menu.receiveYmodem': 'Receive with YMODEM',
|
||||
'terminal.menu.splitHorizontal': 'Split Horizontal',
|
||||
'terminal.menu.splitVertical': 'Split Vertical',
|
||||
'terminal.menu.clearBuffer': 'Clear Buffer',
|
||||
'terminal.menu.closeTerminal': 'Close terminal',
|
||||
'terminal.menu.rename': 'Rename',
|
||||
'terminal.menu.detach': 'Detach from workspace',
|
||||
'terminal.menu.detachSession': 'Detach {name}',
|
||||
'terminal.clipboardImageUpload.noImage': 'Clipboard does not contain an image',
|
||||
'terminal.clipboardImageUpload.failed': 'Could not upload clipboard image',
|
||||
'terminal.osc7Setup.title': 'Configure directory tracking',
|
||||
'terminal.osc7Setup.desc': 'Netcatty will add OSC 7 prompt hooks for the current remote user. This helps SFTP follow the terminal directory after sudo or su.',
|
||||
'terminal.osc7Setup.targets': 'Possible files to update',
|
||||
'terminal.osc7Setup.command': 'Command to run',
|
||||
'terminal.osc7Setup.run': 'Run setup',
|
||||
'terminal.osc7Setup.running': 'Configuring...',
|
||||
'terminal.osc7Setup.configured': 'Directory tracking configured',
|
||||
'terminal.osc7Setup.failed': 'Directory tracking setup failed',
|
||||
'terminal.osc7Setup.sent': 'Directory tracking setup sent to terminal',
|
||||
'terminal.ymodem.selectFile': 'Select file to send',
|
||||
'terminal.ymodem.allFiles': 'All files',
|
||||
'terminal.ymodem.started': 'YMODEM sending {fileName}',
|
||||
'terminal.ymodem.complete': 'YMODEM sent {fileName}',
|
||||
'terminal.ymodem.failed': 'YMODEM send failed',
|
||||
'terminal.ymodem.selectReceiveDirectory': 'Select folder to save received files',
|
||||
'terminal.ymodem.receiveStarted': 'YMODEM receiving...',
|
||||
'terminal.ymodem.receiveComplete': 'YMODEM received {fileName}',
|
||||
'terminal.ymodem.receiveCompleteMultiple': 'YMODEM received {count} files',
|
||||
'terminal.ymodem.receiveEmpty': 'No YMODEM files received',
|
||||
'terminal.ymodem.receiveFailed': 'YMODEM receive failed',
|
||||
'terminal.ymodem.unavailable': 'YMODEM is unavailable',
|
||||
'terminal.selection.addToAI': 'Add to Conversation',
|
||||
'terminal.selection.addToAIDesc': 'Attach selected terminal output to the AI draft',
|
||||
'terminal.auth.password': 'Password',
|
||||
'terminal.auth.sshKey': 'SSH Key',
|
||||
'terminal.auth.username': 'Username',
|
||||
'terminal.auth.username.placeholder': 'root',
|
||||
'terminal.auth.passwordLabel': 'Password',
|
||||
'terminal.auth.password.placeholder': 'Enter password',
|
||||
'terminal.auth.passphrase': 'Passphrase',
|
||||
'terminal.auth.passphrase.placeholder': 'Optional passphrase for the selected private key',
|
||||
'terminal.auth.certificate': 'Certificate',
|
||||
'terminal.auth.selectKey': 'Select Key',
|
||||
'terminal.auth.retryMessage': 'Authentication failed. Please check your credentials and try again.',
|
||||
'terminal.auth.retryLog': 'Authentication failed. Please try again.',
|
||||
'terminal.auth.noKeysHint': 'No keys available. Add keys in Keychain.',
|
||||
'terminal.auth.continueSave': 'Continue & Save',
|
||||
'terminal.auth.credentialsUnavailable': 'Saved credentials cannot be decrypted on this device. Please re-enter and save them again.',
|
||||
'terminal.auth.jumpCredentialsUnavailable': 'A jump host has saved credentials that cannot be decrypted on this device. Open host settings and re-enter them.',
|
||||
'terminal.auth.proxyCredentialsUnavailable': 'Proxy credentials cannot be decrypted on this device. Open host settings and re-enter the proxy password.',
|
||||
'terminal.auth.keyUnavailableFallbackPassword': 'Saved SSH key is unavailable on this device. Falling back to password authentication.',
|
||||
'terminal.progress.timeoutIn': 'Timeout in {seconds}s',
|
||||
'terminal.progress.waitingForUserInput': 'Waiting for user input',
|
||||
'terminal.progress.disconnected': 'Disconnected',
|
||||
'terminal.progress.cancelling': 'Cancelling...',
|
||||
'terminal.progress.startOver': 'Start over',
|
||||
'terminal.progress.enterReconnectHint': 'Press Enter to reconnect',
|
||||
'terminal.progress.autoReconnectScheduled': 'Connection lost. Reconnecting in {seconds}s (attempt {attempt}).',
|
||||
'terminal.progress.autoReconnectAttempt': 'Auto reconnect attempt {attempt}...',
|
||||
'terminal.connection.dismissDisconnectedDialog': 'Dismiss disconnected notice',
|
||||
'terminal.connection.chainOf': 'Chain {current} of {total}',
|
||||
'terminal.connection.showLogs': 'Show logs',
|
||||
'terminal.connection.hideLogs': 'Hide logs',
|
||||
'terminal.connection.protocol.ssh': 'SSH',
|
||||
'terminal.connection.protocol.telnet': 'Telnet',
|
||||
'terminal.connection.protocol.mosh': 'Mosh',
|
||||
'terminal.connection.protocol.et': 'EternalTerminal',
|
||||
'terminal.et.proxyUnsupported': 'EternalTerminal does not currently support Netcatty proxy settings. Use SSH or remove the proxy for this host.',
|
||||
'terminal.et.multiJumpUnsupported': 'EternalTerminal currently supports at most one jump host in Netcatty.',
|
||||
'terminal.connection.protocol.serial': 'Serial',
|
||||
'terminal.connection.protocol.local': 'Local Shell',
|
||||
'terminal.hostKey.unknownTitle': 'Confirm this host key',
|
||||
'terminal.hostKey.changedTitle': 'Host key changed',
|
||||
'terminal.hostKey.unknownDescription': 'The authenticity of {host} cannot be established yet.',
|
||||
'terminal.hostKey.changedDescription': 'The saved key for {host} no longer matches this server.',
|
||||
'terminal.hostKey.fingerprintLabel': '{keyType} fingerprint is SHA256:',
|
||||
'terminal.hostKey.savedFingerprintLabel': 'Saved fingerprint',
|
||||
'terminal.hostKey.unknownHint': 'Remember it if this fingerprint belongs to the server you expected.',
|
||||
'terminal.hostKey.changedHint': 'Only continue if you expected this host to change.',
|
||||
'terminal.hostKey.addAndContinue': 'Add and continue',
|
||||
'terminal.hostKey.updateAndContinue': 'Update and continue',
|
||||
'terminal.themeModal.title': 'Terminal Appearance',
|
||||
'terminal.themeModal.tab.theme': 'Theme',
|
||||
'terminal.themeModal.tab.font': 'Font',
|
||||
'terminal.themeModal.tab.custom': 'Custom',
|
||||
'terminal.themeModal.globalTheme': 'Global Theme',
|
||||
'terminal.themeModal.globalFont': 'Global Font',
|
||||
'terminal.themeModal.fontSize': 'Font Size',
|
||||
'terminal.themeModal.fontWeight': 'Font Weight',
|
||||
'terminal.themeModal.livePreview': 'Live Preview',
|
||||
'terminal.themeModal.themeType': '{type} theme',
|
||||
'terminal.hiddenTheme.title': 'Current hidden theme',
|
||||
'terminal.hiddenTheme.desc': 'This theme is hidden from manual picks and will be replaced when you choose another theme.',
|
||||
'topTabs.toggleTheme.systemExitTitle': 'System theme is active',
|
||||
'topTabs.toggleTheme.systemExitMessage': 'Open Settings to choose a fixed Light or Dark theme.',
|
||||
'topTabs.toggleTheme.openSettings': 'Open Settings',
|
||||
|
||||
// Custom Themes
|
||||
'terminal.customTheme.section': 'Custom Themes',
|
||||
'terminal.customTheme.yourThemes': 'Your Themes',
|
||||
'terminal.customTheme.new': 'New Theme',
|
||||
'terminal.customTheme.newDesc': 'Clone current theme and customize',
|
||||
'terminal.customTheme.newTitle': 'New Custom Theme',
|
||||
'terminal.customTheme.editTitle': 'Edit Theme',
|
||||
'terminal.customTheme.import': 'Import .itermcolors',
|
||||
'terminal.customTheme.importDesc': 'Import from iTerm2 color scheme file',
|
||||
'terminal.customTheme.importError': 'Failed to parse the selected file. Please ensure it is a valid .itermcolors XML file.',
|
||||
'terminal.customTheme.delete': 'Delete Theme',
|
||||
'terminal.customTheme.confirmDelete': 'Confirm Delete',
|
||||
'terminal.customTheme.name': 'Name',
|
||||
'terminal.customTheme.namePlaceholder': 'My Custom Theme',
|
||||
'terminal.customTheme.type': 'Type',
|
||||
'terminal.customTheme.group.general': 'General',
|
||||
'terminal.customTheme.group.normal': 'Normal Colors',
|
||||
'terminal.customTheme.group.bright': 'Bright Colors',
|
||||
'terminal.customTheme.color.background': 'Background',
|
||||
'terminal.customTheme.color.foreground': 'Foreground',
|
||||
'terminal.customTheme.color.cursor': 'Cursor',
|
||||
'terminal.customTheme.color.selection': 'Selection',
|
||||
'terminal.customTheme.color.black': 'Black',
|
||||
'terminal.customTheme.color.red': 'Red',
|
||||
'terminal.customTheme.color.green': 'Green',
|
||||
'terminal.customTheme.color.yellow': 'Yellow',
|
||||
'terminal.customTheme.color.blue': 'Blue',
|
||||
'terminal.customTheme.color.magenta': 'Magenta',
|
||||
'terminal.customTheme.color.cyan': 'Cyan',
|
||||
'terminal.customTheme.color.white': 'White',
|
||||
'terminal.customTheme.color.brightBlack': 'Bright Black',
|
||||
'terminal.customTheme.color.brightRed': 'Bright Red',
|
||||
'terminal.customTheme.color.brightGreen': 'Bright Green',
|
||||
'terminal.customTheme.color.brightYellow': 'Bright Yellow',
|
||||
'terminal.customTheme.color.brightBlue': 'Bright Blue',
|
||||
'terminal.customTheme.color.brightMagenta': 'Bright Magenta',
|
||||
'terminal.customTheme.color.brightCyan': 'Bright Cyan',
|
||||
'terminal.customTheme.color.brightWhite': 'Bright White',
|
||||
|
||||
// Cloud Sync Settings
|
||||
'cloudSync.gate.title': 'End-to-End Encrypted Sync',
|
||||
'cloudSync.gate.desc':
|
||||
'Your data is encrypted locally before syncing. Cloud providers never see your plaintext data. Set a master key to enable secure sync.',
|
||||
'cloudSync.gate.masterKey': 'Master Key',
|
||||
'cloudSync.gate.confirmMasterKey': 'Confirm Master Key',
|
||||
'cloudSync.gate.placeholder': 'Enter a strong password',
|
||||
'cloudSync.gate.confirmPlaceholder': 'Confirm your password',
|
||||
'cloudSync.gate.mismatch': 'Passwords do not match',
|
||||
'cloudSync.gate.warning':
|
||||
'I understand that if I forget my master key, my data cannot be recovered. There is no password reset.',
|
||||
'cloudSync.gate.enableVault': 'Enable Encrypted Vault',
|
||||
'cloudSync.gate.enabledToast': 'Encrypted vault enabled',
|
||||
'cloudSync.gate.setupFailed': 'Failed to set up master key',
|
||||
'cloudSync.passwordStrength.tooShort': 'Too short',
|
||||
'cloudSync.passwordStrength.weak': 'Weak',
|
||||
'cloudSync.passwordStrength.moderate': 'Moderate',
|
||||
'cloudSync.passwordStrength.strong': 'Strong',
|
||||
'cloudSync.passwordStrength.veryStrong': 'Very Strong',
|
||||
'cloudSync.provider.notConnected': 'Not connected',
|
||||
'cloudSync.provider.sync': 'Sync',
|
||||
'cloudSync.provider.connect': 'Connect',
|
||||
'cloudSync.provider.connecting': 'Connecting...',
|
||||
'cloudSync.provider.webdav': 'WebDAV',
|
||||
'cloudSync.provider.webdav.desc': 'Connect to a self-hosted WebDAV endpoint',
|
||||
'cloudSync.provider.s3': 'S3 Compatible',
|
||||
'cloudSync.provider.s3.desc': 'Connect to S3-compatible object storage',
|
||||
'cloudSync.provider.comingSoon': 'Coming soon',
|
||||
'cloudSync.webdav.title': 'WebDAV Settings',
|
||||
'cloudSync.webdav.desc': 'Configure a WebDAV endpoint for encrypted sync.',
|
||||
'cloudSync.webdav.endpoint': 'Endpoint URL',
|
||||
'cloudSync.webdav.authType': 'Auth Type',
|
||||
'cloudSync.webdav.auth.basic': 'Basic',
|
||||
'cloudSync.webdav.auth.digest': 'Digest',
|
||||
'cloudSync.webdav.auth.token': 'Token',
|
||||
'cloudSync.webdav.username': 'Username',
|
||||
'cloudSync.webdav.password': 'Password',
|
||||
'cloudSync.webdav.token': 'Token',
|
||||
'cloudSync.webdav.showSecret': 'Show secret',
|
||||
'cloudSync.webdav.allowInsecure': 'Allow insecure connection (ignore certificate errors)',
|
||||
'cloudSync.webdav.validation.endpoint': 'Enter a valid WebDAV endpoint.',
|
||||
'cloudSync.webdav.validation.credentials': 'Username and password are required.',
|
||||
'cloudSync.webdav.validation.token': 'Token is required.',
|
||||
'cloudSync.s3.title': 'S3 Settings',
|
||||
'cloudSync.s3.desc': 'Connect to S3-compatible object storage for encrypted sync.',
|
||||
'cloudSync.s3.endpoint': 'Endpoint URL',
|
||||
'cloudSync.s3.region': 'Region',
|
||||
'cloudSync.s3.bucket': 'Bucket',
|
||||
'cloudSync.s3.accessKeyId': 'Access Key ID',
|
||||
'cloudSync.s3.secretAccessKey': 'Secret Access Key',
|
||||
'cloudSync.s3.sessionToken': 'Session Token (optional)',
|
||||
'cloudSync.s3.prefix': 'Key Prefix (optional)',
|
||||
'cloudSync.s3.forcePathStyle': 'Force path-style URLs (for MinIO/R2, etc.)',
|
||||
'cloudSync.s3.allowInsecure': 'Allow insecure connection (ignore certificate errors)',
|
||||
'cloudSync.s3.showSecret': 'Show secrets',
|
||||
'cloudSync.s3.validation.required': 'Endpoint, region, bucket, access key, and secret are required.',
|
||||
'cloudSync.smb.title': 'SMB Settings',
|
||||
'cloudSync.smb.desc': 'Connect to an SMB/CIFS file share for encrypted sync.',
|
||||
'cloudSync.smb.share': 'Share Path',
|
||||
'cloudSync.smb.username': 'Username',
|
||||
'cloudSync.smb.password': 'Password',
|
||||
'cloudSync.smb.domain': 'Domain (optional)',
|
||||
'cloudSync.smb.domainPlaceholder': 'e.g., WORKGROUP',
|
||||
'cloudSync.smb.port': 'Port (optional)',
|
||||
'cloudSync.smb.showSecret': 'Show password',
|
||||
'cloudSync.smb.validation.share': 'Share path is required.',
|
||||
'cloudSync.smb.validation.port': 'Port must be a number between 1 and 65535.',
|
||||
'cloudSync.connect.smb.success': 'SMB connected successfully',
|
||||
'cloudSync.connect.smb.failedTitle': 'SMB connection failed',
|
||||
'cloudSync.provider.smb': 'SMB Share',
|
||||
'cloudSync.connect.webdav.success': 'WebDAV connected successfully',
|
||||
'cloudSync.connect.webdav.failedTitle': 'WebDAV connection failed',
|
||||
'cloudSync.connect.s3.success': 'S3 connected successfully',
|
||||
'cloudSync.connect.s3.failedTitle': 'S3 connection failed',
|
||||
'cloudSync.lastSync.never': 'Never',
|
||||
'cloudSync.lastSync.justNow': 'Just now',
|
||||
'cloudSync.lastSync.minutesAgo': '{minutes} min ago',
|
||||
'cloudSync.changeKey': 'Change Key',
|
||||
'cloudSync.providers.title': 'Cloud Providers',
|
||||
'cloudSync.syncAll': 'Sync All Connected Providers',
|
||||
'cloudSync.autoSync.title': 'Auto-sync',
|
||||
'cloudSync.autoSync.desc': 'Automatically sync when changes are made',
|
||||
'cloudSync.strategy.title': 'Sync strategy',
|
||||
'cloudSync.strategy.desc': 'Choose what happens when local and cloud data both changed.',
|
||||
'cloudSync.strategy.smartMerge': 'Smart merge (recommended)',
|
||||
'cloudSync.strategy.smartMergeDesc': 'Combine changes from both sides when possible; if Netcatty cannot decide safely, ask you to choose.',
|
||||
'cloudSync.strategy.preferCloud': 'Cloud wins',
|
||||
'cloudSync.strategy.preferCloudDesc': 'When both sides changed, download the cloud version and replace local changes.',
|
||||
'cloudSync.strategy.preferLocal': 'Local wins',
|
||||
'cloudSync.strategy.preferLocalDesc': 'When both sides changed, upload the local version and replace cloud changes.',
|
||||
'cloudSync.status.title': 'Sync Status',
|
||||
'cloudSync.status.localVersion': 'Local Version',
|
||||
'cloudSync.status.remoteVersion': 'Remote Version',
|
||||
'cloudSync.history.title': 'Sync History',
|
||||
'cloudSync.history.upload': 'Upload',
|
||||
'cloudSync.history.download': 'Download',
|
||||
'cloudSync.history.resolved': 'Resolved',
|
||||
'cloudSync.history.error': 'Error',
|
||||
'cloudSync.localBackups.title': 'Local Backup History',
|
||||
'cloudSync.localBackups.desc': 'Netcatty keeps local restore points before app version changes and before vault restores.',
|
||||
'cloudSync.localBackups.retentionTitle': 'Backup Retention',
|
||||
'cloudSync.localBackups.retentionDesc': 'Choose how many local backups Netcatty should keep.',
|
||||
'cloudSync.localBackups.maxCount': 'Max backups',
|
||||
'cloudSync.localBackups.maxSaved': 'Saved backup retention: {count}',
|
||||
'cloudSync.localBackups.maxInvalid': 'Please enter a number between 1 and 100.',
|
||||
'cloudSync.localBackups.empty': 'No local backups yet.',
|
||||
'cloudSync.localBackups.reason.appVersionChange': 'Before app version change',
|
||||
'cloudSync.localBackups.reason.beforeRestore': 'Before restore',
|
||||
'cloudSync.localBackups.versionChange': '{from} -> {to}',
|
||||
'cloudSync.localBackups.counts': '{hosts} hosts, {keys} keys, {snippets} snippets, {notes} notes',
|
||||
'cloudSync.localBackups.restore': 'Restore',
|
||||
'cloudSync.localBackups.restoreSuccess': 'Local backup restored.',
|
||||
'cloudSync.localBackups.restoreFailedTitle': 'Restore failed',
|
||||
'cloudSync.localBackups.restoreMissing': 'Backup not found.',
|
||||
'cloudSync.localBackups.protectiveBackupFailed': 'Safety backup could not be created, so the restore was aborted to protect your current data. Resolve the underlying issue (e.g. keychain access) and try again. Details: {message}',
|
||||
'cloudSync.localBackups.restoreConfirmTitle': 'Restore this backup?',
|
||||
'cloudSync.localBackups.restoreConfirmDesc': 'Your current hosts, keys, snippets and settings will be replaced with the contents of this backup. A protective snapshot of your current data is taken automatically first.',
|
||||
'cloudSync.localBackups.restoreConfirmButton': 'Restore',
|
||||
'cloudSync.localBackups.restoreConfirmCancel': 'Cancel',
|
||||
'cloudSync.localBackups.unavailableTitle': 'Local backups unavailable',
|
||||
'cloudSync.localBackups.unavailableDesc': 'This platform does not expose a secure keychain to Netcatty, so local backups cannot be written safely. Install Netcatty on a system with a supported keychain to enable the local backup history.',
|
||||
'cloudSync.localBackups.lockedTitle': 'Master key required',
|
||||
'cloudSync.localBackups.lockedDesc': 'Set up or unlock your master key before restoring a backup, so restored credentials remain encrypted.',
|
||||
'cloudSync.revisionHistory.viewButton': 'History',
|
||||
'cloudSync.revisionHistory.title': 'Vault Version History',
|
||||
'cloudSync.revisionHistory.description': 'Browse and restore previous versions of your vault from the Gist revision history.',
|
||||
'cloudSync.revisionHistory.empty': 'No revisions found.',
|
||||
'cloudSync.revisionHistory.current': 'Current',
|
||||
'cloudSync.revisionHistory.revision': 'Revision',
|
||||
'cloudSync.revisionHistory.revisionPreview': 'Revision Contents',
|
||||
'cloudSync.revisionHistory.device': 'Device',
|
||||
'cloudSync.revisionHistory.hosts': 'Hosts',
|
||||
'cloudSync.revisionHistory.keys': 'Keys',
|
||||
'cloudSync.revisionHistory.snippets': 'Snippets',
|
||||
'cloudSync.revisionHistory.notes': 'Notes',
|
||||
'cloudSync.revisionHistory.identities': 'Identities',
|
||||
'cloudSync.revisionHistory.restoreButton': 'Restore This Version',
|
||||
'cloudSync.revisionHistory.restored': 'Vault restored from selected revision.',
|
||||
'cloudSync.revisionHistory.revisionNotFound': 'Revision not found or does not contain vault data.',
|
||||
'cloudSync.revisionHistory.decryptFailed': 'Cannot decrypt this revision. It may have been encrypted with a different master password.',
|
||||
'cloudSync.changeKey.title': 'Change Master Key',
|
||||
'cloudSync.changeKey.current': 'Current Master Key',
|
||||
'cloudSync.changeKey.new': 'New Master Key',
|
||||
'cloudSync.changeKey.confirmNew': 'Confirm New Master Key',
|
||||
'cloudSync.changeKey.currentPlaceholder': 'Enter current master key',
|
||||
'cloudSync.changeKey.newPlaceholder': 'Enter new master key',
|
||||
'cloudSync.changeKey.confirmPlaceholder': 'Confirm new master key',
|
||||
'cloudSync.changeKey.fillAll': 'Please fill in all fields',
|
||||
'cloudSync.changeKey.minLength': 'New master key must be at least 8 characters',
|
||||
'cloudSync.changeKey.notMatch': 'New master keys do not match',
|
||||
'cloudSync.changeKey.incorrectCurrent': 'Incorrect current master key',
|
||||
'cloudSync.changeKey.failed': 'Failed to change master key',
|
||||
'cloudSync.changeKey.desc': 'This will re-encrypt your vault. Make sure you remember the new key.',
|
||||
'cloudSync.changeKey.showKeys': 'Show keys',
|
||||
'cloudSync.changeKey.updatedToast': 'Master key updated',
|
||||
'cloudSync.changeKey.updateButton': 'Update Key',
|
||||
'cloudSync.unlock.title': 'Enter Master Key',
|
||||
'cloudSync.unlock.masterKey': 'Master Key',
|
||||
'cloudSync.unlock.desc':
|
||||
'Enter your master key once to enable encrypted sync. It will be stored securely using your OS keychain.',
|
||||
'cloudSync.unlock.placeholder': 'Enter your master key',
|
||||
'cloudSync.unlock.empty': 'Please enter your master key',
|
||||
'cloudSync.unlock.incorrect': 'Incorrect master key',
|
||||
'cloudSync.unlock.failed': 'Failed to unlock vault',
|
||||
'cloudSync.unlock.showKey': 'Show key',
|
||||
'cloudSync.unlock.notNow': 'Not now',
|
||||
'cloudSync.unlock.readyToast': 'Vault ready',
|
||||
'cloudSync.unlock.unlockButton': 'Unlock',
|
||||
'cloudSync.header.vaultReady': 'Vault ready',
|
||||
'cloudSync.header.preparingVault': 'Preparing vault...',
|
||||
'cloudSync.header.providersConnected': '{count} provider(s) connected',
|
||||
'cloudSync.githubFlow.title': 'Connect to GitHub',
|
||||
'cloudSync.githubFlow.desc': 'Copy the code below and enter it on GitHub to authorize Netcatty.',
|
||||
'cloudSync.githubFlow.copyCode': 'Copy code',
|
||||
'cloudSync.githubFlow.copied': 'Copied!',
|
||||
'cloudSync.githubFlow.openGitHub': 'Open GitHub',
|
||||
'cloudSync.githubFlow.waiting': 'Waiting for authorization...',
|
||||
'cloudSync.conflict.title': 'Version conflict detected',
|
||||
'cloudSync.conflict.desc': 'Choose which version to keep',
|
||||
'cloudSync.conflict.local': 'LOCAL',
|
||||
'cloudSync.conflict.cloud': 'CLOUD',
|
||||
'cloudSync.conflict.detailsTitle': 'Changed data',
|
||||
'cloudSync.conflict.detailsCounts': 'Local {local} · Cloud {cloud} · Conflicts {conflicts}',
|
||||
'cloudSync.conflict.entity.hosts': 'Hosts',
|
||||
'cloudSync.conflict.entity.keys': 'Keys',
|
||||
'cloudSync.conflict.entity.identities': 'Identities',
|
||||
'cloudSync.conflict.entity.proxyProfiles': 'Proxy profiles',
|
||||
'cloudSync.conflict.entity.snippets': 'Snippets',
|
||||
'cloudSync.conflict.entity.notes': 'Notes',
|
||||
'cloudSync.conflict.entity.noteGroups': 'Note groups',
|
||||
'cloudSync.conflict.entity.customGroups': 'Groups',
|
||||
'cloudSync.conflict.entity.snippetPackages': 'Snippet packages',
|
||||
'cloudSync.conflict.entity.portForwardingRules': 'Port forwarding',
|
||||
'cloudSync.conflict.entity.groupConfigs': 'Group settings',
|
||||
'cloudSync.conflict.entity.settings': 'Settings',
|
||||
'cloudSync.conflict.keepLocal': 'Overwrite cloud (keep local)',
|
||||
'cloudSync.conflict.useCloud': 'Download cloud (overwrite local)',
|
||||
'cloudSync.connect.browserContinue': 'Complete authorization in browser',
|
||||
'cloudSync.connect.browserCancelled': 'Previous browser authorization was cancelled',
|
||||
'cloudSync.connect.github.success': 'GitHub connected successfully',
|
||||
'cloudSync.connect.github.failedTitle': 'GitHub connection failed',
|
||||
'cloudSync.connect.github.timeout': 'GitHub connection timed out. Check your network or proxy settings.',
|
||||
'cloudSync.connect.github.networkError': 'Unable to reach GitHub. Check your network or proxy settings.',
|
||||
'cloudSync.connect.google.failedTitle': 'Google connection failed',
|
||||
'cloudSync.connect.onedrive.failedTitle': 'OneDrive connection failed',
|
||||
'cloudSync.sync.success': 'Synced to {provider}',
|
||||
'cloudSync.sync.failed': 'Sync failed',
|
||||
'cloudSync.sync.failedTitle': 'Sync failed',
|
||||
'cloudSync.sync.errorTitle': 'Sync error',
|
||||
'cloudSync.resolve.downloaded': 'Downloaded cloud data',
|
||||
'cloudSync.resolve.uploaded': 'Uploaded local data',
|
||||
'cloudSync.resolve.failedTitle': 'Conflict resolution failed',
|
||||
'cloudSync.clearLocal.title': 'Clear Local Data',
|
||||
'cloudSync.clearLocal.desc': 'Reset local version and sync history. Next sync will download from cloud.',
|
||||
'cloudSync.clearLocal.button': 'Clear',
|
||||
'cloudSync.clearLocal.dialog.title': 'Clear Local Vault Data?',
|
||||
'cloudSync.clearLocal.dialog.desc': 'This will reset local version to 0 and clear sync history. Your next sync will download data from the cloud, replacing local data.',
|
||||
'cloudSync.clearLocal.dialog.cancel': 'Cancel',
|
||||
'cloudSync.clearLocal.dialog.confirm': 'Clear Local Data',
|
||||
'cloudSync.clearLocal.toast.title': 'Local data cleared',
|
||||
'cloudSync.clearLocal.toast.desc': 'Local version reset to 0. Sync to download from cloud.',
|
||||
|
||||
// Keychain
|
||||
'keychain.filter.key': 'KEY',
|
||||
'keychain.filter.certificate': 'CERTIFICATE',
|
||||
'keychain.action.generateKey': 'Generate Key',
|
||||
'keychain.action.importKey': 'Import Key',
|
||||
'keychain.action.newIdentity': 'New Identity',
|
||||
'keychain.action.importCertificate': 'Import Certificate',
|
||||
'keychain.view.grid': 'Grid',
|
||||
'keychain.view.list': 'List',
|
||||
'keychain.section.keys': 'Keys',
|
||||
'keychain.section.identities': 'Identities',
|
||||
'keychain.count.items': '{count} items',
|
||||
'keychain.empty.title': 'Set up your keys',
|
||||
'keychain.empty.desc': 'Import or generate SSH keys for secure authentication.',
|
||||
'keychain.panel.generateKey': 'Generate Key',
|
||||
'keychain.panel.newKey': 'New Key',
|
||||
'keychain.panel.keyDetails': 'Key Details',
|
||||
'keychain.panel.editKey': 'Edit Key',
|
||||
'keychain.panel.editIdentity': 'Edit Identity',
|
||||
'keychain.panel.newIdentity': 'New Identity',
|
||||
'keychain.panel.keyExport': 'Key Export',
|
||||
'keychain.validation.labelRequired': 'Please enter a label for the key',
|
||||
'keychain.validation.labelAndPrivateKeyRequired': 'Label and private key are required',
|
||||
'keychain.validation.labelAndUsernameRequired': 'Label and username are required',
|
||||
'keychain.error.generationUnavailable':
|
||||
'Key generation not available - please ensure the app is running in Electron',
|
||||
'keychain.error.generateKeyPairFailed': 'Failed to generate key pair',
|
||||
'keychain.error.generateKeyFailed': 'Failed to generate key',
|
||||
'keychain.error.keyGenerationTitle': 'Key Generation',
|
||||
'keychain.export.exportTo': 'Export to *',
|
||||
'keychain.export.selectHost': 'Select Host',
|
||||
'keychain.export.location': 'Location ~ $1 *',
|
||||
'keychain.export.filename': 'Filename ~ $2 *',
|
||||
'keychain.export.note':
|
||||
'Key export currently supports only {unix} systems. Use the {advanced} section to customize the export script.',
|
||||
'keychain.export.script': 'Script *',
|
||||
'keychain.export.scriptPlaceholder': 'Export script...',
|
||||
'keychain.export.missingCredentials':
|
||||
'Host has no saved password or key. Please add password credentials to the host first.',
|
||||
'keychain.export.successTitle': 'Export Successful',
|
||||
'keychain.export.successMessage': 'Public key exported and attached to {host}',
|
||||
'keychain.export.failedTitle': 'Export Failed',
|
||||
'keychain.export.failedMessage': 'Failed to export key: {error}',
|
||||
'keychain.export.failedPrefix': 'Export failed: {error}',
|
||||
'keychain.export.exitCode': 'Command exited with code {code}',
|
||||
'keychain.export.exporting': 'Exporting...',
|
||||
'keychain.export.exportAndAttach': 'Export and Attach',
|
||||
'keychain.export.title': 'Key export',
|
||||
'keychain.export.exportToRequired': 'Export to *',
|
||||
'keychain.export.selectHostPlaceholder': 'Select a host...',
|
||||
'keychain.export.locationLabel': 'Location ~ $1 *',
|
||||
'keychain.export.filenameLabel': 'Filename ~ $2 *',
|
||||
'keychain.export.advanced': 'Advanced',
|
||||
'keychain.export.note.supportsOnly': 'Key export currently supports only',
|
||||
'keychain.export.note.systems': 'systems.',
|
||||
'keychain.export.note.use': 'Use',
|
||||
'keychain.export.note.customize': 'section to customize the export script.',
|
||||
'keychain.export.scriptRequired': 'Script *',
|
||||
'keychain.export.exportToHost': 'Export to host',
|
||||
'keychain.export.failedGeneric': 'Export failed: {message}',
|
||||
'keychain.field.label': 'Label',
|
||||
'keychain.field.labelRequired': 'Label *',
|
||||
'keychain.field.labelPlaceholder': 'Key label',
|
||||
'keychain.field.privateKeyRequired': 'Private key *',
|
||||
'keychain.field.publicKey': 'Public key',
|
||||
'keychain.field.certificatePlaceholder': 'Certificate content (optional)',
|
||||
'keychain.generate.keyType': 'Key type',
|
||||
'keychain.generate.keySize': 'Key size',
|
||||
'keychain.generate.labelPlaceholder': 'Key label',
|
||||
'keychain.generate.passphrasePlaceholder': 'Passphrase (optional)',
|
||||
'keychain.generate.savePassphrase': 'Save passphrase',
|
||||
'keychain.generate.generate': 'Generate',
|
||||
'keychain.generate.generateSave': 'Generate & Save',
|
||||
'keychain.import.dropHint': 'Drop a key file here',
|
||||
'keychain.import.importFromFile': 'Import from file',
|
||||
'keychain.import.saveKey': 'Save Key',
|
||||
'keychain.import.importedKeyLabel': 'Imported Key',
|
||||
'keychain.identity.usernameRequired': 'Username *',
|
||||
'keychain.identity.method.passwordOnly': 'Password',
|
||||
'keychain.identity.summary.password': 'Auth password',
|
||||
'keychain.identity.summary.key': 'Auth key',
|
||||
'keychain.identity.summary.certificate': 'Auth certificate',
|
||||
'keychain.identity.summary.passwordAndKey': 'Auth password and key',
|
||||
'keychain.identity.summary.passwordAndCertificate': 'Auth password and certificate',
|
||||
'keychain.identity.summary.none': 'No credentials',
|
||||
'keychain.identity.selectCredential': 'Select {kind}',
|
||||
'keychain.identity.save': 'Save',
|
||||
'keychain.identity.update': 'Update',
|
||||
'keychain.keyDialog.newTitle': 'New Key',
|
||||
'keychain.keyDialog.newDesc': 'Add a new SSH key',
|
||||
'keychain.keyDialog.editTitle': 'Edit Key',
|
||||
'keychain.keyDialog.editDesc': 'Update this SSH key',
|
||||
'keychain.keyDialog.updateKey': 'Update Key',
|
||||
|
||||
// Tabs
|
||||
'tabs.closeSessionAria': 'Close session',
|
||||
'tabs.closeLogViewAria': 'Close log view',
|
||||
'tabs.logPrefix': 'Log:',
|
||||
'tabs.logLocal': 'Local',
|
||||
'tabs.copyTab': 'Copy Tab',
|
||||
'tabs.copyTabToNewWindow': 'Copy Tab to New Window',
|
||||
'tabs.copyTabToNewWindowFailed': 'Failed to open tab in a new window',
|
||||
'tabs.closeOthers': 'Close Others',
|
||||
'tabs.closeToRight': 'Close Tabs to the Right',
|
||||
'tabs.closeAll': 'Close All',
|
||||
'keychain.edit.labelRequired': 'Label *',
|
||||
'keychain.edit.keyLabelPlaceholder': 'Key label',
|
||||
'keychain.edit.privateKeyRequired': 'Private key *',
|
||||
'keychain.edit.publicKey': 'Public key',
|
||||
'keychain.edit.certificate': 'Certificate',
|
||||
'keychain.edit.certificatePlaceholder': 'Certificate content (optional)',
|
||||
'keychain.edit.filePath': 'File path',
|
||||
'keychain.edit.keyExport': 'Key export',
|
||||
'keychain.edit.exportToHost': 'Export to host',
|
||||
|
||||
// Snippets
|
||||
'snippets.searchPlaceholder': 'Search scripts...',
|
||||
'snippets.action.newSnippet': 'New Snippet',
|
||||
'snippets.action.newPackage': 'New Script Package',
|
||||
'snippets.action.import': 'Import',
|
||||
'snippets.action.selectSnippets': 'Select snippets',
|
||||
'snippets.panel.newTitle': 'New Snippet',
|
||||
'snippets.panel.editTitle': 'Edit Snippet',
|
||||
'snippets.panel.newAutomationTitle': 'New Automation Script',
|
||||
'snippets.panel.editAutomationTitle': 'Edit Automation Script',
|
||||
'snippets.panel.resizeWidth': 'Resize panel width',
|
||||
'snippets.field.description': 'Action description',
|
||||
'snippets.field.descriptionPlaceholder': 'Example: check network load',
|
||||
'snippets.field.package': 'Add a script package',
|
||||
'snippets.field.packagePlaceholder': 'Select or create script package',
|
||||
'snippets.field.createPackage': 'Create Script Package',
|
||||
'snippets.field.scriptRequired': 'Script *',
|
||||
'snippets.scriptEditor.expand': 'Open in dialog',
|
||||
'snippets.scriptEditor.resize': 'Resize editor height',
|
||||
'snippets.scriptEditor.modalTitle': 'Edit script',
|
||||
'snippets.targets.title': 'Targets',
|
||||
'snippets.targets.add': 'Add targets',
|
||||
'snippets.targets.allHosts': 'Apply to all hosts',
|
||||
'snippets.targets.allHostsShort': 'All hosts',
|
||||
'snippets.targets.allHostsActive': 'Applies to every connectable host.',
|
||||
'snippets.history.title': 'Shell History',
|
||||
'snippets.history.subtitle': '{count} commands',
|
||||
'snippets.history.emptyTitle': 'No shell history yet',
|
||||
'snippets.history.emptyDesc': 'Commands you execute will appear here',
|
||||
'snippets.history.loadMore': 'Load more',
|
||||
'snippets.history.separator': '•',
|
||||
'snippets.history.labelPlaceholder': 'Set a label for this snippet',
|
||||
'snippets.history.saveAsSnippet': 'Save as Snippet',
|
||||
'snippets.history.time.justNow': 'just now',
|
||||
'snippets.history.time.minutesAgo': '{count}m ago',
|
||||
'snippets.history.time.hoursAgo': '{count}h ago',
|
||||
'snippets.history.time.daysAgo': '{count}d ago',
|
||||
'snippets.breadcrumb.allPackages': 'All script packages',
|
||||
'snippets.breadcrumb.separator': '›',
|
||||
'snippets.empty.title': 'Create scripts',
|
||||
'snippets.empty.desc': 'Save common commands as code snippets, or write automation scripts for repeatable operations.',
|
||||
'snippets.search.noResults.title': 'No matches',
|
||||
'snippets.search.noResults.desc': 'No scripts or script packages match "{query}". Try a different search term or clear the search to browse.',
|
||||
'snippets.section.packages': 'Script Packages',
|
||||
'snippets.section.snippets': 'Scripts',
|
||||
'snippets.kind.codeSnippet': 'Code snippet',
|
||||
'snippets.kind.automationScript': 'Automation script',
|
||||
'snippets.package.count': '{count} script(s)',
|
||||
'snippets.commandFallback': 'Command',
|
||||
'snippets.view.grid': 'Grid',
|
||||
'snippets.view.list': 'List',
|
||||
'snippets.selection.selected': '{count} selected',
|
||||
'snippets.selection.selectVisible': 'Select visible',
|
||||
'snippets.selection.deselectAll': 'Deselect all',
|
||||
'snippets.selection.exportSelected': 'Export selected ({count})',
|
||||
'snippets.export.snippet': 'Export snippet',
|
||||
'snippets.export.package': 'Export script package',
|
||||
'snippets.export.toast.empty': 'No snippets to export.',
|
||||
'snippets.export.toast.successTitle': 'Export ready',
|
||||
'snippets.export.toast.success': 'Exported {count} snippet(s).',
|
||||
'snippets.import.toast.empty': 'No importable snippets found.',
|
||||
'snippets.import.toast.failedTitle': 'Import failed',
|
||||
'snippets.import.toast.invalidDesc': 'This is not a valid Netcatty snippets file.',
|
||||
'snippets.import.toast.successTitle': 'Import completed',
|
||||
'snippets.import.toast.summary': 'Imported {imported}, overwritten {overwritten}, skipped {skipped}.',
|
||||
'snippets.import.modal.title': 'Import snippets',
|
||||
'snippets.import.modal.desc': 'Choose one or more Netcatty snippets JSON files. Netcatty will preview them before anything is imported.',
|
||||
'snippets.import.modal.exampleTitle': 'Example JSON',
|
||||
'snippets.import.modal.noFile': 'No file selected yet. Use the example format below, or a plain JSON array of snippets, then choose one or more files.',
|
||||
'snippets.import.modal.chooseFile': 'Choose file',
|
||||
'snippets.import.modal.downloadExamples': 'Download samples',
|
||||
'snippets.import.modal.multipleFiles': '{count} files selected',
|
||||
'snippets.import.modal.parsedSummary': '{files} file(s), {total} script(s), {packages} script package(s), {conflicts} duplicate command(s). Host bindings are ignored.',
|
||||
'snippets.import.modal.confirm': 'Confirm import',
|
||||
'snippets.import.conflict.title': 'Import snippets?',
|
||||
'snippets.import.conflict.desc': '{file} contains {total} snippet(s). {conflicts} duplicate command(s) already exist.',
|
||||
'snippets.import.conflict.hostBindingsNote': 'Host bindings are not imported or exported with snippets.',
|
||||
'snippets.import.conflict.skip': 'Skip duplicates',
|
||||
'snippets.import.conflict.overwrite': 'Overwrite duplicates',
|
||||
'snippets.packageDialog.title': 'New Script Package',
|
||||
'snippets.packageDialog.parent': 'Parent: {parent}',
|
||||
'snippets.packageDialog.root': 'Root',
|
||||
'snippets.packageDialog.placeholder': 'e.g. ops/maintenance',
|
||||
'snippets.packageDialog.hint': 'Use "/" to create nested script packages.',
|
||||
|
||||
// Snippets Rename Dialog
|
||||
'snippets.renameDialog.title': 'Rename Script Package',
|
||||
'snippets.renameDialog.currentPath': 'Current path: {path}',
|
||||
'snippets.renameDialog.placeholder': 'Enter new name',
|
||||
'snippets.renameDialog.error.empty': 'Script package name cannot be empty',
|
||||
'snippets.renameDialog.error.duplicate': 'A script package with this name already exists',
|
||||
'snippets.renameDialog.error.invalidChars': 'Script package name can only contain letters, numbers, hyphens, and underscores',
|
||||
|
||||
'snippets.field.noAutoRun': 'Paste only (do not auto-execute)',
|
||||
'snippets.field.multiLineRunMode': 'Multi-line run',
|
||||
'snippets.field.multiLineRunMode.paste': 'Send all at once',
|
||||
'snippets.field.multiLineRunMode.lineDelay': 'Send line by line',
|
||||
'snippets.field.multiLineRunModeHint': 'Use line by line for prompt-based logins or device macros.',
|
||||
// Snippet Shortkey
|
||||
'snippets.field.shortkey': 'Keyboard Shortcut',
|
||||
'snippets.shortkey.placeholder': 'Click to set shortcut',
|
||||
'snippets.shortkey.recording': 'Press a key combination...',
|
||||
'snippets.shortkey.hint': 'Press this shortcut in terminal to quickly send the command.',
|
||||
'snippets.shortkey.clear': 'Clear shortcut',
|
||||
'snippets.shortkey.error.systemConflict': 'This shortcut conflicts with a system shortcut',
|
||||
'snippets.shortkey.error.snippetConflict': 'This shortcut is already used by snippet: {name}',
|
||||
|
||||
'snippets.variables.dialogTitle': 'Snippet variables',
|
||||
'snippets.variables.dialogDesc': 'Fill in values for "{label}" before running.',
|
||||
'snippets.variables.hint': 'Values are inserted as-is into the script (not shell-escaped).',
|
||||
'snippets.variables.preview': 'Preview',
|
||||
'snippets.variables.placeholder': 'Enter a value',
|
||||
'snippets.variables.placeholderDefault': 'Default: {value}',
|
||||
'snippets.variables.required': 'This variable is required',
|
||||
'snippets.variables.run': 'Run',
|
||||
'snippets.field.variablesHelp': 'Use {{name}} or {{name:default}} for placeholders in the script.',
|
||||
'snippets.field.variablesDetected': 'Variables',
|
||||
'snippets.field.variableDefault': 'default {value}',
|
||||
|
||||
// Serial Port
|
||||
'serial.button': 'Serial',
|
||||
'serial.modal.title': 'Connect to Serial Port',
|
||||
'serial.modal.desc': 'Configure serial port connection settings',
|
||||
'serial.field.port': 'Serial Port',
|
||||
'serial.field.selectPort': 'Select a port...',
|
||||
'serial.field.baudRate': 'Baud Rate',
|
||||
'serial.field.dataBits': 'Data Bits',
|
||||
'serial.field.stopBits': 'Stop Bits',
|
||||
'serial.field.stopBits15Warning': '1.5 stop bits may not be supported on all Windows devices',
|
||||
'serial.field.parity': 'Parity',
|
||||
'serial.field.flowControl': 'Flow Control',
|
||||
'serial.noPorts': 'No serial ports detected. Connect a device and refresh.',
|
||||
'serial.field.customPort': 'Custom Port Path',
|
||||
'serial.field.customPortPlaceholder': 'e.g. /dev/ttys001 or COM1',
|
||||
'serial.type.hardware': 'Hardware',
|
||||
'serial.type.pseudo': 'Pseudo Terminal',
|
||||
'serial.type.custom': 'Custom',
|
||||
'serial.parity.none': 'None',
|
||||
'serial.parity.even': 'Even',
|
||||
'serial.parity.odd': 'Odd',
|
||||
'serial.parity.mark': 'Mark',
|
||||
'serial.parity.space': 'Space',
|
||||
'serial.flowControl.none': 'None',
|
||||
'serial.flowControl.xon/xoff': 'XON/XOFF (Software)',
|
||||
'serial.flowControl.rts/cts': 'RTS/CTS (Hardware)',
|
||||
'serial.field.localEcho': 'Force Local Echo',
|
||||
'serial.field.localEchoDesc': 'Echo typed characters locally (for devices without remote echo)',
|
||||
'serial.field.lineMode': 'Line Mode',
|
||||
'serial.field.lineModeDesc': 'Buffer input and send on Enter (instead of character-by-character)',
|
||||
'serial.field.charset': 'Charset',
|
||||
'serial.connectionError': 'Failed to connect to serial port',
|
||||
'serial.field.baudRatePlaceholder': 'Select or enter baud rate...',
|
||||
'serial.field.baudRateEmpty': 'Enter a custom baud rate',
|
||||
'serial.field.customBaudRate': 'Using custom baud rate',
|
||||
'serial.field.saveConfig': 'Save Configuration',
|
||||
'serial.field.saveConfigDesc': 'Save this serial configuration to hosts for quick access',
|
||||
'serial.field.configLabel': 'Configuration Name',
|
||||
'serial.field.configLabelPlaceholder': 'e.g. Arduino Uno',
|
||||
'serial.connectAndSave': 'Connect & Save',
|
||||
'serial.edit.title': 'Serial Port Settings',
|
||||
|
||||
// Keyboard Interactive Authentication (2FA/MFA)
|
||||
'keyboard.interactive.title': 'Authentication Required',
|
||||
'keyboard.interactive.desc': 'The server requires additional authentication.',
|
||||
'keyboard.interactive.descWithHost': 'The server {hostname} requires additional authentication.',
|
||||
'keyboard.interactive.response': 'Response',
|
||||
'keyboard.interactive.enterCode': 'Enter verification code',
|
||||
'keyboard.interactive.enterResponse': 'Enter response',
|
||||
'keyboard.interactive.submit': 'Submit',
|
||||
'keyboard.interactive.verifying': 'Verifying...',
|
||||
'keyboard.interactive.savePassword': 'Save password',
|
||||
|
||||
// Passphrase Modal for encrypted SSH keys
|
||||
'passphrase.title': 'SSH Key Passphrase',
|
||||
'passphrase.desc': 'Enter the passphrase for {keyName}',
|
||||
'passphrase.descWithHost': 'Enter the passphrase for {keyName} to connect to {hostname}',
|
||||
'passphrase.label': 'Passphrase',
|
||||
'passphrase.keyPath': 'Key',
|
||||
'passphrase.unlock': 'Unlock',
|
||||
'passphrase.unlocking': 'Unlocking...',
|
||||
'passphrase.skip': 'Skip',
|
||||
'passphrase.remember': 'Remember this passphrase',
|
||||
|
||||
// Text Editor
|
||||
'sftp.editor.wordWrap': 'Word Wrap',
|
||||
'sftp.editor.maximize': 'Maximize',
|
||||
'sftp.editor.unsavedTitle': 'Unsaved changes',
|
||||
'sftp.editor.unsavedMessage': '{fileName} has unsaved changes. Save before closing?',
|
||||
'sftp.editor.discardChanges': 'Discard',
|
||||
'sftp.editor.saveAndClose': 'Save and close',
|
||||
'sftp.editor.quitBlockedByDirty': 'Unsaved editors — please save or discard before quitting',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,792 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const enVaultMessages: Messages = {
|
||||
// Notes
|
||||
'notes.search.placeholder': 'Search notes, hosts, tags...',
|
||||
'notes.action.newNote': 'New Note',
|
||||
'notes.action.newGroup': 'New Folder',
|
||||
'notes.action.importMarkdown': 'Import Markdown',
|
||||
'notes.action.exportNote': 'Export Note',
|
||||
'notes.action.exportGroup': 'Export Folder',
|
||||
'notes.action.exportAll': 'Export Notes',
|
||||
'notes.import.toast.success': 'Imported {count} note(s)',
|
||||
'notes.import.toast.noNotes': 'No Markdown files were imported.',
|
||||
'notes.import.toast.failed': 'Failed to import Markdown files.',
|
||||
'notes.import.toast.inProgress': 'A Markdown import is already in progress.',
|
||||
'notes.import.toast.skipped': 'Skipped {count} unsupported file(s).',
|
||||
'notes.export.toast.success': 'Exported {count} note(s)',
|
||||
'notes.export.toast.empty': 'No notes to export.',
|
||||
'notes.export.toast.failed': 'Failed to export notes.',
|
||||
'notes.codeBlock.copied': 'Copied',
|
||||
'notes.codeBlock.copyFailed': 'Failed to copy code block.',
|
||||
'notes.group.prompt': 'Folder path',
|
||||
'notes.group.all': 'Notebook',
|
||||
'notes.group.ungrouped': 'Ungrouped',
|
||||
'notes.section.notebooks': 'Notebook',
|
||||
'notes.section.notes': 'Notes',
|
||||
'notes.mode.edit': 'Edit',
|
||||
'notes.mode.split': 'Split',
|
||||
'notes.mode.preview': 'Preview',
|
||||
'notes.title.placeholder': 'Note title',
|
||||
'notes.editor.placeholder': 'Write Markdown notes here...',
|
||||
'notes.preview.empty': 'Nothing to preview yet.',
|
||||
'notes.empty.title': 'No notes yet',
|
||||
'notes.empty.desc': 'Create Markdown notes for runbooks, reminders, and server handoff details.',
|
||||
'notes.empty.group': 'No notes in this notebook.',
|
||||
'notes.search.noResults': 'No notes found.',
|
||||
|
||||
// Vault import
|
||||
'vault.import.title': 'Add data to your vault',
|
||||
'vault.import.desc':
|
||||
'Transfer your connections from popular clients. Select a file format to start the migration.',
|
||||
'vault.import.chooseFormat': 'Select a file format',
|
||||
'vault.import.csv.tip': 'Bulk import: use the CSV template.',
|
||||
'vault.import.csv.downloadTemplate': 'Download CSV template',
|
||||
'vault.import.toast.start': 'Importing from {format}...',
|
||||
'vault.import.toast.completedTitle': 'Import completed',
|
||||
'vault.import.toast.failedTitle': 'Import failed',
|
||||
'vault.import.toast.noEntries': 'No importable entries found in {format}.',
|
||||
'vault.import.toast.noNewHosts': 'No new hosts imported from {format}.',
|
||||
'vault.import.toast.summary':
|
||||
'Imported {count} hosts (skipped {skipped}, duplicates {duplicates}).',
|
||||
'vault.import.toast.firstIssue': 'First issue: {issue}',
|
||||
'vault.import.sshConfig.chooseMode': 'Choose how to import your SSH config file.',
|
||||
'vault.import.sshConfig.modeQuestion': 'How would you like to import?',
|
||||
'vault.import.sshConfig.importOnly': 'Import Only',
|
||||
'vault.import.sshConfig.importOnlyDesc': 'One-time import. Changes won\'t sync back to the file.',
|
||||
'vault.import.sshConfig.managed': 'Managed Sync',
|
||||
'vault.import.sshConfig.managedDesc': 'Keep in sync. Changes will be saved back to the file.',
|
||||
'vault.import.sshConfig.managedGroup': 'ssh config',
|
||||
'vault.import.sshConfig.managedSuccess': 'Imported {count} hosts. File is now managed.',
|
||||
'vault.import.sshConfig.alreadyManaged': 'This file is already being managed.',
|
||||
'vault.import.sshConfig.alreadyManagedDesc': 'This file is already managed under group "{group}". Remove the existing managed source first if you want to re-import.',
|
||||
'vault.import.sshConfig.noFilePath': 'Cannot manage this file.',
|
||||
'vault.import.sshConfig.noFilePathDesc': 'Unable to determine the file path. Managed sync requires access to the file system.',
|
||||
|
||||
// Known Hosts
|
||||
'knownHosts.search.placeholder': 'Search known hosts...',
|
||||
'knownHosts.action.scanSystem': 'Scan System',
|
||||
'knownHosts.action.importFile': 'Import File',
|
||||
'knownHosts.action.browseFile': 'Browse File',
|
||||
'knownHosts.empty.title': 'No Known Hosts',
|
||||
'knownHosts.empty.desc':
|
||||
"Known hosts are SSH servers you've connected to before. Import from your system's known_hosts file to get started.",
|
||||
'knownHosts.results.showingLimited':
|
||||
'Showing {shown} of {total} hosts. Use search to find specific hosts.',
|
||||
'knownHosts.toast.scanUnavailable': 'System scan is unavailable on this platform.',
|
||||
'knownHosts.toast.scanNoFile': 'No system known_hosts file found.',
|
||||
'knownHosts.toast.scanNoEntries': 'No usable entries found in known_hosts.',
|
||||
'knownHosts.toast.scanImported': 'Imported {count} new hosts.',
|
||||
'knownHosts.toast.scanNoNew': 'No new hosts found.',
|
||||
'knownHosts.toast.scanFailed': 'Failed to scan system known_hosts.',
|
||||
|
||||
// Port Forwarding
|
||||
'pf.empty.title': 'Set up port forwarding',
|
||||
'pf.empty.desc': 'Save port forwarding to access databases, web apps, and other services.',
|
||||
'pf.title': 'Port Forwarding',
|
||||
'pf.rulesCount': '{count} rules',
|
||||
'pf.wizard.editTitle': 'Edit Port Forwarding',
|
||||
'pf.wizard.newTitle': 'New Port Forwarding',
|
||||
'pf.wizard.saveChanges': 'Save Changes',
|
||||
'pf.wizard.done': 'Done',
|
||||
'pf.wizard.continue': 'Continue',
|
||||
'pf.wizard.cancel': 'Cancel',
|
||||
'pf.wizard.skipWizard': 'Skip wizard',
|
||||
'pf.error.hostNotFound': 'Host not found',
|
||||
'pf.toast.titleWithLabel': 'Port Forwarding: {label}',
|
||||
'pf.type.local': 'Local',
|
||||
'pf.type.remote': 'Remote',
|
||||
'pf.type.dynamic': 'Dynamic',
|
||||
'pf.type.menu.local': 'Local Forwarding',
|
||||
'pf.type.menu.remote': 'Remote Forwarding',
|
||||
'pf.type.menu.dynamic': 'Dynamic Forwarding',
|
||||
'pf.type.local.desc': "Local forwarding lets you access a remote server's listening port as though it were local.",
|
||||
'pf.type.remote.desc': 'Remote forwarding opens a port on the remote machine and forwards connections to the local (current) host.',
|
||||
'pf.type.dynamic.desc': 'Dynamic port forwarding turns Netcatty into a SOCKS proxy server.',
|
||||
'pf.wizard.type.title': 'Select the port forwarding type:',
|
||||
'pf.wizard.localConfig.title': 'Set the local port and binding address:',
|
||||
'pf.wizard.localConfig.desc': 'This port will be open on the local (current) device, and it will receive the traffic.',
|
||||
'pf.wizard.localConfig.localPort': 'Local port number *',
|
||||
'pf.wizard.bindAddress': 'Bind address',
|
||||
'pf.wizard.remoteHost.title': 'Select the remote host:',
|
||||
'pf.wizard.remoteHost.desc': 'Select a host where the port will be open. Traffic from this port will be forwarded to the destination host.',
|
||||
'pf.wizard.remoteConfig.title': 'Set the port and binding address:',
|
||||
'pf.wizard.remoteConfig.desc': 'Traffic will be forwarded from the specified port and interface address of the selected host.',
|
||||
'pf.wizard.remoteConfig.remotePort': 'Remote port number *',
|
||||
'pf.wizard.destination.title': 'Select the destination host:',
|
||||
'pf.wizard.destination.desc.local': 'Enter the remote destination that you want to access through the tunnel.',
|
||||
'pf.wizard.destination.desc.remote': 'The destination address and port where the traffic will be forwarded.',
|
||||
'pf.wizard.destination.address': 'Destination address *',
|
||||
'pf.wizard.destination.addressPlaceholder': 'e.g. 127.0.0.1 or 192.168.1.100',
|
||||
'pf.wizard.destination.port': 'Destination port number *',
|
||||
'pf.wizard.sshServer.title': 'Select the SSH server:',
|
||||
'pf.wizard.sshServer.desc.dynamic': 'Select the SSH server that will act as your SOCKS proxy.',
|
||||
'pf.wizard.sshServer.desc.default': 'Select the SSH server that will tunnel your traffic to the destination.',
|
||||
'pf.wizard.label.title': 'Select the label:',
|
||||
'pf.wizard.label.placeholder.dynamic': 'e.g. SOCKS Proxy',
|
||||
'pf.wizard.label.placeholder.default': 'e.g. MySQL Production',
|
||||
'pf.wizard.label.placeholder.remoteRule': 'e.g. Remote Rule',
|
||||
'pf.wizard.placeholders.portExample': 'e.g. {port}',
|
||||
'pf.action.newForwarding': 'New Forwarding',
|
||||
'pf.form.labelPlaceholder': 'Rule label',
|
||||
'pf.form.intermediateHost': 'Intermediate host *',
|
||||
'pf.form.createRule': 'Create Rule',
|
||||
'pf.form.openWizard': 'Open Wizard',
|
||||
'pf.form.openWizardTitle': 'Open Port Forwarding Wizard',
|
||||
'pf.view.grid': 'Grid',
|
||||
'pf.view.list': 'List',
|
||||
'pf.rule.summary.local': 'Local {bindAddress}:{localPort} -> SSH tunnel -> {remoteHost}:{remotePort}',
|
||||
'pf.rule.summary.remote': 'Remote {bindAddress}:{localPort} -> SSH tunnel -> local {remoteHost}:{remotePort}',
|
||||
'pf.rule.summary.dynamic': 'SOCKS on {bindAddress}:{localPort}',
|
||||
'pf.tooltip.relayHost': 'Relay Host',
|
||||
'pf.tooltip.hostLabel': 'Host',
|
||||
'pf.tooltip.hostAddress': 'Address',
|
||||
'pf.tooltip.noHost': 'No relay host configured',
|
||||
'pf.tooltip.localDesc': 'Local port forwarding: Access remote services through SSH tunnel',
|
||||
'pf.tooltip.remoteDesc': 'Remote port forwarding: Expose local services to remote host',
|
||||
'pf.tooltip.dynamicDesc': 'Dynamic SOCKS proxy: Route traffic through SSH tunnel',
|
||||
'pf.deleteActive.title': 'Delete Active Port Forwarding?',
|
||||
'pf.deleteActive.desc': 'This port forwarding rule "{label}" is currently active. Deleting it will stop the tunnel first.',
|
||||
'pf.deleteActive.confirm': 'Stop and Delete',
|
||||
'pf.form.autoStart': 'Auto Start',
|
||||
'pf.form.autoStartDesc': 'Automatically start this rule when the app launches',
|
||||
|
||||
// SFTP
|
||||
'sftp.newFolder': 'New Folder',
|
||||
'sftp.newFile': 'New File',
|
||||
'sftp.filter': 'Filter',
|
||||
'sftp.filter.placeholder': 'Filter by filename...',
|
||||
'sftp.bookmark.add': 'Bookmark this path',
|
||||
'sftp.bookmark.remove': 'Remove bookmark',
|
||||
'sftp.bookmark.list': 'Bookmarked paths',
|
||||
'sftp.bookmark.addGlobal': '+Global',
|
||||
'sftp.bookmark.addGlobalTooltip': 'Save as global bookmark (shared across all hosts)',
|
||||
'sftp.bookmark.empty': 'No bookmarks yet',
|
||||
'sftp.columns.name': 'Name',
|
||||
'sftp.columns.modified': 'Modified',
|
||||
'sftp.columns.size': 'Size',
|
||||
'sftp.columns.kind': 'Kind',
|
||||
'sftp.columns.actions': 'Actions',
|
||||
'sftp.emptyDirectory': 'Empty directory',
|
||||
'sftp.nav.up': 'Go up',
|
||||
'sftp.nav.home': 'Go to home',
|
||||
'sftp.nav.refresh': 'Refresh',
|
||||
'sftp.upload': 'Upload',
|
||||
'sftp.uploadFiles': 'Upload files',
|
||||
'sftp.uploadFolder': 'Upload folder',
|
||||
'sftp.dragDropToUpload': 'Drag and drop files here to upload',
|
||||
'sftp.retry': 'Retry',
|
||||
'sftp.context.open': 'Open',
|
||||
'sftp.context.navigateTo': 'Navigate to',
|
||||
'sftp.context.moveTo': 'Move to...',
|
||||
'sftp.context.moveToParent': 'Move to parent directory',
|
||||
'sftp.moveTo.title': 'Move to directory',
|
||||
'sftp.moveTo.placeholder': 'Enter target directory path',
|
||||
'sftp.moveTo.confirm': 'Move',
|
||||
'sftp.moveTo.pathNotFound': 'Directory not found or inaccessible',
|
||||
'sftp.context.download': 'Download',
|
||||
'sftp.context.copyToOtherPane': 'Copy to other pane',
|
||||
'sftp.copyCurrentPath': 'Copy current path',
|
||||
'sftp.copyCurrentPath.success': 'Current path copied',
|
||||
'sftp.copyCurrentPath.error': 'Could not copy current path',
|
||||
'sftp.viewMode.label': 'View mode',
|
||||
'sftp.viewMode.list': 'List view',
|
||||
'sftp.viewMode.tree': 'Tree view',
|
||||
'sftp.viewMode.switchToList': 'Switch to list view',
|
||||
'sftp.viewMode.switchToTree': 'Switch to tree view',
|
||||
'sftp.tree.loadError': 'Failed to load directory',
|
||||
'sftp.tree.loading': 'Loading...',
|
||||
'sftp.kind.folder': 'Folder',
|
||||
'sftp.context.rename': 'Rename',
|
||||
'sftp.context.permissions': 'Permissions',
|
||||
'sftp.context.delete': 'Delete',
|
||||
'sftp.context.refresh': 'Refresh',
|
||||
'sftp.context.uploadFiles': 'Upload File(s)...',
|
||||
'sftp.context.uploadFilesHere': 'Upload File(s) Here...',
|
||||
'sftp.context.uploadFolder': 'Upload Folder...',
|
||||
'sftp.context.uploadFolderHere': 'Upload Folder Here...',
|
||||
'sftp.context.downloadSelected': 'Download selected ({count})',
|
||||
'sftp.context.deleteSelected': 'Delete selected ({count})',
|
||||
'sftp.dropFilesHere': 'Drop files here',
|
||||
'sftp.itemsCount': '{count} items',
|
||||
'sftp.selectedCount': '{count} selected',
|
||||
'sftp.path.doubleClickToEdit': 'Double-click to edit path',
|
||||
'sftp.showHiddenPaths': 'Hidden paths',
|
||||
'sftp.task.waiting': 'Waiting...',
|
||||
'sftp.transfer.preparing': 'preparing...',
|
||||
'sftp.status.loading': 'Loading...',
|
||||
'sftp.status.uploading': 'Uploading...',
|
||||
'sftp.status.ready': 'Ready',
|
||||
'sftp.transfers': 'Transfers',
|
||||
'sftp.transfers.active': '{count} active',
|
||||
'sftp.transfers.clearCompleted': 'Clear completed',
|
||||
'sftp.transfers.calculatingTotal': 'Calculating total size...',
|
||||
'sftp.transfers.filesCount': '{count} files',
|
||||
'sftp.transfers.filesProgress': '{current}/{total} files',
|
||||
'sftp.transfers.expandChildren': 'Show files',
|
||||
'sftp.transfers.collapseChildren': 'Hide files',
|
||||
'sftp.transfers.expandChildList': 'Show detail',
|
||||
'sftp.transfers.collapseChildList': 'Hide',
|
||||
'sftp.transfers.retryAction': 'Retry',
|
||||
'sftp.transfers.dismissAction': 'Dismiss',
|
||||
'sftp.transfers.openTargetFolder': 'Open target folder',
|
||||
'sftp.transfers.openTargetFolderError': 'Could not open target folder',
|
||||
'sftp.transfers.copyTargetPath': 'Copy target path',
|
||||
'sftp.transfers.copyTargetPathSuccess': 'Target path copied',
|
||||
'sftp.transfers.copyTargetPathError': 'Could not copy target path',
|
||||
'sftp.transfers.resizeNameColumn': 'Resize file name column',
|
||||
'sftp.transfers.dragToResize': 'Drag to resize',
|
||||
'sftp.goUp': 'Go up',
|
||||
'sftp.goToTerminalCwd': 'Go to terminal directory',
|
||||
'sftp.followTerminalCwd': 'Follow terminal directory',
|
||||
'sftp.followTerminalCwd.enable': 'Enable follow terminal directory',
|
||||
'sftp.followTerminalCwd.disable': 'Disable follow terminal directory',
|
||||
'sftp.encoding.label': 'Filename Encoding',
|
||||
'sftp.encoding.auto': 'Auto',
|
||||
'sftp.encoding.utf8': 'UTF-8',
|
||||
'sftp.encoding.gb18030': 'GB18030',
|
||||
'sftp.goHome': 'Go to home',
|
||||
'sftp.folderName': 'Folder name',
|
||||
'sftp.folderName.placeholder': 'Enter folder name',
|
||||
'sftp.fileName': 'File name',
|
||||
'sftp.fileName.placeholder': 'Enter file name',
|
||||
'sftp.prompt.newFolderName': 'New folder name?',
|
||||
'sftp.rename.title': 'Rename',
|
||||
'sftp.rename.newName': 'New name',
|
||||
'sftp.rename.placeholder': 'Enter new name',
|
||||
'sftp.confirm.deleteOne': 'Delete "{name}"?',
|
||||
'sftp.deleteConfirm.single': 'Delete "{name}"?',
|
||||
'sftp.deleteConfirm.title': 'Delete {count} item(s)?',
|
||||
'sftp.deleteConfirm.desc': 'This action cannot be undone. The following will be deleted:',
|
||||
'sftp.deleteConfirm.descSingle': 'This action cannot be undone.',
|
||||
'sftp.deleteConfirm.host': 'Host',
|
||||
'sftp.deleteConfirm.path': 'Path',
|
||||
'sftp.error.loadFailed': 'Failed to load directory',
|
||||
'sftp.error.downloadFailed': 'Download failed',
|
||||
'sftp.error.uploadFailed': 'Upload failed',
|
||||
'sftp.error.deleteFailed': 'Delete failed',
|
||||
'sftp.error.createFolderFailed': 'Failed to create folder',
|
||||
'sftp.error.createFileFailed': 'Failed to create file',
|
||||
'sftp.error.invalidFileName': 'Filename contains invalid characters: {chars}',
|
||||
'sftp.error.reservedName': 'This filename is reserved by the system',
|
||||
'sftp.overwrite.title': 'File Already Exists',
|
||||
'sftp.overwrite.desc': 'A file named "{name}" already exists. Do you want to replace it?',
|
||||
'sftp.overwrite.confirm': 'Replace',
|
||||
'sftp.error.renameFailed': 'Failed to rename',
|
||||
'sftp.picker.title': 'Select Host',
|
||||
'sftp.picker.desc': 'Pick a host for the {side} pane',
|
||||
'sftp.picker.searchPlaceholder': 'Search hosts...',
|
||||
'sftp.picker.local.title': 'Local filesystem',
|
||||
'sftp.picker.local.desc': 'Browse local files',
|
||||
'sftp.picker.local.badge': 'Local',
|
||||
'sftp.picker.connected.section': 'Connected',
|
||||
'sftp.picker.connected.badge': 'Live',
|
||||
'sftp.picker.connected.connecting': 'Connecting',
|
||||
'sftp.picker.noMatch': 'No matching hosts',
|
||||
'sftp.permissions.title': 'Edit Permissions',
|
||||
'sftp.permissions.owner': 'Owner',
|
||||
'sftp.permissions.group': 'Group',
|
||||
'sftp.permissions.others': 'Others',
|
||||
'sftp.permissions.octal': 'Octal',
|
||||
'sftp.permissions.symbolic': 'Symbolic',
|
||||
'sftp.permissions.success': 'Permissions updated successfully',
|
||||
'sftp.permissions.failed': 'Failed to update permissions',
|
||||
'sftp.pane.local': 'Local',
|
||||
'sftp.pane.remote': 'Remote',
|
||||
'sftp.pane.selectHost': 'Select host',
|
||||
'sftp.pane.selectHostToStart': 'Select a host to start',
|
||||
'sftp.pane.chooseFilesystem': 'Choose a local or remote filesystem to browse',
|
||||
'sftp.tabs.addTab': 'Add new tab',
|
||||
'sftp.tabs.closeTab': 'Close tab',
|
||||
'sftp.tabs.newTab': 'New Tab',
|
||||
'sftp.tabs.copyDefaultPath': 'Copy tab (default path)',
|
||||
'sftp.tabs.copyCurrentPath': 'Copy and go to current path',
|
||||
'sftp.conflict.title': 'File Conflict',
|
||||
'sftp.conflict.desc': 'A file with the same name already exists at the destination',
|
||||
'sftp.conflict.alreadyExistsSuffix': 'already exists',
|
||||
'sftp.conflict.existingFile': 'Existing file',
|
||||
'sftp.conflict.newFile': 'New file',
|
||||
'sftp.conflict.size': 'Size:',
|
||||
'sftp.conflict.modified': 'Modified:',
|
||||
'sftp.conflict.applyToAll': 'Apply this action to all {count} remaining conflicts',
|
||||
'sftp.conflict.action.stop': 'Stop',
|
||||
'sftp.conflict.action.skip': 'Skip',
|
||||
'sftp.conflict.action.keepBoth': 'Keep Both',
|
||||
'sftp.conflict.action.duplicate': 'Duplicate',
|
||||
'sftp.conflict.action.merge': 'Merge',
|
||||
'sftp.conflict.action.replace': 'Replace',
|
||||
|
||||
// SFTP Upload Phases
|
||||
'sftp.upload.phase.compressing': 'Compressing',
|
||||
'sftp.upload.phase.uploading': 'Uploading',
|
||||
'sftp.upload.phase.extracting': 'Extracting',
|
||||
'sftp.upload.phase.compressed': 'Compressed',
|
||||
|
||||
// SFTP File Opener
|
||||
'sftp.context.copyPath': 'Copy file path',
|
||||
'sftp.context.openWithDefault': 'Open with system default',
|
||||
'sftp.context.openWith': 'Open with...',
|
||||
'sftp.context.edit': 'Edit',
|
||||
'sftp.context.preview': 'Preview',
|
||||
'sftp.opener.title': 'Open with',
|
||||
'sftp.opener.desc': 'Choose an application to open this file',
|
||||
'sftp.opener.builtInEditor': 'Built-in Editor',
|
||||
'sftp.opener.editDescription': 'Edit text files',
|
||||
'sftp.opener.builtInImageViewer': 'Built-in Image Viewer',
|
||||
'sftp.opener.previewDescription': 'Preview images',
|
||||
'sftp.opener.systemApp': 'Choose Application...',
|
||||
'sftp.opener.systemAppDescription': 'Select an application from your computer',
|
||||
'sftp.opener.onlySystemApp': 'This file can only be opened with an external application',
|
||||
'sftp.opener.noAppsAvailable': 'No applications available',
|
||||
'sftp.opener.noExtension': 'files without extension',
|
||||
'sftp.opener.setDefault': 'Always use this for {ext} files',
|
||||
'sftp.opener.confirmTitle': 'Set as Default?',
|
||||
'sftp.opener.confirmDescription': 'Do you want to always use {app} for {ext} files?',
|
||||
'sftp.opener.yesRemember': 'Yes, remember this choice',
|
||||
'sftp.opener.justOnce': 'Just this once',
|
||||
'sftp.opener.confirm.title': 'Set Default Application',
|
||||
'sftp.opener.confirm.desc': 'Do you want to always open .{ext} files with this application?',
|
||||
'sftp.editor.title': 'Text Editor',
|
||||
'sftp.editor.save': 'Save to Remote',
|
||||
'sftp.editor.saving': 'Saving...',
|
||||
'sftp.editor.saved': 'Saved successfully',
|
||||
'sftp.editor.saveFailed': 'Failed to save file',
|
||||
'sftp.editor.unsavedChanges': 'You have unsaved changes. Close anyway?',
|
||||
'sftp.editor.syntaxHighlight': 'Syntax Highlighting',
|
||||
'sftp.preview.title': 'Image Preview',
|
||||
'sftp.preview.zoomIn': 'Zoom In',
|
||||
'sftp.preview.zoomOut': 'Zoom Out',
|
||||
'sftp.preview.resetZoom': 'Reset Zoom',
|
||||
'sftp.preview.fitToWindow': 'Fit to Window',
|
||||
|
||||
// Settings > SFTP File Associations
|
||||
'settings.tab.sftpFileAssociations': 'SFTP',
|
||||
'settings.sftp.transferConcurrency': 'Transfer Concurrency',
|
||||
'settings.sftp.transferConcurrency.desc': 'Number of files to transfer in parallel when uploading or downloading folders. Higher values may improve speed but can overwhelm some servers.',
|
||||
'settings.sftp.defaultOpener': 'Default File Opener',
|
||||
'settings.sftp.defaultOpener.desc': 'Choose the default application for opening files without a specific file association',
|
||||
'settings.sftp.defaultOpener.ask': 'Always ask',
|
||||
'settings.sftp.defaultOpener.askDesc': 'Show a dialog to choose an application each time',
|
||||
'settings.sftp.defaultOpener.builtInDesc': 'Open text files in the built-in editor by default',
|
||||
'settings.sftp.defaultOpener.systemApp': 'Choose Application...',
|
||||
'settings.sftp.defaultOpener.systemAppDesc': 'Open files with a specific application by default',
|
||||
'settings.sftpFileAssociations.title': 'SFTP File Associations',
|
||||
'settings.sftpFileAssociations.desc': 'Configure default applications for opening files by extension',
|
||||
'settings.sftpFileAssociations.extension': 'Extension',
|
||||
'settings.sftpFileAssociations.application': 'Application',
|
||||
'settings.sftpFileAssociations.noAssociations': 'No file associations configured',
|
||||
'settings.sftpFileAssociations.remove': 'Remove',
|
||||
'settings.sftpFileAssociations.removeConfirm': 'Remove association for .{ext}?',
|
||||
|
||||
// Settings > SFTP Behavior
|
||||
'settings.sftp.doubleClickBehavior': 'Double-click behavior',
|
||||
'settings.sftp.doubleClickBehavior.desc': 'Choose the action when double-clicking a file in SFTP View',
|
||||
'settings.sftp.doubleClickBehavior.open': 'Open file',
|
||||
'settings.sftp.doubleClickBehavior.transfer': 'Transfer to other pane',
|
||||
'settings.sftp.doubleClickBehavior.openDesc': 'Open the file in the default application',
|
||||
'settings.sftp.doubleClickBehavior.transferDesc': 'Transfer the file to the other pane\'s active host',
|
||||
'settings.sshDeepLink.title': 'SSH and Telnet links',
|
||||
'settings.sshDeepLink.enable': 'Open ssh:// and telnet:// links with Netcatty',
|
||||
'settings.sshDeepLink.enableDesc': 'Allow Netcatty to handle ssh:// and telnet:// links from browsers and other apps.',
|
||||
'settings.jmsDeepLink.title': 'JumpServer Links',
|
||||
'settings.jmsDeepLink.enable': 'Handle jms:// links',
|
||||
'settings.jmsDeepLink.enableDesc': 'Register Netcatty as the handler for JumpServer jms:// client links. May conflict with the official JumpServer client.',
|
||||
|
||||
// Settings > SFTP Auto Sync
|
||||
'settings.sftp.autoSync': 'Auto-sync to remote',
|
||||
'settings.sftp.autoSync.desc': 'Automatically sync file changes back to the remote server when opening files with external applications',
|
||||
'settings.sftp.autoSync.enable': 'Enable auto-sync',
|
||||
'settings.sftp.autoSync.enableDesc': 'When you save a file in an external application, changes will be automatically uploaded to the remote server',
|
||||
|
||||
// Settings > SFTP Auto Open Sidebar
|
||||
'settings.sftp.autoOpenSidebar': 'Auto-open sidebar on connect',
|
||||
'settings.sftp.autoOpenSidebar.desc': 'Automatically open the SFTP file browser sidebar when connecting to a host',
|
||||
'settings.sftp.autoOpenSidebar.enable': 'Enable auto-open sidebar',
|
||||
'settings.sftp.autoOpenSidebar.enableDesc': 'The SFTP sidebar will open automatically when a terminal session connects to a remote host',
|
||||
'settings.sftp.followTerminalCwd': 'Follow terminal directory',
|
||||
'settings.sftp.followTerminalCwd.desc': 'Automatically sync the sidebar SFTP browser with the terminal working directory (toggle in toolbar)',
|
||||
'settings.sftp.followTerminalCwd.enable': 'Enable follow terminal directory by default',
|
||||
'settings.sftp.followTerminalCwd.enableDesc': 'When the SFTP sidebar is open, follow mode stays on by default and updates after terminal cd commands',
|
||||
|
||||
'settings.sftp.defaultViewMode': 'Default View Mode',
|
||||
'settings.sftp.defaultViewMode.desc': 'Choose the default view mode when opening a new SFTP tab. Per-host preferences override this setting.',
|
||||
'settings.sftp.defaultViewMode.list': 'List View',
|
||||
'settings.sftp.defaultViewMode.listDesc': 'Display files in a flat list for the current directory',
|
||||
'settings.sftp.defaultViewMode.tree': 'Tree View',
|
||||
'settings.sftp.defaultViewMode.treeDesc': 'Display files in a hierarchical tree structure',
|
||||
|
||||
'sftp.autoSync.success': 'File synced to remote: {fileName}',
|
||||
'sftp.autoSync.error': 'Failed to sync file: {error}',
|
||||
|
||||
// SFTP Folder Upload Progress
|
||||
'sftp.upload.progress': 'Uploading {current} of {total} files...',
|
||||
'sftp.upload.uploading': 'Uploading...',
|
||||
'sftp.upload.compressing': 'Compressing...',
|
||||
'sftp.upload.extracting': 'Extracting...',
|
||||
'sftp.upload.scanning': 'Scanning files...',
|
||||
'sftp.upload.completed': 'Completed',
|
||||
'sftp.upload.compressed': 'Compressed Transfer',
|
||||
'sftp.upload.currentFile': 'Current: {fileName}',
|
||||
'sftp.upload.cancelled': 'Upload cancelled',
|
||||
'sftp.upload.cancel': 'Cancel',
|
||||
'sftp.upload.completedToPath': 'Uploaded to {path}',
|
||||
|
||||
// SFTP Download
|
||||
'sftp.download.completed': 'Downloaded',
|
||||
'sftp.download.cancelled': 'Download cancelled',
|
||||
|
||||
// SFTP Reconnecting
|
||||
'sftp.reconnecting.title': 'Reconnecting...',
|
||||
'sftp.reconnecting.desc': 'Connection lost, attempting to reconnect',
|
||||
'sftp.reconnected': 'Connection restored',
|
||||
'sftp.error.reconnectFailed': 'Failed to reconnect. Please try again.',
|
||||
'sftp.error.connectionLostManual': 'Connection lost. Please reconnect manually.',
|
||||
'sftp.error.connectionLostReconnecting': 'Connection lost. Reconnecting...',
|
||||
'sftp.error.sessionLost': 'SFTP session lost. Please reconnect.',
|
||||
|
||||
// Settings > SFTP Show Hidden Files
|
||||
'settings.sftp.showHiddenFiles': 'Show hidden files',
|
||||
'settings.sftp.showHiddenFiles.desc': 'Display hidden files (dotfiles on Unix/macOS and files with the hidden attribute on Windows) in the SFTP file browser.',
|
||||
'settings.sftp.showHiddenFiles.enable': 'Show hidden files',
|
||||
'settings.sftp.showHiddenFiles.enableDesc': 'Display hidden files when browsing both local and remote filesystems',
|
||||
|
||||
// Settings > SFTP Compressed Upload
|
||||
'settings.sftp.compressedUpload': 'Folder Compression Transfer',
|
||||
'settings.sftp.compressedUpload.desc': 'Compress folders before uploading to significantly reduce transfer time.',
|
||||
'settings.sftp.compressedUpload.enable': 'Enable folder compression',
|
||||
'settings.sftp.compressedUpload.enableDesc': 'Automatically compress folders using tar before transfer. Requires tar support on the server. Falls back to regular transfer if not available.',
|
||||
|
||||
// Quick Switcher
|
||||
'qs.search.placeholder': 'Search hosts or tabs',
|
||||
'qs.jumpTo': 'Jump To',
|
||||
'qs.localTerminal': 'Local Terminal',
|
||||
'qs.localShells': 'Local Shells',
|
||||
'qs.default': 'Default',
|
||||
|
||||
// Select Host panel
|
||||
'selectHost.title': 'Select Host',
|
||||
'selectHost.noHostsFound': 'No hosts found',
|
||||
'selectHost.newHost': 'New Host',
|
||||
'selectHost.continue': 'Continue',
|
||||
'selectHost.continueWithCount': 'Continue ({count} selected)',
|
||||
|
||||
// Quick Connect
|
||||
'quickConnect.knownHost.title': 'Are you sure you want to connect?',
|
||||
'quickConnect.knownHost.authenticity': 'The authenticity of {hostname} can not be established.',
|
||||
'quickConnect.knownHost.fingerprintLabel': '{keyType} fingerprint is SHA256:',
|
||||
'quickConnect.knownHost.addQuestion': 'Do you want to add it to the list of known hosts?',
|
||||
'quickConnect.knownHost.addAndContinue': 'Add and continue',
|
||||
'quickConnect.addKey': 'Add key',
|
||||
'quickConnect.warning.unparsedOptions': 'Some SSH arguments were ignored: {options}',
|
||||
|
||||
// Terminal
|
||||
'terminal.connectionErrorTitle': 'Connection Error',
|
||||
|
||||
// Protocol select dialog
|
||||
'protocolSelect.chooseProtocol': 'Choose protocol',
|
||||
'protocolSelect.port': 'port:',
|
||||
|
||||
// Host Details
|
||||
'hostDetails.title.details': 'Host Details',
|
||||
'hostDetails.title.new': 'New Host',
|
||||
'vault.panel.resizeWidth': 'Resize panel width',
|
||||
'hostDetails.saveAria': 'Save',
|
||||
'hostDetails.section.address': 'Address',
|
||||
'hostDetails.hostname.placeholder': 'IP or Hostname',
|
||||
'hostDetails.section.general': 'General',
|
||||
'hostDetails.section.sftp': 'SFTP Settings',
|
||||
'hostDetails.sftp.sudo': 'Sudo Mode',
|
||||
'hostDetails.sftp.sudo.desc': 'Automatically acquire Root privileges using stored password',
|
||||
'hostDetails.sftp.sudo.passwordWarning': 'Sudo mode requires a password. Configure one above, or ensure the server allows passwordless sudo.',
|
||||
'hostDetails.sftp.encoding': 'Filename Encoding',
|
||||
'hostDetails.sftp.encoding.desc': 'Select the encoding used to decode and send SFTP filenames.',
|
||||
'hostDetails.label.placeholder': 'Label (e.g., Production Server)',
|
||||
'hostDetails.notes.label': 'Notes',
|
||||
'hostDetails.notes.placeholder': 'Hardware, project, customer, region, role...',
|
||||
'hostDetails.notes.help': 'Supports Markdown. Do not store passwords or private keys here.',
|
||||
'hostDetails.notes.tab.edit': 'Edit',
|
||||
'hostDetails.notes.tab.preview': 'Preview',
|
||||
'hostDetails.notes.preview.empty': 'Nothing to preview yet.',
|
||||
'hostDetails.notes.toggle.show': 'Show notes editor',
|
||||
'hostDetails.notes.toggle.hide': 'Hide notes editor',
|
||||
'hostDetails.group.placeholder': 'Parent Group',
|
||||
'hostDetails.section.credentials': 'Credentials',
|
||||
'hostDetails.section.portCredentials': 'Port & Credentials',
|
||||
'hostDetails.section.appearance': 'Appearance',
|
||||
'hostDetails.distro.title': 'Linux Distribution',
|
||||
'hostDetails.distro.desc': 'Auto-detect on connect, or override the distro icon manually.',
|
||||
'hostDetails.icon.title': 'Host Icon',
|
||||
'hostDetails.icon.sectionTitle': 'Color & Icon',
|
||||
'hostDetails.icon.desc': 'Choose the icon source and color for this host.',
|
||||
'hostDetails.icon.source': 'Source',
|
||||
'hostDetails.icon.manualLabel': 'Manual icon',
|
||||
'hostDetails.icon.tab.brand': 'Brand',
|
||||
'hostDetails.icon.tab.type': 'Type',
|
||||
'hostDetails.icon.mode.auto': 'Automatic',
|
||||
'hostDetails.icon.mode.custom': 'Custom',
|
||||
'hostDetails.icon.reset': 'Reset host icon',
|
||||
'hostDetails.icon.colorLabel': 'Icon color',
|
||||
'hostDetails.icon.colorDefault': 'Default',
|
||||
'hostDetails.icon.colorAuto': 'Automatic',
|
||||
'hostDetails.icon.colorManual': 'Manual',
|
||||
'hostDetails.icon.colorChoice': 'Color',
|
||||
'hostDetails.icon.autoColorLabel': 'Current color',
|
||||
'hostDetails.icon.preview': 'Icon preview',
|
||||
'hostDetails.icon.showLibrary': 'Show icon library',
|
||||
'hostDetails.icon.hideLibrary': 'Hide icon library',
|
||||
'hostDetails.icon.autoUsesDistro': 'Use Linux Distribution icon and selected color for this host.',
|
||||
'hostDetails.icon.customOverridesDistro': 'Built-in icon replaces Linux Distribution for this host.',
|
||||
'hostDetails.icon.option.server': 'Server',
|
||||
'hostDetails.icon.option.terminal': 'Terminal',
|
||||
'hostDetails.icon.option.database': 'Database',
|
||||
'hostDetails.icon.option.cloud': 'Cloud',
|
||||
'hostDetails.icon.option.router': 'Router',
|
||||
'hostDetails.icon.option.shield': 'Shield',
|
||||
'hostDetails.icon.option.code': 'Code',
|
||||
'hostDetails.icon.option.box': 'Box',
|
||||
'hostDetails.icon.option.globe': 'Globe',
|
||||
'hostDetails.icon.option.cpu': 'CPU',
|
||||
'hostDetails.icon.option.hard-drive': 'Storage',
|
||||
'hostDetails.icon.option.network': 'Network',
|
||||
'hostDetails.icon.option.wifi': 'Wireless',
|
||||
'hostDetails.icon.option.lock': 'Lock',
|
||||
'hostDetails.icon.option.key': 'Key',
|
||||
'hostDetails.icon.option.monitor': 'Monitor',
|
||||
'hostDetails.icon.option.container': 'Container',
|
||||
'hostDetails.icon.option.activity': 'Activity',
|
||||
'hostDetails.icon.option.zap': 'Fast',
|
||||
'hostDetails.icon.option.server-cog': 'Server settings',
|
||||
'hostDetails.icon.color.blue': 'Blue',
|
||||
'hostDetails.icon.color.green': 'Green',
|
||||
'hostDetails.icon.color.red': 'Red',
|
||||
'hostDetails.icon.color.amber': 'Amber',
|
||||
'hostDetails.icon.color.purple': 'Purple',
|
||||
'hostDetails.icon.color.cyan': 'Cyan',
|
||||
'hostDetails.icon.color.orange': 'Orange',
|
||||
'hostDetails.icon.color.slate': 'Slate',
|
||||
'hostDetails.icon.color.violet': 'Violet',
|
||||
'hostDetails.icon.color.pink': 'Pink',
|
||||
'hostDetails.icon.color.rose': 'Rose',
|
||||
'hostDetails.icon.color.lime': 'Lime',
|
||||
'hostDetails.icon.color.teal': 'Teal',
|
||||
'hostDetails.icon.color.sky': 'Sky',
|
||||
'hostDetails.icon.color.indigo': 'Indigo',
|
||||
'hostDetails.icon.color.zinc': 'Zinc',
|
||||
'hostDetails.icon.color.custom': 'Custom color',
|
||||
'hostDetails.distro.mode': 'Source',
|
||||
'hostDetails.distro.mode.auto': 'Auto-detect',
|
||||
'hostDetails.distro.mode.manual': 'Manual override',
|
||||
'hostDetails.distro.detectedLabel': 'Current',
|
||||
'hostDetails.distro.manualLabel': 'Override',
|
||||
'hostDetails.distro.pending': 'Detect after first connection',
|
||||
'hostDetails.distro.unknown': 'Unknown',
|
||||
'hostDetails.distro.option.linux': 'Generic Linux',
|
||||
'hostDetails.distro.option.ubuntu': 'Ubuntu',
|
||||
'hostDetails.distro.option.debian': 'Debian',
|
||||
'hostDetails.distro.option.centos': 'CentOS',
|
||||
'hostDetails.distro.option.rocky': 'Rocky Linux',
|
||||
'hostDetails.distro.option.fedora': 'Fedora',
|
||||
'hostDetails.distro.option.arch': 'Arch Linux',
|
||||
'hostDetails.distro.option.alpine': 'Alpine',
|
||||
'hostDetails.distro.option.amazon': 'Amazon Linux',
|
||||
'hostDetails.distro.option.opensuse': 'openSUSE / SLES',
|
||||
'hostDetails.distro.option.redhat': 'Red Hat / RHEL',
|
||||
'hostDetails.distro.option.almalinux': 'AlmaLinux',
|
||||
'hostDetails.distro.option.alinux': 'Alibaba Cloud Linux',
|
||||
'hostDetails.distro.option.openeuler': 'openEuler',
|
||||
'hostDetails.distro.option.oracle': 'Oracle Linux',
|
||||
'hostDetails.distro.option.kali': 'Kali Linux',
|
||||
'hostDetails.distro.option.cisco': 'Cisco',
|
||||
'hostDetails.distro.option.juniper': 'Juniper Networks',
|
||||
'hostDetails.distro.option.huawei': 'Huawei',
|
||||
'hostDetails.distro.option.h3c': 'H3C',
|
||||
'hostDetails.distro.option.hpe': 'HPE',
|
||||
'hostDetails.distro.option.mikrotik': 'MikroTik',
|
||||
'hostDetails.distro.option.fortinet': 'Fortinet',
|
||||
'hostDetails.distro.option.paloalto': 'Palo Alto Networks',
|
||||
'hostDetails.distro.option.zyxel': 'ZyXEL',
|
||||
'hostDetails.distro.option.ruijie': 'Ruijie',
|
||||
'hostDetails.section.mosh': 'Mosh',
|
||||
'hostDetails.section.et': 'EternalTerminal',
|
||||
'hostDetails.et.port': 'ET server port',
|
||||
'hostDetails.et.port.desc': 'Port etserver listens on (default 2022)',
|
||||
'hostDetails.username.placeholder': 'Username',
|
||||
'hostDetails.password.placeholder': 'Password',
|
||||
'hostDetails.password.show': 'Show password',
|
||||
'hostDetails.password.hide': 'Hide password',
|
||||
'hostDetails.password.save': 'Save password',
|
||||
'hostDetails.identity.suggestions': 'Identities',
|
||||
'hostDetails.identity.missing': 'Identity not found',
|
||||
'hostDetails.credential.keyCertificate': 'Key, Certificate, Local Key File',
|
||||
'hostDetails.credential.key': 'Key',
|
||||
'hostDetails.credential.certificate': 'Certificate',
|
||||
'hostDetails.credential.localKeyFile': 'Local Key File',
|
||||
'hostDetails.credential.localKeyFilePlaceholder': '~/.ssh/id_ed25519',
|
||||
'hostDetails.credential.browseKeyFile': 'Browse...',
|
||||
'hostDetails.credential.missing': 'Credential not found',
|
||||
'hostDetails.keys.search': 'Search keys...',
|
||||
'hostDetails.keys.empty': 'No keys available',
|
||||
'hostDetails.certs.search': 'Search certificates...',
|
||||
'hostDetails.certs.empty': 'No certificates available',
|
||||
'hostDetails.agentForwarding': 'Forward SSH Agent',
|
||||
'hostDetails.agentForwarding.desc': 'Allow remote server to use your local SSH keys (e.g., for git operations)',
|
||||
'hostDetails.agentForwarding.agentNotRunning': 'SSH Agent is not available',
|
||||
'hostDetails.agentForwarding.agentNotRunningHint': 'No SSH agent detected. Enable OpenSSH Authentication Agent in Windows Services, or use a compatible agent such as Bitwarden, 1Password, or gpg-agent.',
|
||||
'hostDetails.section.agentForwarding': 'SSH Agent',
|
||||
'hostDetails.x11Forwarding': 'Forward X11 apps',
|
||||
'hostDetails.x11Forwarding.desc': 'Show remote graphical apps on your local desktop when a local X server is running.',
|
||||
'hostDetails.section.x11Forwarding': 'X11 Forwarding',
|
||||
'hostDetails.section.deviceType': 'Device Type',
|
||||
'hostDetails.deviceType': 'Network Device Mode',
|
||||
'hostDetails.deviceType.desc': 'Enable for network equipment (switches, routers, firewalls) connected via SSH. Commands are sent as-is without shell wrapping, compatible with vendor CLIs like Huawei VRP and Cisco IOS.',
|
||||
'hostDetails.deviceType.warning': 'AI agent commands will be sent directly without exit code tracking. Only enable for devices that do not run a standard shell.',
|
||||
'hostDetails.section.sshAlgorithms': 'SSH Algorithms',
|
||||
'hostDetails.section.terminalBehavior': 'Terminal Behavior',
|
||||
'hostDetails.lineTimestamps': 'Show output timestamps',
|
||||
'hostDetails.lineTimestamps.desc': 'Show local time beside visible output lines for this host without changing terminal text.',
|
||||
'hostDetails.legacyAlgorithms': 'Allow Legacy Algorithms',
|
||||
'hostDetails.legacyAlgorithms.desc': 'Enable deprecated SSH algorithms (diffie-hellman-group1, ssh-dss, 3des-cbc, etc.) for connecting to older network equipment.',
|
||||
'hostDetails.legacyAlgorithms.warning': 'These algorithms have known security weaknesses. Only enable for legacy devices that do not support modern cryptography.',
|
||||
'hostDetails.skipEcdsaHostKey': 'Skip ECDSA host key',
|
||||
'hostDetails.skipEcdsaHostKey.desc': 'Some old Huawei / Cisco switches produce non-standard ECDSA host-key signatures that cause "signature verification failed". Turning this on drops every ecdsa-sha2-* from the client offer so negotiation falls back to RSA / Ed25519.',
|
||||
'hostDetails.algorithms.advanced': 'Advanced algorithm overrides',
|
||||
'hostDetails.algorithms.advanced.desc': 'Replace the offered algorithm list for any category on a per-host basis. Leaving a category untouched uses the default; selecting a subset fully replaces the default list. Incorrect values can make the host unreachable.',
|
||||
'hostDetails.algorithms.inheritedNotice': 'The current group has algorithm overrides set for: {categories}. The "Reset" button here falls back to the group\'s lists, not NetCatty\'s defaults. To ignore the group restriction, clear the override in the group\'s algorithm settings.',
|
||||
'hostDetails.algorithms.customized': 'customized',
|
||||
'hostDetails.algorithms.reset': 'Reset',
|
||||
'hostDetails.algorithms.category.kex': 'Key Exchange (KEX)',
|
||||
'hostDetails.algorithms.category.cipher': 'Cipher',
|
||||
'hostDetails.algorithms.category.hmac': 'MAC (HMAC)',
|
||||
'hostDetails.algorithms.category.serverHostKey': 'Host Key',
|
||||
'hostDetails.algorithms.category.compress': 'Compression',
|
||||
'hostDetails.section.keepalive': 'Keepalive',
|
||||
'hostDetails.keepalive.override': 'Override global keepalive',
|
||||
'hostDetails.keepalive.desc': 'Use a custom keepalive policy for this host instead of the global setting. Useful for older routers or switches whose SSH server does not reply to keepalive@openssh.com requests — set interval to 0 to disable keepalive entirely on this host.',
|
||||
'hostDetails.keepalive.interval': 'Interval (seconds)',
|
||||
'hostDetails.keepalive.countMax': 'Max unanswered keepalives',
|
||||
'hostDetails.keepalive.disabledHint': 'Interval = 0 disables keepalive for this host. The session will rely on TCP-level timeouts to detect a dead connection.',
|
||||
'hostDetails.backspaceBehavior': 'Backspace Behavior',
|
||||
'hostDetails.backspaceBehavior.default': 'Default',
|
||||
'hostDetails.jumpHosts': 'Proxy via Hosts',
|
||||
'hostDetails.jumpHosts.hops': '{count} hop(s)',
|
||||
'hostDetails.jumpHosts.direct': 'Direct',
|
||||
'hostDetails.jumpHosts.configure': 'Configure Proxy Hosts',
|
||||
'hostDetails.proxy': 'Proxy via HTTP/SOCKS5/Command',
|
||||
'hostDetails.proxy.none': 'None',
|
||||
'hostDetails.proxy.edit': 'Edit Proxy',
|
||||
'hostDetails.proxy.configure': 'Configure Proxy',
|
||||
'hostDetails.proxyPanel.title': 'Proxy via HTTP/SOCKS5/Command',
|
||||
'hostDetails.proxyPanel.hostPlaceholder': 'Proxy host',
|
||||
'hostDetails.proxyPanel.command': 'ProxyCommand',
|
||||
'hostDetails.proxyPanel.commandPlaceholder': 'cloudflared access ssh --hostname %h',
|
||||
'hostDetails.proxyPanel.commandHelp': 'Use %h for the target host, %p for the target port, and %% for a literal percent.',
|
||||
'hostDetails.proxyPanel.credentials': 'Credentials',
|
||||
'hostDetails.proxyPanel.usernamePlaceholder': 'Username',
|
||||
'hostDetails.proxyPanel.passwordPlaceholder': 'Password',
|
||||
'hostDetails.proxyPanel.identities': 'Identities',
|
||||
'hostDetails.proxyPanel.keychainIdentity': 'Keychain identity',
|
||||
'hostDetails.proxyPanel.manualCredentials': 'Manual credentials',
|
||||
'hostDetails.proxyPanel.remove': 'Remove Proxy',
|
||||
'hostDetails.proxyPanel.savedProxy': 'Saved proxy',
|
||||
'hostDetails.proxyPanel.selectSaved': 'Select saved proxy',
|
||||
'hostDetails.proxyPanel.customProxy': 'Custom proxy',
|
||||
'hostDetails.proxyPanel.missing': 'Missing',
|
||||
'hostDetails.proxyPanel.missingSaved': 'Missing saved proxy',
|
||||
'hostDetails.proxyPanel.missingIdentity': 'Missing keychain identity',
|
||||
'hostDetails.proxyPanel.incompleteIdentity': 'Proxy identity needs a username and password',
|
||||
'hostDetails.proxyPanel.unreadableIdentity': 'Proxy identity password cannot be read on this device',
|
||||
'hostDetails.proxyPanel.error.required': 'Proxy host and port, or a ProxyCommand, are required.',
|
||||
'hostDetails.envVars': 'Environment Variables',
|
||||
'hostDetails.envVars.add': 'Add Environment Variable',
|
||||
'hostDetails.envVars.title': 'Environment Variables',
|
||||
'hostDetails.envVars.desc': 'Set an environment variable for {host}.',
|
||||
'hostDetails.envVars.note':
|
||||
'Some SSH servers by default only allow variables with prefix LC_ and LANG_.',
|
||||
'hostDetails.envVars.variable': 'Variable',
|
||||
'hostDetails.envVars.value': 'Value',
|
||||
'hostDetails.envVars.newVariable': 'New Variable',
|
||||
'hostDetails.envVars.variableName': 'Variable name',
|
||||
'hostDetails.chain.title': 'Edit Chain',
|
||||
'hostDetails.chain.desc': 'Adding another host will create a connection to {host}.',
|
||||
'hostDetails.chain.addHost': 'Add a Host',
|
||||
'hostDetails.chain.target': 'Target',
|
||||
'hostDetails.chain.availableHosts': 'Available Hosts',
|
||||
'hostDetails.chain.clear': 'Clear',
|
||||
'hostDetails.group.title': 'New Group',
|
||||
'hostDetails.group.general': 'General',
|
||||
'hostDetails.group.namePlaceholder': 'Group name',
|
||||
'hostDetails.group.parentPlaceholder': 'Parent Group',
|
||||
'hostDetails.group.cloudSync': 'Cloud Sync',
|
||||
'hostDetails.group.addProtocol': 'Add protocol',
|
||||
'hostDetails.startupCommand': 'Startup Command',
|
||||
'hostDetails.startupCommand.placeholder': 'Command to run on connect (e.g., cd /app && ls)',
|
||||
'hostDetails.startupCommand.help':
|
||||
'This command will be executed automatically after SSH connection is established.',
|
||||
'hostDetails.startupCommand.runMode': 'Multi-line run',
|
||||
'hostDetails.startupCommand.runMode.help': 'Choose how multi-line startup commands are sent after connection.',
|
||||
'hostDetails.startupCommand.runMode.lineDelay': 'Send line by line',
|
||||
'hostDetails.startupCommand.runMode.paste': 'Send all at once',
|
||||
'hostDetails.otherProtocols': 'Other Protocols',
|
||||
'hostDetails.telnetOn': 'Telnet on',
|
||||
'hostDetails.port': 'port',
|
||||
'hostDetails.telnet.credentials': 'Credentials',
|
||||
'hostDetails.telnet.identity.placeholder': 'Manual identity',
|
||||
'hostDetails.telnet.username': 'Telnet Username',
|
||||
'hostDetails.telnet.password': 'Telnet Password',
|
||||
'hostDetails.charset.placeholder': 'Charset (e.g. UTF-8)',
|
||||
'hostDetails.telnet.add': 'Add Telnet Protocol',
|
||||
'hostDetails.telnet.setDefault': 'Connect with Telnet by default',
|
||||
'hostDetails.tags': 'Tags',
|
||||
'hostDetails.group': 'Group',
|
||||
'hostDetails.selectGroup': 'Select Group',
|
||||
'hostDetails.addTag': 'Add a tag...',
|
||||
'hostDetails.createTag': 'Create tag',
|
||||
'hostDetails.createGroup': 'Create group',
|
||||
|
||||
// Host form (legacy modal)
|
||||
'hostForm.title.edit': 'Edit Host',
|
||||
'hostForm.title.new': 'New Host',
|
||||
'hostForm.desc.edit': 'Update connection details for this host',
|
||||
'hostForm.desc.new': 'Create a new SSH host entry',
|
||||
'hostForm.field.label': 'Label',
|
||||
'hostForm.placeholder.label': 'My Production Server',
|
||||
'hostForm.field.hostname': 'Hostname / IP',
|
||||
'hostForm.placeholder.hostname': '192.168.1.1',
|
||||
'hostForm.field.port': 'Port',
|
||||
'hostForm.field.username': 'Username',
|
||||
'hostForm.field.osType': 'OS Type',
|
||||
'hostForm.placeholder.selectOs': 'Select OS',
|
||||
'hostForm.field.group': 'Group',
|
||||
'hostForm.placeholder.group': 'e.g. AWS, DigitalOcean',
|
||||
'hostForm.field.tags': 'Tags',
|
||||
'hostForm.placeholder.addTag': 'Add a tag...',
|
||||
'hostForm.auth.method': 'Authentication Method',
|
||||
'hostForm.auth.password': 'Password',
|
||||
'hostForm.auth.sshKey': 'SSH Key',
|
||||
'hostForm.auth.selectKey': 'Select an SSH Key',
|
||||
'hostForm.auth.noKeys': 'No keys available',
|
||||
'hostForm.auth.noKeysHint': 'No SSH keys found in Keychain. Please create one first.',
|
||||
'hostForm.saveHost': 'Save Host',
|
||||
|
||||
// Connection logs
|
||||
'logs.table.date': 'Date',
|
||||
'logs.table.user': 'User',
|
||||
'logs.table.host': 'Host',
|
||||
'logs.table.saved': 'Saved',
|
||||
'logs.empty.title': 'No Connection Logs',
|
||||
'logs.empty.desc':
|
||||
'Your connection history will appear here when you connect to hosts or open local terminals.',
|
||||
'logs.loadMore': 'Load {count} more logs',
|
||||
'logs.ongoing': 'ongoing',
|
||||
'logs.localTerminal': 'Local Terminal',
|
||||
'logs.action.save': 'Save',
|
||||
'logs.action.unsave': 'Unsave',
|
||||
'logs.action.delete': 'Delete',
|
||||
|
||||
// Log view
|
||||
'logView.customizeAppearance': 'Customize appearance',
|
||||
'logView.appearance': 'Appearance',
|
||||
'logView.readOnly': 'Read-only',
|
||||
'logView.export': 'Export',
|
||||
|
||||
};
|
||||
+13
-2125
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,468 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const ruAiMessages: Messages = {
|
||||
// AI Settings
|
||||
'ai.agentSettings': 'Настройки агента',
|
||||
'ai.chat.preparing': 'Подготовка…',
|
||||
'ai.title': 'AI',
|
||||
'ai.description': 'Настройка AI-провайдеров, агентов и параметров безопасности',
|
||||
'ai.providers': 'Провайдеры',
|
||||
'ai.agents': 'Агенты',
|
||||
'ai.providers.empty': 'Провайдеры не настроены. Добавьте провайдера, чтобы начать.',
|
||||
'ai.providers.add': 'Добавить провайдера',
|
||||
'ai.providers.active': 'Активен',
|
||||
'ai.providers.apiKeyConfigured': 'API-ключ настроен',
|
||||
'ai.providers.noApiKey': 'Нет API-ключа',
|
||||
'ai.providers.configure': 'Настроить',
|
||||
'ai.providers.remove': 'Удалить',
|
||||
'ai.providers.name': 'Отображаемое имя',
|
||||
'ai.providers.name.placeholder': 'например, Мой провайдер',
|
||||
'ai.providers.style': 'Стиль протокола',
|
||||
'ai.providers.style.anthropic': 'Совместимый с Anthropic',
|
||||
'ai.providers.style.openai': 'Совместимый с OpenAI',
|
||||
'ai.providers.style.google': 'Совместимый с Google',
|
||||
'ai.providers.style.inherited': 'авто',
|
||||
'ai.providers.style.help': 'Определяет, какой формат API используется для запросов. Переопределите, если стороннее API использует другой диалект.',
|
||||
'ai.providers.icon.change': 'Изменить иконку',
|
||||
'ai.providers.icon.upload': 'Загрузить изображение',
|
||||
'ai.providers.icon.reset': 'Сбросить',
|
||||
'ai.providers.icon.close': 'Свернуть',
|
||||
'ai.providers.icon.uploadedNote': 'Своя иконка (64×64 WebP)',
|
||||
'ai.providers.icon.errorType': 'Пожалуйста, выберите файл изображения.',
|
||||
'ai.providers.apiKey': 'API-ключ',
|
||||
'ai.providers.apiKey.placeholder': 'Введите API-ключ',
|
||||
'ai.providers.apiKey.decrypting': 'Расшифровка...',
|
||||
'ai.providers.baseUrl': 'Базовый URL',
|
||||
'ai.providers.skipTLSVerify': 'Пропустить проверку TLS-сертификата (для самоподписанных сертификатов)',
|
||||
'ai.providers.defaultModel': 'Модель по умолчанию',
|
||||
'ai.providers.defaultModel.placeholder': 'например, gpt-4o, claude-sonnet-4-20250514',
|
||||
'ai.providers.contextWindow': 'Контекстное окно',
|
||||
'ai.providers.contextWindow.placeholder': 'например, 128000',
|
||||
'ai.providers.contextWindow.help': 'Оставьте пустым, чтобы использовать значение из списка моделей, если оно доступно; иначе Netcatty применит безопасное значение по умолчанию.',
|
||||
'ai.providers.contextWindow.error': 'Введите положительное целое число или оставьте поле пустым.',
|
||||
'ai.providers.refreshModels': 'Обновить модели',
|
||||
'ai.providers.searchModel': 'Искать или ввести ID модели...',
|
||||
'ai.providers.filterModels': 'Фильтровать модели...',
|
||||
'ai.providers.loadingModels': 'Загрузка моделей...',
|
||||
'ai.providers.noMatchingModels': 'Нет подходящих моделей',
|
||||
'ai.providers.clickToLoadModels': 'Нажмите, чтобы загрузить модели',
|
||||
'ai.providers.showingModels': 'Показаны первые 100 из {count} моделей. Введите текст для фильтрации.',
|
||||
'ai.providers.advancedParams': 'Дополнительные параметры',
|
||||
'ai.providers.advancedParams.hint': 'Оставьте пустым, чтобы использовать настройки провайдера по умолчанию.',
|
||||
'ai.providers.advancedParams.maxTokens.placeholder': 'например, 4096',
|
||||
'ai.providers.advancedParams.default': 'По умолчанию у провайдера',
|
||||
|
||||
// AI Codex
|
||||
'ai.codex': 'Codex',
|
||||
'ai.codex.title': 'Codex CLI',
|
||||
'ai.codex.description': 'Подключение OpenAI Codex. Здесь можно войти через ChatGPT или включить API-ключ OpenAI-совместимого провайдера и пользовательский endpoint в настройках.',
|
||||
'ai.codex.detecting': 'Обнаружение...',
|
||||
'ai.codex.notFound': 'Не найден',
|
||||
'ai.codex.awaitingLogin': 'Ожидание входа',
|
||||
'ai.codex.connectedChatGPT': 'Подключено через ChatGPT',
|
||||
'ai.codex.connectedApiKey': 'Подключено через API-ключ',
|
||||
'ai.codex.connectedCustomConfig': 'Подключено через ~/.codex/config.toml',
|
||||
'ai.codex.customConfigIncomplete': 'Обнаружен пользовательский конфиг (отсутствует переменная окружения)',
|
||||
'ai.codex.customConfigHint': 'Используется пользовательский провайдер "{provider}", настроенный в ~/.codex/config.toml — вход через ChatGPT не требуется.',
|
||||
'ai.codex.customConfigMissingEnvKey': 'Предупреждение: {envKey} не задана в переменных окружения вашей оболочки. Экспортируйте её (или запустите netcatty из оболочки, где она задана), чтобы Codex мог пройти аутентификацию.',
|
||||
'ai.codex.notConnected': 'Не подключено',
|
||||
'ai.codex.statusUnknown': 'Статус неизвестен',
|
||||
'ai.codex.path': 'Путь:',
|
||||
'ai.codex.notFoundHint': 'Не удалось найти codex в PATH. Установите его или укажите путь к исполняемому файлу ниже.',
|
||||
'ai.codex.customPathPlaceholder': 'например, /usr/local/bin/codex',
|
||||
'ai.codex.check': 'Проверить',
|
||||
'ai.codex.resetPath': 'Сбросить',
|
||||
'ai.codex.openLogin': 'Открыть вход',
|
||||
'ai.codex.logout': 'Выйти',
|
||||
'ai.codex.connectChatGPT': 'Подключить ChatGPT',
|
||||
'ai.codex.refreshStatus': 'Обновить статус',
|
||||
|
||||
// AI Claude Code
|
||||
'ai.claude.title': 'Claude Code',
|
||||
'ai.claude.description': 'Агентный помощник для программирования от Anthropic. Требует установленный в системе Claude Code CLI.',
|
||||
'ai.claude.detecting': 'Обнаружение...',
|
||||
'ai.claude.detected': 'Обнаружен',
|
||||
'ai.claude.notFound': 'Не найден',
|
||||
'ai.claude.path': 'Путь:',
|
||||
'ai.claude.notFoundHint': 'Не удалось найти claude в PATH. Установите его или укажите путь к исполняемому файлу ниже.',
|
||||
'ai.claude.customPathPlaceholder': 'например, /usr/local/bin/claude',
|
||||
'ai.claude.configSection': 'Аутентификация и конфигурация (опционально)',
|
||||
'ai.claude.configDir': 'Каталог конфигурации',
|
||||
'ai.claude.configDir.placeholder': '~/.claude (пусто — по умолчанию)',
|
||||
'ai.claude.configDir.hint': 'Задаёт CLAUDE_CONFIG_DIR — укажите папку, где выполнен вход `claude` (содержит settings.json и учётные данные).',
|
||||
'ai.claude.settings': 'Файл настроек',
|
||||
'ai.claude.settings.placeholder': '~/team-settings.json (путь или встроенный {"model":"..."})',
|
||||
'ai.claude.settings.hint': 'Опционально. Путь к settings.json или встроенный JSON, передаётся в SDK как `settings`. Дополняет «Каталог конфигурации» выше и независим от него (накладывается сверху, не заменяет).',
|
||||
'ai.claude.envVars': 'Переменные окружения',
|
||||
'ai.claude.envVars.placeholder': 'ANTHROPIC_BASE_URL=https://...\nANTHROPIC_MODEL=...',
|
||||
'ai.claude.envVars.hint': 'По одному KEY=VALUE в строке, передаётся агенту Claude. Хранится локально в открытом виде — для API-ключей и учётных данных используйте «Каталог конфигурации» выше (вход `claude`).',
|
||||
'ai.claude.check': 'Проверить',
|
||||
'ai.claude.resetPath': 'Сбросить',
|
||||
|
||||
// AI GitHub Copilot CLI
|
||||
'ai.copilot.title': 'GitHub Copilot CLI',
|
||||
'ai.copilot.description': 'Использует GitHub Copilot CLI. После обнаружения может быть выбран как внешний агент для программирования.',
|
||||
'ai.copilot.detecting': 'Обнаружение...',
|
||||
'ai.copilot.detected': 'Обнаружен',
|
||||
'ai.copilot.notFound': 'Не найден',
|
||||
'ai.copilot.path': 'Путь:',
|
||||
'ai.copilot.notFoundHint': 'Не удалось найти copilot в PATH. Установите его или укажите путь к исполняемому файлу ниже.',
|
||||
'ai.copilot.customPathPlaceholder': 'например, /usr/local/bin/copilot',
|
||||
'ai.copilot.check': 'Проверить',
|
||||
'ai.copilot.resetPath': 'Сбросить',
|
||||
|
||||
// AI Cursor SDK
|
||||
'ai.cursor.title': 'Cursor',
|
||||
'ai.cursor.description': 'Использует Cursor SDK.',
|
||||
'ai.cursor.detecting': 'Обнаружение...',
|
||||
'ai.cursor.detected': 'Доступен',
|
||||
'ai.cursor.notFound': 'Недоступен',
|
||||
'ai.cursor.path': 'Среда:',
|
||||
'ai.cursor.notFoundHint': 'Укажите API-ключ, чтобы включить Cursor.',
|
||||
'ai.cursor.notInstalledHint': 'Cursor SDK не обнаружен.',
|
||||
'ai.cursor.installStatus': 'Cursor SDK',
|
||||
'ai.cursor.installed': 'Обнаружено',
|
||||
'ai.cursor.notInstalled': 'Не обнаружено',
|
||||
'ai.cursor.apiKeyStatus': 'API-ключ',
|
||||
'ai.cursor.apiKeyConfigured': 'Настроен',
|
||||
'ai.cursor.apiKeyMissing': 'Не указан',
|
||||
'ai.cursor.apiKeyFromEnv': 'Из окружения',
|
||||
'ai.cursor.apiKey': 'API-ключ',
|
||||
'ai.cursor.apiKeyPlaceholder': 'Введите API-ключ Cursor',
|
||||
'ai.cursor.apiKeyPlaceholder.env': 'Используется CURSOR_API_KEY; введите ключ для замены',
|
||||
'ai.cursor.apiKeyEnvHint': 'Cursor может использовать CURSOR_API_KEY из shell. Сохраняйте ключ здесь только если хотите переопределить его в Netcatty.',
|
||||
'ai.cursor.apiKeyOverrideHint': 'Netcatty сначала использует сохранённый здесь ключ, затем CURSOR_API_KEY.',
|
||||
'ai.cursor.saveApiKey': 'Сохранить',
|
||||
'ai.cursor.saved': 'Сохранено',
|
||||
'ai.cursor.showApiKey': 'Показать API-ключ',
|
||||
'ai.cursor.hideApiKey': 'Скрыть API-ключ',
|
||||
'ai.cursor.customPathPlaceholder': 'например, /usr/local/bin/cursor',
|
||||
'ai.cursor.check': 'Проверить',
|
||||
|
||||
// AI CodeBuddy Code
|
||||
'ai.codebuddy.title': 'CodeBuddy Code',
|
||||
'ai.codebuddy.description': 'Использует CodeBuddy Code через официальный Agent SDK (`@tencent-ai/agent-sdk`). После обнаружения может быть выбран как внешний агент для программирования.',
|
||||
'ai.codebuddy.detecting': 'Обнаружение...',
|
||||
'ai.codebuddy.detected': 'Обнаружен',
|
||||
'ai.codebuddy.notFound': 'Не найден',
|
||||
'ai.codebuddy.path': 'Путь:',
|
||||
'ai.codebuddy.notFoundHint': 'Не удалось найти codebuddy в PATH. Установите его или укажите путь к исполняемому файлу ниже.',
|
||||
'ai.codebuddy.customPathPlaceholder': 'например, /usr/local/bin/codebuddy',
|
||||
'ai.codebuddy.check': 'Проверить',
|
||||
'ai.codebuddy.resetPath': 'Сбросить',
|
||||
'ai.codebuddy.configSection': 'Аутентификация и конфигурация (необязательно)',
|
||||
'ai.codebuddy.internetEnv': 'Сетевая среда',
|
||||
'ai.codebuddy.internetEnv.default': 'По умолчанию (зарубежная)',
|
||||
'ai.codebuddy.internetEnv.internal': 'Internal',
|
||||
'ai.codebuddy.internetEnv.ioa': 'IOA',
|
||||
'ai.codebuddy.internetEnv.hint': 'Устанавливает CODEBUDDY_INTERNET_ENVIRONMENT — выберите Internal или IOA для ограниченных сетевых сред.',
|
||||
'ai.codebuddy.envVars': 'Переменные окружения',
|
||||
'ai.codebuddy.envVars.placeholder': 'CODEBUDDY_API_KEY=...\nCODEBUDDY_AUTH_TOKEN=...\nOTHER_VAR=...',
|
||||
'ai.codebuddy.envVars.hint': 'По одной записи KEY=VALUE на строку, передаются агенту CodeBuddy. Укажите CODEBUDDY_API_KEY или CODEBUDDY_AUTH_TOKEN для аутентификации. Хранятся локально в открытом виде.',
|
||||
|
||||
// AI Default Agent
|
||||
'ai.defaultAgent': 'Агент по умолчанию',
|
||||
'ai.defaultAgent.description': 'Агент, который будет использоваться при запуске новой AI-сессии',
|
||||
'ai.defaultAgent.catty': 'Catty (встроенный)',
|
||||
'ai.toolAccess.title': 'Доступ к инструментам',
|
||||
'ai.toolAccess.mode': 'Режим доступа Netcatty',
|
||||
'ai.toolAccess.description': 'Выберите, как внешние агенты получают доступ к сессиям Netcatty. MCP предоставляет встроенный сервер, а Skills + CLI указывает агентам на локальный skill Netcatty и команды CLI.',
|
||||
'ai.toolAccess.mode.mcp': 'MCP',
|
||||
'ai.toolAccess.mode.skills': 'Skills + CLI',
|
||||
|
||||
'ai.externalMcp.title': 'Внешний MCP',
|
||||
'ai.externalMcp.description': 'Откройте Netcatty как MCP-сервер для Codex, Claude Code, Cursor и Grok. Набор инструментов тот же, что у встроенных агентов. Держите Netcatty запущенным, пока клиенты подключены.',
|
||||
'ai.externalMcp.sessionsExposed': 'Сессий в области: {count}',
|
||||
'ai.externalMcp.mode': 'Режим доступности',
|
||||
'ai.externalMcp.mode.temporary': 'Временный',
|
||||
'ai.externalMcp.mode.persistent': 'Постоянный',
|
||||
'ai.externalMcp.mode.description': 'Временный режим отключается после простоя. Постоянный восстанавливает External MCP при запуске Netcatty.',
|
||||
'ai.externalMcp.idleTimeout': 'Таймаут простоя',
|
||||
'ai.externalMcp.idleTimeout.description': 'В временном режиме отключить External MCP после указанного числа минут без MCP-операций.',
|
||||
'ai.externalMcp.idleTimeout.minutes': 'мин',
|
||||
'ai.externalMcp.usage.title': 'Как пользоваться',
|
||||
'ai.externalMcp.usage.keepRunning': '1. Включите External MCP и держите Netcatty запущенным.',
|
||||
'ai.externalMcp.usage.localhost': '2. Клиенты подключаются через локальный launcher (только 127.0.0.1). Discovery удаляется при отключении.',
|
||||
'ai.externalMcp.usage.permissions': '3. Запись следует Settings → AI → Safety и списку блокировки команд.',
|
||||
'ai.externalMcp.usage.capabilities': '4. Доступен полный catalog: терминал, SFTP, Vault и проброс портов. Секреты не возвращаются.',
|
||||
'ai.externalMcp.help.ariaLabel': 'Справка External MCP',
|
||||
'ai.externalMcp.security': 'Безопасность',
|
||||
'ai.externalMcp.security.description': 'Слушает только 127.0.0.1 с ротацией token, использует AI Permission Mode для записи и удаляет discovery при отключении.',
|
||||
'ai.externalMcp.permissionMode': 'Текущий режим разрешений: {mode}',
|
||||
'ai.externalMcp.permissionMode.label': 'Режим разрешений на запись',
|
||||
'ai.externalMcp.permissionMode.hint': 'Тот же параметр, что Settings → AI → Safety. Auto выполняет write-инструменты Netcatty без запросов; Confirm спрашивает каждый раз. Внешние клиенты (Codex / Claude / Grok) могут показывать своё подтверждение инструментов.',
|
||||
'ai.externalMcp.permissionMode.unknown': 'Неизвестно',
|
||||
'ai.externalMcp.discovery': 'Discovery',
|
||||
'ai.externalMcp.launcher': 'Launcher',
|
||||
'ai.externalMcp.unavailable': 'Недоступно',
|
||||
'ai.externalMcp.bridgeUnavailable': 'Мост External MCP недоступен',
|
||||
'ai.externalMcp.copy': 'Копировать',
|
||||
'ai.externalMcp.copied': 'Скопировано',
|
||||
'ai.externalMcp.copyFailed': 'Не удалось скопировать. Скопируйте вручную.',
|
||||
'ai.externalMcp.refresh': 'Обновить',
|
||||
'ai.externalMcp.clientConfiguration': 'Настройка клиента',
|
||||
'ai.externalMcp.clientConfiguration.description': 'Выберите клиент для установки в один клик или скопируйте CLI / фрагмент конфига.',
|
||||
'ai.externalMcp.client.codex': 'Codex',
|
||||
'ai.externalMcp.client.claude': 'Claude Code',
|
||||
'ai.externalMcp.client.grok': 'Grok',
|
||||
'ai.externalMcp.client.cursor': 'Cursor',
|
||||
'ai.externalMcp.cliCommand': 'CLI-команда',
|
||||
'ai.externalMcp.configSnippet': 'Фрагмент конфига',
|
||||
'ai.externalMcp.addToCodex': 'Добавить в Codex',
|
||||
'ai.externalMcp.addToClaude': 'Добавить в Claude Code',
|
||||
'ai.externalMcp.addToGrok': 'Добавить в Grok',
|
||||
'ai.externalMcp.codexAdded': 'Запись MCP для Codex добавлена. Перезапустите Codex или откройте новую сессию.',
|
||||
'ai.externalMcp.claudeAdded': 'Запись MCP для Claude Code добавлена. Перезапустите Claude Code или откройте новую сессию.',
|
||||
'ai.externalMcp.grokAdded': 'Запись MCP для Grok добавлена. Перезапустите Grok или откройте новую сессию.',
|
||||
'ai.externalMcp.installCodex': 'Сначала установите Codex, затем нажмите Обновить.',
|
||||
'ai.externalMcp.installClaude': 'Сначала установите Claude Code, затем нажмите Обновить.',
|
||||
'ai.externalMcp.installGrok': 'Сначала установите Grok CLI, затем нажмите Обновить.',
|
||||
'ai.externalMcp.conflict.description': 'Запись netcatty-external уже существует и указывает в другое место. Удалите или измените её вручную.',
|
||||
'ai.externalMcp.enableForLauncher': 'Включите External MCP, чтобы получить путь launcher.',
|
||||
'ai.externalMcp.cursor.title': 'Cursor / другие клиенты',
|
||||
'ai.externalMcp.cursor.description': 'Объедините с MCP-конфигом (например ~/.cursor/mcp.json). Не заменяйте весь файл, если там уже есть другие серверы.',
|
||||
'ai.externalMcp.status.unavailable': 'Недоступно',
|
||||
'ai.externalMcp.status.disabled': 'Выключено',
|
||||
'ai.externalMcp.status.running': 'Работает',
|
||||
'ai.externalMcp.status.starting': 'Запуск',
|
||||
'ai.externalMcp.status.error': 'Ошибка',
|
||||
'ai.externalMcp.status.configured': 'Настроено',
|
||||
'ai.externalMcp.status.notConfigured': 'Не настроено',
|
||||
'ai.externalMcp.status.checking': 'Проверка',
|
||||
'ai.externalMcp.status.codexNotFound': 'Codex не найден',
|
||||
'ai.externalMcp.status.claudeNotFound': 'Claude Code не найден',
|
||||
'ai.externalMcp.status.grokNotFound': 'Grok не найден',
|
||||
'ai.externalMcp.status.conflict': 'Конфликт',
|
||||
'ai.userSkills.title': 'Пользовательские skills',
|
||||
'ai.userSkills.description': 'Откройте папку skills Netcatty, чтобы добавить свои каталоги skills. Netcatty автоматически сканирует их и добавляет только лёгкие индексы, если skill явно не соответствует текущему запросу.',
|
||||
'ai.userSkills.openFolder': 'Открыть папку skills',
|
||||
'ai.userSkills.reload': 'Перезагрузить skills',
|
||||
'ai.userSkills.location': 'Расположение',
|
||||
'ai.userSkills.loading': 'Сканирование пользовательских skills...',
|
||||
'ai.userSkills.summary': '{ready} готово, {warnings} предупреждений',
|
||||
'ai.userSkills.empty': 'Пользовательские skills пока не найдены. Откройте папку, чтобы добавить каталоги skills с файлом SKILL.md.',
|
||||
'ai.userSkills.unavailable': 'Пользовательские skills недоступны в этой среде.',
|
||||
'ai.userSkills.status.ready': 'Готово',
|
||||
'ai.userSkills.status.warning': 'Предупреждение',
|
||||
|
||||
// AI Quick Messages
|
||||
'ai.quickMessages.title': 'Быстрые сообщения',
|
||||
'ai.quickMessages.description': 'Создавайте часто используемые подсказки и вставляйте их в AI-чат через / или кнопку быстрых сообщений. В отличие от user skills, быстрые сообщения заполняют поле ввода текстом.',
|
||||
'ai.quickMessages.add': 'Добавить быстрое сообщение',
|
||||
'ai.quickMessages.createTitle': 'Новое быстрое сообщение',
|
||||
'ai.quickMessages.editTitle': 'Редактировать быстрое сообщение',
|
||||
'ai.quickMessages.name': 'Название',
|
||||
'ai.quickMessages.name.placeholder': 'например: Проверить диск',
|
||||
'ai.quickMessages.slug': 'Команда',
|
||||
'ai.quickMessages.slug.placeholder': 'disk-check',
|
||||
'ai.quickMessages.descriptionField': 'Описание (необязательно)',
|
||||
'ai.quickMessages.descriptionField.placeholder': 'Краткая подсказка о назначении',
|
||||
'ai.quickMessages.content': 'Текст сообщения',
|
||||
'ai.quickMessages.content.placeholder': 'Полный текст подсказки для вставки...',
|
||||
'ai.quickMessages.empty': 'Быстрых сообщений пока нет. Добавьте несколько часто используемых подсказок.',
|
||||
'ai.quickMessages.confirmDelete': 'Удалить быстрое сообщение «{name}»?',
|
||||
'ai.quickMessages.error.nameRequired': 'Укажите название.',
|
||||
'ai.quickMessages.error.invalidSlug': 'Команда может содержать только строчные буквы, цифры и дефисы.',
|
||||
'ai.quickMessages.error.contentRequired': 'Укажите текст сообщения.',
|
||||
'ai.quickMessages.error.slugTaken': 'Эта команда уже используется другим быстрым сообщением.',
|
||||
'ai.quickMessages.error.slugConflictsWithSkill': 'Команда конфликтует с user skill «/{slug}». Выберите другую.',
|
||||
'ai.quickMessages.error.maxItems': 'Можно сохранить не более {max} быстрых сообщений.',
|
||||
|
||||
// AI Chat
|
||||
'ai.chat.noProvider': 'AI-провайдер не настроен. Перейдите в **Настройки → AI → Провайдеры**, чтобы добавить и включить провайдера.',
|
||||
'ai.chat.toolDenied': 'Действие было отклонено пользователем.',
|
||||
'ai.chat.toolApproved': 'Одобрено',
|
||||
'ai.chat.toolApprovalHint': 'Enter — разрешить один раз, Escape — отклонить',
|
||||
'ai.chat.approve': 'Одобрить',
|
||||
'ai.chat.approveOnce': 'Разрешить один раз',
|
||||
'ai.chat.alwaysAllow': 'Всегда разрешать',
|
||||
'ai.chat.slashStopDesc': 'Остановить текущий ход AI и отменить выполняющиеся инструменты',
|
||||
'ai.chat.reject': 'Отклонить',
|
||||
'ai.chat.toolLabel': 'Инструмент',
|
||||
'ai.chat.targetLabel': 'Цель',
|
||||
'ai.chat.permissionRequired': 'Требуется разрешение',
|
||||
'ai.chat.permissionDescription': 'AI-агент хочет выполнить вызов инструмента, для которого требуется ваше одобрение.',
|
||||
'ai.chat.commandBlocked': 'Эта команда заблокирована вашей политикой безопасности и не может быть выполнена.',
|
||||
'ai.chat.recommendAllow': 'Разрешить',
|
||||
'ai.chat.recommendConfirm': 'Подтвердить',
|
||||
'ai.chat.recommendDeny': 'Запретить',
|
||||
'ai.chat.exportConversation': 'Экспортировать разговор',
|
||||
'ai.chat.exportAs': 'Экспортировать как',
|
||||
'ai.chat.exportMarkdown': 'Markdown',
|
||||
'ai.chat.exportJSON': 'JSON',
|
||||
'ai.chat.exportPlainText': 'Обычный текст',
|
||||
'ai.chat.thinking': 'Размышляет',
|
||||
'ai.chat.thoughtFor': 'Размышлял {duration}',
|
||||
'ai.chat.thought': 'Мысль',
|
||||
'ai.chat.agents': 'Агенты',
|
||||
'ai.chat.detectedOnMachine': 'Обнаружено на этом устройстве',
|
||||
'ai.chat.rescan': 'Пересканировать',
|
||||
'ai.chat.permObserver': 'Наблюдатель',
|
||||
'ai.chat.permConfirm': 'Подтверждение',
|
||||
'ai.chat.permAuto': 'Авто',
|
||||
'ai.chat.permObserverDesc': 'Только чтение',
|
||||
'ai.chat.permConfirmDesc': 'Спрашивать перед действиями',
|
||||
'ai.chat.permAutoDesc': 'Выполнять свободно',
|
||||
'ai.chat.emptyHint': 'Спрашивайте о ваших серверах, запускайте команды или получайте помощь с конфигурациями.',
|
||||
'ai.chat.placeholder': 'Сообщение {agent} — @ для добавления контекста, / для команд',
|
||||
'ai.chat.placeholderDefault': 'Сообщение агенту Catty...',
|
||||
'ai.chat.noModel': 'Нет модели',
|
||||
'ai.chat.noProviderModel': 'Модель по умолчанию не задана — настройте её в Настройки → AI → Провайдеры.',
|
||||
'ai.chat.selectProvider': 'Выберите провайдера',
|
||||
'ai.chat.recent': 'Недавние',
|
||||
'ai.chat.viewAll': 'Показать всё',
|
||||
'ai.chat.untitled': 'Без названия',
|
||||
'ai.chat.justNow': 'Только что',
|
||||
'ai.chat.minutesAgo': '{n}м назад',
|
||||
'ai.chat.hoursAgo': '{n}ч назад',
|
||||
'ai.chat.daysAgo': '{n}д назад',
|
||||
'ai.chat.newChat': 'Новый чат',
|
||||
'ai.chat.allSessions': 'Все сессии',
|
||||
'ai.chat.loadEarlierMessages': 'Загрузить более ранние сообщения (ещё {n})',
|
||||
'ai.chat.usedTools': 'Использовано инструментов: {n}',
|
||||
'ai.chat.loadMoreSessions': 'Загрузить больше сессий (ещё {n})',
|
||||
'ai.chat.noSessions': 'Предыдущих сессий нет',
|
||||
'ai.chat.retryHint': 'Вы можете повторить попытку, отправив сообщение ещё раз.',
|
||||
'ai.chat.approvalTimeout': 'Время ожидания одобрения инструмента истекло через 5 минут. Вы можете повторить попытку, отправив сообщение ещё раз.',
|
||||
'ai.chat.menuHosts': 'Хосты',
|
||||
'ai.chat.menuContext': 'Контекст',
|
||||
'ai.chat.menuFiles': 'Файлы',
|
||||
'ai.chat.menuImage': 'Изображение',
|
||||
'ai.chat.menuMentionHost': 'Упомянуть хост',
|
||||
'ai.chat.menuUserSkills': 'Пользовательские skills',
|
||||
'ai.chat.menuSlashCommands': 'Команды /',
|
||||
'ai.chat.slashCommands': 'Команды /',
|
||||
'ai.chat.slashQuickMessages': 'Быстрые сообщения',
|
||||
'ai.chat.slashUserSkills': 'User skills',
|
||||
'ai.chat.quickMessages': 'Команды /',
|
||||
'ai.chat.slashNoResults': 'Нет подходящих команд',
|
||||
'ai.chat.slashEmptyHint': 'Добавьте подсказки в Настройки → AI → Быстрые сообщения.',
|
||||
|
||||
// AI Chat Shortcuts
|
||||
'ai.chatShortcuts.title': 'Быстрые действия чата',
|
||||
'ai.chatShortcuts.selectionAction': 'Показывать «Добавить в чат» при выделении в терминале',
|
||||
'ai.chatShortcuts.selectionAction.description': 'Показывать небольшую кнопку AI рядом с выделенным текстом терминала.',
|
||||
|
||||
// AI Error
|
||||
'ai.codex.bridgeError': 'Обработчики главного процесса Codex ещё не загружены. Полностью перезапустите Netcatty или dev-процесс Electron и попробуйте снова.',
|
||||
|
||||
// AI Web Search
|
||||
'ai.webSearch.title': 'Веб-поиск',
|
||||
'ai.webSearch.enable': 'Включить веб-поиск',
|
||||
'ai.webSearch.enable.description': 'Разрешить AI-агенту искать в интернете актуальную информацию.',
|
||||
'ai.webSearch.provider': 'Провайдер поиска',
|
||||
'ai.webSearch.provider.description': 'Выберите провайдера API веб-поиска.',
|
||||
'ai.webSearch.apiKey': 'API-ключ',
|
||||
'ai.webSearch.apiKey.description': 'API-ключ для выбранного провайдера поиска.',
|
||||
'ai.webSearch.apiKey.placeholder': 'Введите API-ключ...',
|
||||
'ai.webSearch.apiHost': 'API Host',
|
||||
'ai.webSearch.apiHost.description': 'Пользовательский API endpoint. Оставьте по умолчанию, если не используете прокси.',
|
||||
'ai.webSearch.apiHost.searxngDescription': 'URL вашего экземпляра SearXNG (обязательно).',
|
||||
'ai.webSearch.maxResults': 'Макс. число результатов',
|
||||
'ai.webSearch.maxResults.description': 'Максимальное количество результатов поиска для возврата (1-20).',
|
||||
|
||||
// AI Safety Settings
|
||||
'ai.safety.title': 'Безопасность',
|
||||
'ai.safety.permissionMode': 'Режим разрешений',
|
||||
'ai.safety.permissionMode.description': 'Управляет тем, как AI взаимодействует с вашими терминалами. Режим наблюдателя блокирует все операции записи через Netcatty и применяется как к встроенным, так и к внешним агентам. Режим подтверждения носит рекомендательный характер для внешних агентов (они управляют собственным потоком одобрения инструментов).',
|
||||
'ai.safety.permissionMode.observer': 'Наблюдатель — только чтение, без действий',
|
||||
'ai.safety.permissionMode.confirm': 'Подтверждение — спрашивать перед действиями',
|
||||
'ai.safety.permissionMode.auto': 'Авто — выполнять свободно',
|
||||
'ai.safety.commandTimeout': 'Тайм-аут команды',
|
||||
'ai.safety.commandTimeout.description': 'Максимальное число секунд, которое команда может выполняться до принудительного завершения. Применяется как к встроенным, так и к внешним агентам.',
|
||||
'ai.safety.commandTimeout.unit': 'с',
|
||||
'ai.safety.maxIterations': 'Макс. число итераций',
|
||||
'ai.safety.maxIterations.description': 'Максимальное число циклов использования инструментов AI, чтобы предотвратить бесконтрольное выполнение. У внешних агентов могут быть собственные внутренние лимиты итераций, имеющие приоритет.',
|
||||
'ai.safety.blocklist': 'Чёрный список команд',
|
||||
'ai.safety.blocklist.description': 'Regex-шаблоны для блокировки опасных команд. Применяется как к встроенным, так и к внешним агентам через механизм выполнения Netcatty.',
|
||||
'ai.safety.blocklist.placeholder': 'Regex-шаблон...',
|
||||
'ai.safety.blocklist.reset': 'Сбросить по умолчанию',
|
||||
'ai.safety.blocklist.add': 'Добавить шаблон',
|
||||
'ai.safety.grants.title': 'Память разрешений',
|
||||
'ai.safety.grants.heading': 'Правила для режима confirm',
|
||||
'ai.safety.grants.description': 'В режиме confirm действие сначала требует подтверждения. Сохранённые правила автоматически разрешают похожие действия во всех терминальных сессиях, их можно редактировать вручную.',
|
||||
'ai.safety.grants.empty': 'Правил пока нет. Выберите «Всегда разрешать» при одобрении или добавьте вручную.',
|
||||
'ai.safety.grants.capability': 'Capability ID',
|
||||
'ai.safety.grants.sessionPattern': 'Шаблон сессии',
|
||||
'ai.safety.grants.commandPattern': 'Шаблон команды (необяз.)',
|
||||
'ai.safety.grants.note': 'Заметка (необяз.)',
|
||||
'ai.safety.grants.add': 'Добавить правило',
|
||||
'ai.safety.grants.remove': 'Удалить',
|
||||
'ai.safety.grants.export': 'Экспорт JSON',
|
||||
'ai.safety.grants.import': 'Импорт JSON',
|
||||
'ai.safety.note': 'Эти настройки безопасности применяются к действиям, выполняемым через Netcatty. Внешние CLI-агенты могут иметь собственные локальные инструменты и собственные правила управления ими.',
|
||||
|
||||
// Unified tooltips for terminal workspace and top tabs (issue #954)
|
||||
'terminal.layer.addTerminal': 'Добавить терминал',
|
||||
'terminal.layer.switchToSplitView': 'Переключить в режим разделения',
|
||||
'terminal.layer.sftp': 'SFTP',
|
||||
'terminal.layer.scripts': 'Скрипты',
|
||||
'terminal.layer.history': 'История',
|
||||
'terminal.layer.theme': 'Тема',
|
||||
'terminal.layer.notes': 'Заметки',
|
||||
'terminal.layer.aiChat': 'AI-чат',
|
||||
'terminal.layer.movePanelLeft': 'Переместить панель влево',
|
||||
'terminal.layer.movePanelRight': 'Переместить панель вправо',
|
||||
'terminal.layer.closePanel': 'Закрыть панель',
|
||||
'terminal.layer.hostTree.search': 'Поиск хостов...',
|
||||
'terminal.layer.hostTree.searchButton': 'Поиск',
|
||||
'terminal.layer.hostTree.tagsButton': 'Фильтр по тегам',
|
||||
'terminal.layer.hostTree.newGroup': 'Новая группа',
|
||||
'terminal.layer.hostTree.localShell': 'Локальная оболочка',
|
||||
'terminal.layer.hostTree.tagsEmpty': 'Нет доступных тегов',
|
||||
'terminal.layer.hostTree.clearTags': 'Сбросить выбор',
|
||||
'terminal.layer.hostTree.collapse': 'Свернуть список хостов',
|
||||
'terminal.layer.hostTree.expand': 'Развернуть список хостов',
|
||||
'terminal.layer.hostTree.empty': 'Хосты не найдены',
|
||||
'topTabs.openQuickSwitcher': 'Открыть быстрый переключатель',
|
||||
'topTabs.moreTabs': 'Больше вкладок',
|
||||
'topTabs.aiAssistant': 'AI-помощник',
|
||||
'topTabs.windowOpacity': 'Прозрачность окна',
|
||||
'topTabs.toggleTheme': 'Переключить тему',
|
||||
'topTabs.openSettings': 'Открыть настройки',
|
||||
'ai.chat.sessionHistory': 'История сессий',
|
||||
'ai.chat.attach': 'Прикрепить',
|
||||
'ai.chat.terminalSelectionAttachment': 'Выделение терминала',
|
||||
'ai.chat.terminalSelectionLines': 'строк: {count}',
|
||||
'ai.chat.collapse': 'Свернуть',
|
||||
'ai.chat.expand': 'Развернуть',
|
||||
'ai.chat.enableAgent': 'Включить {name}',
|
||||
'ai.chat.artifact.noteFallback': 'Заметка Vault',
|
||||
'ai.chat.artifact.openNotes': 'Открыть заметки',
|
||||
'ai.chat.artifact.openHosts': 'Открыть хосты',
|
||||
'ai.chat.artifact.notesSummary': '{count} заметок в Vault',
|
||||
'ai.chat.artifact.hostsSummary': '{count} хостов в Vault',
|
||||
'ai.chat.artifact.hostsAdded': 'Добавлено хостов: {count}',
|
||||
'ai.chat.artifact.hostsPreview': 'Предпросмотр хостов: {count}',
|
||||
'ai.chat.artifact.failed': 'Операция Vault не удалась',
|
||||
'ai.chat.artifact.unavailableTitle': 'Недоступно',
|
||||
'ai.chat.artifact.noteMissing': 'Эта заметка больше не в Vault.',
|
||||
'ai.chat.artifact.hostMissing': 'Этот хост больше не в Vault.',
|
||||
'ai.chat.artifact.snippetMissing': 'Этот snippet или скрипт больше не в Vault.',
|
||||
'ai.chat.artifact.openSnippets': 'Открыть Snippets',
|
||||
'ai.chat.artifact.snippetsSummary': '{count} snippets в Vault',
|
||||
'ai.chat.artifact.scriptsSummary': '{count} scripts в Vault',
|
||||
'ai.chat.artifact.snippetFallback': 'Snippet Vault',
|
||||
'ai.chat.artifact.scriptFallback': 'Automation script',
|
||||
'ai.chat.artifact.scriptLanguage': 'Script {language}',
|
||||
'ai.chat.artifact.snippetDeleted': 'Snippet удален',
|
||||
'ai.chat.artifact.scriptDeleted': 'Script удален',
|
||||
'ai.chat.artifact.snippetRan': 'Snippet выполнен',
|
||||
'ai.chat.artifact.scriptStarted': 'Запуск script начат',
|
||||
'ai.chat.artifact.scriptRunStatus': 'Script run {status}',
|
||||
'ai.chat.artifact.scriptRunsSummary': '{count} script runs',
|
||||
'ai.chat.artifact.scriptRunStopped': 'Script run остановлен',
|
||||
'ai.chat.artifact.scriptRunPaused': 'Script run приостановлен',
|
||||
'ai.chat.artifact.scriptRunResumed': 'Script run возобновлен',
|
||||
'ai.chat.artifact.scriptReference': 'Справка nct API',
|
||||
'zmodem.waitingForRemote': 'Ожидание удалённой стороны...',
|
||||
'zmodem.uploading': 'Загрузка',
|
||||
'zmodem.downloading': 'Скачивание',
|
||||
'zmodem.cancelTransfer': 'Отменить передачу (Ctrl+C)',
|
||||
'zmodem.overwrite.title': 'Remote file already exists',
|
||||
'zmodem.overwrite.applyToRest': 'Apply to remaining conflicts',
|
||||
'zmodem.overwrite.overwrite': 'Overwrite',
|
||||
'zmodem.overwrite.skip': 'Skip',
|
||||
'zmodem.overwrite.cancel': 'Cancel',
|
||||
'settings.shortcuts.resetToDefault': 'Сбросить по умолчанию',
|
||||
};
|
||||
@@ -0,0 +1,768 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const ruCoreMessages: Messages = {
|
||||
// Common
|
||||
'common.save': 'Сохранить',
|
||||
'common.cancel': 'Отмена',
|
||||
'common.close': 'Закрыть',
|
||||
'common.reset': 'Сбросить',
|
||||
'common.zoomIn': 'Увеличить',
|
||||
'common.zoomOut': 'Уменьшить',
|
||||
'common.settings': 'Настройки',
|
||||
'common.search': 'Поиск',
|
||||
'common.searchPlaceholder': 'Поиск...',
|
||||
'common.connect': 'Подключиться',
|
||||
'common.terminal': 'Терминал',
|
||||
'common.create': 'Создать',
|
||||
'common.import': 'Импорт',
|
||||
'common.generate': 'Сгенерировать',
|
||||
'common.delete': 'Удалить',
|
||||
'common.edit': 'Редактировать',
|
||||
'common.clear': 'Очистить',
|
||||
'common.optional': 'Необязательно',
|
||||
'common.selectPlaceholder': 'Выбрать...',
|
||||
'common.add': 'Добавить',
|
||||
'common.rename': 'Переименовать',
|
||||
'common.refresh': 'Обновить',
|
||||
'common.continue': 'Продолжить',
|
||||
'common.enabled': 'Включено',
|
||||
'common.disabled': 'Отключено',
|
||||
'common.error': 'Ошибка',
|
||||
'common.validation': 'Проверка',
|
||||
'common.unknownError': 'Неизвестная ошибка',
|
||||
'common.noResultsFound': 'Ничего не найдено',
|
||||
'common.back': 'Назад',
|
||||
'common.apply': 'Применить',
|
||||
'common.use': 'Использовать',
|
||||
'common.useGlobal': 'Использовать глобальное',
|
||||
'common.saveChanges': 'Сохранить изменения',
|
||||
'common.advanced': 'Дополнительно',
|
||||
'common.left': 'Слева',
|
||||
'common.right': 'Справа',
|
||||
'common.more': 'Ещё',
|
||||
'common.selectAHost': 'Выберите хост',
|
||||
'common.selectAHostPlaceholder': 'Выберите хост...',
|
||||
'sort.manual': 'Ручной порядок',
|
||||
'sort.az': 'А-Я',
|
||||
'sort.za': 'Я-А',
|
||||
'sort.newest': 'Сначала новые',
|
||||
'sort.oldest': 'Сначала старые',
|
||||
'sort.group': 'По группе',
|
||||
'field.label': 'Метка',
|
||||
'field.type': 'Тип',
|
||||
'auth.keyType': 'Тип {type}',
|
||||
'auth.showAllKeys': 'Показать все ключи',
|
||||
|
||||
// Dialogs / prompts
|
||||
'confirm.deleteHost': 'Удалить хост "{name}"?',
|
||||
'confirm.deleteIdentity': 'Удалить идентификатор "{name}"?',
|
||||
'confirm.removeProvider': 'Удалить провайдера "{name}"?',
|
||||
'confirm.closeBusyTerminal.title': 'Подтвердите закрытие',
|
||||
'confirm.closeBusyTerminal.message': 'Процесс "{command}" всё ещё выполняется и будет завершён.',
|
||||
'confirm.closeBusyTerminal.messageWithMore': 'Процесс "{command}" и ещё {count} выполняющихся процесс(ов) будут завершены.',
|
||||
'confirm.closeBusyTerminal.cancel': 'Отмена',
|
||||
'confirm.closeBusyTerminal.close': 'Закрыть',
|
||||
'dialog.createWorkspace.title': 'Создать рабочее пространство',
|
||||
'dialog.renameWorkspace.title': 'Переименовать рабочее пространство',
|
||||
'dialog.renameSession.title': 'Переименовать сессию',
|
||||
'field.name': 'Имя',
|
||||
'field.selectHosts': 'Выбрать хосты',
|
||||
'placeholder.workspaceName': 'Имя рабочего пространства',
|
||||
'placeholder.sessionName': 'Имя сессии',
|
||||
'placeholder.searchHosts': 'Поиск хостов...',
|
||||
'toast.settingsUnavailable': 'Окно настроек недоступно на этой платформе.',
|
||||
'credentials.protectionUnavailable.title': 'Защита учётных данных недоступна',
|
||||
'credentials.protectionUnavailable.message': 'Сохранённые пароли и ключи не могут быть автоматически расшифрованы на этом устройстве. Перед подключением введите учётные данные заново.',
|
||||
'credentials.protectionUnavailable.action': 'Открыть настройки',
|
||||
|
||||
// Settings shell
|
||||
'settings.title': 'Настройки',
|
||||
'settings.tab.application': 'Приложение',
|
||||
'settings.tab.appearance': 'Внешний вид',
|
||||
'settings.tab.terminal': 'Терминал',
|
||||
'settings.tab.shortcuts': 'Горячие клавиши',
|
||||
'settings.tab.syncCloud': 'Синхронизация и облако',
|
||||
'settings.tab.system': 'Система',
|
||||
|
||||
// Settings > System
|
||||
'settings.system.title': 'Система',
|
||||
'settings.system.description': 'Системная информация и управление временными файлами.',
|
||||
'settings.system.tempDirectory': 'Временные файлы',
|
||||
'settings.system.location': 'Расположение',
|
||||
'settings.system.fileCount': 'Файлы',
|
||||
'settings.system.totalSize': 'Размер',
|
||||
'settings.system.openFolder': 'Открыть папку',
|
||||
'settings.system.refresh': 'Обновить',
|
||||
'settings.system.clearTempFiles': 'Очистить временные файлы',
|
||||
'settings.system.clearing': 'Очистка...',
|
||||
'settings.system.clearResult': 'Удалено файлов: {deleted}, ошибок: {failed}.',
|
||||
'settings.system.tempDirectoryHint': 'Временные файлы создаются при открытии удалённых файлов во внешних приложениях. Они автоматически очищаются при закрытии SFTP-сессий.',
|
||||
'settings.system.credentials.title': 'Защита учётных данных',
|
||||
'settings.system.credentials.status': 'Статус',
|
||||
'settings.system.credentials.checking': 'Проверка...',
|
||||
'settings.system.credentials.available': 'Доступно (системное хранилище ключей готово)',
|
||||
'settings.system.credentials.unavailable': 'Недоступно (невозможно расшифровать сохранённые учётные данные)',
|
||||
'settings.system.credentials.unknown': 'Неизвестно (не поддерживается в этой среде)',
|
||||
'settings.system.credentials.unavailableHint': 'Учётные данные, зашифрованные в другом профиле пользователя или на другой машине, здесь расшифровать нельзя. Повторно введите и сохраните их на этом устройстве.',
|
||||
'settings.system.credentials.portabilityHint': 'Облачная синхронизация переносима, потому что использует шифрование вашим мастер-ключом. Локальное шифрование safeStorage привязано к устройству и пользователю.',
|
||||
|
||||
// Settings > System > Network Proxy
|
||||
'settings.system.networkProxy.title': 'Сетевой прокси',
|
||||
'settings.system.networkProxy.description': 'HTTP(S)-прокси для облачной синхронизации и AI-провайдеров. Не связан с SSH ProxyJump / ProxyCommand.',
|
||||
'settings.system.networkProxy.mode': 'Режим прокси',
|
||||
'settings.system.networkProxy.mode.system': 'Системный прокси',
|
||||
'settings.system.networkProxy.mode.direct': 'Напрямую (без прокси)',
|
||||
'settings.system.networkProxy.mode.custom': 'Свой прокси',
|
||||
'settings.system.networkProxy.url': 'URL прокси',
|
||||
'settings.system.networkProxy.url.placeholder': 'http://127.0.0.1:7890',
|
||||
'settings.system.networkProxy.url.desc': 'Поддерживаются URL вида http://, https:// и socks5:// host:port. Учётные данные в URL прокси не поддерживаются.',
|
||||
'settings.system.networkProxy.bypass': 'Список исключений',
|
||||
'settings.system.networkProxy.bypass.placeholder': '<local>,localhost,127.0.0.1',
|
||||
'settings.system.networkProxy.bypass.desc': 'Хосты через запятую, для которых прокси не используется. <local> — localhost и локальные имена.',
|
||||
'settings.system.networkProxy.hint': 'Влияет на вход в Google Drive / OneDrive / GitHub sync и запросы к AI-провайдерам. SSH-подключения не меняет.',
|
||||
|
||||
// Settings > System > Crash Logs
|
||||
'settings.system.crashLogs.title': 'Журналы сбоев',
|
||||
'settings.system.crashLogs.description': 'Просмотр журналов ошибок основного процесса для диагностики неожиданного поведения.',
|
||||
'settings.system.crashLogs.noLogs': 'Журналы сбоев не найдены.',
|
||||
'settings.system.crashLogs.entries': 'Записей: {count}',
|
||||
'settings.system.crashLogs.clear': 'Очистить все журналы',
|
||||
'settings.system.crashLogs.cleared': 'Очищено файлов журналов: {count}.',
|
||||
'settings.system.crashLogs.source': 'Источник',
|
||||
'settings.system.crashLogs.time': 'Время',
|
||||
'settings.system.crashLogs.message': 'Сообщение',
|
||||
'settings.system.crashLogs.stack': 'Трассировка стека',
|
||||
'settings.system.crashLogs.hint': 'Журналы сбоев хранятся 30 дней и автоматически ротируются.',
|
||||
'settings.system.crashLogs.collapse': 'Свернуть',
|
||||
'settings.system.crashLogs.expand': 'Показать детали',
|
||||
|
||||
// Settings > System > Software Update
|
||||
'settings.update.title': 'Обновление программы',
|
||||
'settings.update.currentVersion': 'Текущая версия',
|
||||
'settings.update.checkForUpdates': 'Проверить обновления',
|
||||
'settings.update.checking': 'Проверка...',
|
||||
'settings.update.upToDate': 'Вы используете последнюю версию.',
|
||||
'settings.update.available': 'Доступна новая версия {version}.',
|
||||
'settings.update.download': 'Скачать обновление',
|
||||
'settings.update.downloading': 'Загрузка... {percent}%',
|
||||
'settings.update.readyToInstall': 'Обновление загружено и готово к установке.',
|
||||
'settings.update.restartNow': 'Перезапустить для обновления',
|
||||
'settings.update.error': 'Не удалось проверить наличие обновлений.',
|
||||
'settings.update.downloadError': 'Не удалось загрузить обновление.',
|
||||
'settings.update.manualDownload': 'Скачать с GitHub',
|
||||
'settings.update.manualDownloadHint': 'Автообновление недоступно на этой платформе. Скачайте последнюю версию с GitHub.',
|
||||
'settings.update.hint': 'Netcatty проверяет обновления через GitHub Releases.',
|
||||
'settings.update.lastCheckedJustNow': 'только что',
|
||||
'settings.update.lastCheckedMinutesAgo': '{n} мин назад',
|
||||
'settings.update.lastCheckedHoursAgo': '{n} ч назад',
|
||||
'settings.update.lastCheckedPrefix': 'Последняя проверка: ',
|
||||
'settings.update.autoUpdateEnabled': 'Автоматические обновления',
|
||||
'settings.update.autoUpdateEnabledDesc': 'Автоматически проверять и загружать обновления, когда они доступны.',
|
||||
|
||||
// Settings > Session Logs
|
||||
'settings.sessionLogs.title': 'Журналы сессий',
|
||||
'settings.sessionLogs.description': 'Настройка экспорта журналов сессий и параметров автосохранения.',
|
||||
'settings.sessionLogs.autoSave': 'Автосохранение',
|
||||
'settings.sessionLogs.enableAutoSave': 'Включить автосохранение',
|
||||
'settings.sessionLogs.enableAutoSaveDesc': 'Автоматически сохранять журналы сессий после завершения терминальных сессий.',
|
||||
'settings.sessionLogs.directory': 'Папка сохранения',
|
||||
'settings.sessionLogs.noDirectory': 'Папка не выбрана',
|
||||
'settings.sessionLogs.browse': 'Обзор',
|
||||
'settings.sessionLogs.openFolder': 'Открыть папку',
|
||||
'settings.sessionLogs.directoryHint': 'Журналы будут организованы по хостам во вложенных папках.',
|
||||
'settings.sessionLogs.format': 'Формат журнала',
|
||||
'settings.sessionLogs.formatDesc': 'Выберите формат сохраняемых файлов журналов.',
|
||||
'settings.sessionLogs.formatTxt': 'Обычный текст (.txt)',
|
||||
'settings.sessionLogs.formatRaw': 'Сырые данные с ANSI (.log)',
|
||||
'settings.sessionLogs.formatHtml': 'HTML (.html)',
|
||||
'settings.sessionLogs.timestamps': 'Добавлять метки времени',
|
||||
'settings.sessionLogs.timestampsDesc': 'Добавлять локальное время в начало каждой строки в текстовых и HTML-журналах.',
|
||||
'settings.sessionLogs.hint': 'Журналы сессий сохраняют весь вывод терминала для диагностики и аудита.',
|
||||
|
||||
// Settings > SSH Debug Logs
|
||||
'settings.sshDebugLogs.title': 'Отладочные журналы SSH',
|
||||
'settings.sshDebugLogs.enable': 'Включить отладочные журналы SSH',
|
||||
'settings.sshDebugLogs.enableDesc': 'Записывать подключение, аутентификацию, рукопожатие, отключение и причины ошибок без вывода терминала.',
|
||||
'settings.sshDebugLogs.location': 'Расположение журнала',
|
||||
'settings.sshDebugLogs.status': 'Статус',
|
||||
'settings.sshDebugLogs.statusOn': 'Включено',
|
||||
'settings.sshDebugLogs.statusOff': 'Отключено',
|
||||
'settings.sshDebugLogs.size': 'Размер',
|
||||
'settings.sshDebugLogs.hint': 'Когда включено, новые SSH-подключения записывают диагностические события для разбора бастионов, аутентификации и неожиданных отключений.',
|
||||
|
||||
// Settings > Global Hotkey (Quake Mode)
|
||||
'settings.globalHotkey.title': 'Глобальная горячая клавиша',
|
||||
'settings.globalHotkey.toggleWindow': 'Переключение окна',
|
||||
'settings.globalHotkey.toggleWindowDesc': 'Нажмите сочетание клавиш, чтобы задать глобальную горячую клавишу для показа или скрытия окна.',
|
||||
'settings.globalHotkey.notSet': 'Не задано',
|
||||
'settings.globalHotkey.reset': 'Сбросить по умолчанию',
|
||||
'settings.globalHotkey.closeToTray': 'Сворачивать в системный трей',
|
||||
'settings.globalHotkey.closeToTrayDesc': 'Если включено, при закрытии окно будет сворачиваться в системный трей вместо выхода из приложения.',
|
||||
'settings.globalHotkey.enabled': 'Включить глобальную горячую клавишу',
|
||||
'settings.globalHotkey.enabledDesc': 'Регистрировать системные сочетания клавиш. Когда отключено, все глобальные горячие клавиши снимаются с регистрации.',
|
||||
'settings.globalHotkey.hint': 'Глобальная горячая клавиша работает на уровне всей системы и позволяет быстро показывать или скрывать окно (терминал в стиле Quake).',
|
||||
|
||||
// Tray Panel
|
||||
'tray.openMainWindow': 'Открыть главное окно',
|
||||
'tray.sessions': 'Сессии',
|
||||
'tray.portForwarding': 'Проброс портов',
|
||||
'tray.status.connected': 'Подключено',
|
||||
'tray.status.connecting': 'Подключение',
|
||||
'tray.status.disconnected': 'Отключено',
|
||||
'tray.status.active': 'Активно',
|
||||
'tray.status.inactive': 'Неактивно',
|
||||
'tray.status.error': 'Ошибка',
|
||||
'tray.recentHosts': 'Недавние хосты',
|
||||
'tray.empty.title': 'Пока здесь ничего нет',
|
||||
'tray.empty.subtitle': 'Подключитесь к серверу, они по вам скучают 🚀',
|
||||
'tray.quit': 'Выйти из Netcatty',
|
||||
|
||||
// Vault Sidebar
|
||||
'vault.sidebar.collapse': 'Свернуть боковую панель',
|
||||
'vault.sidebar.expand': 'Развернуть боковую панель',
|
||||
'vault.sidebar.resize': 'Изменить ширину боковой панели',
|
||||
|
||||
// Settings > Application
|
||||
'settings.application.checkUpdates': 'Проверить обновления',
|
||||
'settings.application.reportProblem': 'Сообщить о проблеме',
|
||||
'settings.application.reportProblem.subtitle': 'Создать заранее заполненную задачу на GitHub',
|
||||
'settings.application.community': 'Сообщество',
|
||||
'settings.application.community.subtitle': 'На GitHub Discussions',
|
||||
'settings.application.github': 'GitHub',
|
||||
'settings.application.github.subtitle': 'Исходный код',
|
||||
'settings.application.whatsNew': 'Что нового',
|
||||
'settings.application.whatsNew.subtitle': 'Показать примечания к релизу',
|
||||
'settings.application.openExternal.failedTitle': 'Не удалось открыть ссылку',
|
||||
'settings.application.openExternal.failedBody': 'Не удалось открыть ссылку ни в системном браузере, ни во встроенном окне браузера.',
|
||||
'settings.vault.title': 'Хранилище',
|
||||
'settings.vault.showRecentHosts': 'Показывать недавно подключённые хосты',
|
||||
'settings.vault.showRecentHostsDesc': 'Показывать раздел недавно подключённых хостов в верхней части хранилища',
|
||||
'settings.vault.showOnlyUngroupedHostsInRoot': 'Показывать в корне только хосты без группы',
|
||||
'settings.vault.showOnlyUngroupedHostsInRootDesc': 'Если включено, в корневом списке хостов будут показаны только хосты без группы. Откройте группу на боковой панели, чтобы увидеть сгруппированные хосты.',
|
||||
'settings.vault.showSftpTab': 'Показывать вкладку SFTP',
|
||||
'settings.vault.showSftpTabDesc': 'Показывать отдельный SFTP-вид в верхней панели вкладок. Если скрыто, используйте боковую панель SFTP внутри сессии.',
|
||||
'settings.vault.showHostTreeSidebar': 'Показывать боковую панель хостов',
|
||||
'settings.vault.showHostTreeSidebarDesc': 'Показывать список хостов и кнопку в верхней панели для вкладок терминала и редактора.',
|
||||
|
||||
// Update notifications
|
||||
'update.available.title': 'Доступно обновление',
|
||||
'update.available.message': 'Доступна новая версия {version}. Нажмите, чтобы скачать.',
|
||||
'update.checking': 'Проверка обновлений...',
|
||||
'update.upToDate.title': 'Актуальная версия',
|
||||
'update.upToDate.message': 'У вас установлена последняя версия ({version}).',
|
||||
'update.error': 'Не удалось проверить наличие обновлений',
|
||||
'update.downloadNow': 'Скачать сейчас',
|
||||
'update.viewInSettings': 'Открыть в настройках',
|
||||
'update.readyToInstall.title': 'Обновление готово',
|
||||
'update.readyToInstall.message': 'Версия {version} загружена и готова к установке.',
|
||||
'update.restartNow': 'Перезапустить сейчас',
|
||||
'update.downloadFailed.title': 'Ошибка обновления',
|
||||
'update.downloadFailed.message': 'Не удалось скачать обновление. Вы можете скачать его вручную.',
|
||||
'update.needsSave.title': 'Несохранённые изменения',
|
||||
'update.needsSave.message': 'Сначала сохраните открытые редакторы, затем снова нажмите «Перезапустить сейчас», чтобы установить обновление.',
|
||||
'update.openReleases': 'Открыть релизы',
|
||||
'update.remindLater': 'Напомнить позже',
|
||||
'update.skipVersion': 'Пропустить эту версию',
|
||||
|
||||
// Settings > Appearance
|
||||
'settings.appearance.uiTheme': 'Тема интерфейса',
|
||||
'settings.appearance.theme': 'Тема',
|
||||
'settings.appearance.theme.desc': 'Выберите светлую, тёмную тему или следование системным настройкам',
|
||||
'settings.appearance.theme.light': 'Светлая',
|
||||
'settings.appearance.theme.dark': 'Тёмная',
|
||||
'settings.appearance.theme.system': 'Системная',
|
||||
'settings.appearance.accentColor': 'Акцентный цвет',
|
||||
'settings.appearance.customColor': 'Пользовательский цвет',
|
||||
'settings.appearance.accentColor.mode': 'Использовать свой акцент',
|
||||
'settings.appearance.accentColor.mode.desc': 'Переопределить акцентный цвет темы',
|
||||
'settings.appearance.accentColor.custom': 'Пользовательский акцент',
|
||||
'settings.appearance.themeColor': 'Цвет темы',
|
||||
'settings.appearance.themeColor.desc': 'Выберите готовую палитру для каждой темы',
|
||||
'settings.appearance.themeColor.light': 'Палитра светлой темы',
|
||||
'settings.appearance.themeColor.dark': 'Палитра тёмной темы',
|
||||
'settings.appearance.customCss': 'Пользовательский CSS',
|
||||
'settings.appearance.customCss.desc':
|
||||
'Добавьте пользовательский CSS, чтобы настроить внешний вид приложения. Изменения применяются сразу. Основные области интерфейса имеют атрибут [data-section="..."], который можно использовать для выбора элементов, например: snippets-panel, host-details-panel, group-details-panel, serial-host-details-panel, ai-chat-panel, vault-sidebar, vault-main, vault-hosts-header, vault-host-list, vault-view, terminal-workspace, terminal-workspace-sidebar (список терминалов в режиме Focus), terminal-host-tree-sidebar, terminal-host-tree-sidebar-content, terminal-host-tree-sidebar-row, terminal-side-panel (панель SFTP/скриптов/темы/AI, доступна пока открыта), terminal-side-panel-tabs, terminal-side-panel-content, terminal-sftp-panel, terminal-sftp-host-header, terminal-sftp-pane, terminal-sftp-toolbar, terminal-sftp-path, terminal-sftp-filter-bar, terminal-sftp-list, terminal-sftp-list-header, terminal-sftp-list-row, terminal-sftp-tree, terminal-sftp-tree-row, terminal-sftp-transfer-queue, terminal-sftp-transfer-row, terminal-split-pane, terminal-split-resizer, top-tabs, top-tabs-host-tree-toggle, top-tabs-quick-switcher-toggle.',
|
||||
'settings.appearance.customCss.placeholder':
|
||||
'/* Примеры — используйте !important, чтобы переопределить специфичность утилит Tailwind */\n\n/* Скрыть переключатель списка хостов в верхней панели вкладок */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* Скрыть кнопку плюса, открывающую быстрый переключатель */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* Рамка вокруг боковой панели SFTP (не остаётся после закрытия) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* Изменить фон всей боковой панели, а не только верхних вкладок */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* Настроить выбранные строки SFTP */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* Более заметные разделители сплита */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* Подсветка активной панели сплита */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* Или: Настройки → Терминал → Индикатор фокуса → Рамка вокруг активной панели */',
|
||||
'settings.appearance.language': 'Язык',
|
||||
'settings.appearance.language.desc': 'Выберите язык интерфейса',
|
||||
'settings.appearance.uiFont': 'Шрифт интерфейса',
|
||||
'settings.appearance.uiFont.desc': 'Выберите шрифт для интерфейса приложения',
|
||||
'settings.appearance.windowOpacity': 'Прозрачность окна',
|
||||
'settings.appearance.windowOpacity.desc': 'Настройте прозрачность всего окна приложения. При низких значениях текст терминала тоже бледнеет. В некоторых средах Linux это может не поддерживаться.',
|
||||
'settings.appearance.appIcon': 'Значок приложения',
|
||||
'settings.appearance.appIcon.desc': 'Выберите стиль значка для Dock/панели задач, чтобы Netcatty было проще найти при переключении приложений. Применяется во время работы; значок установленного пакета не меняется.',
|
||||
'settings.appearance.appIcon.original': 'Оригинал',
|
||||
'settings.appearance.appIcon.bright': 'Яркий',
|
||||
'settings.appearance.appIcon.dark': 'Тёмный',
|
||||
'settings.appearance.appIcon.colorful': 'Цветной',
|
||||
'settings.appearance.appIcon.highContrast': 'Высокий контраст',
|
||||
'settings.appearance.appIcon.group.classic': 'Цветной фон',
|
||||
'settings.appearance.appIcon.group.white': 'Белый фон',
|
||||
'settings.appearance.appIcon.group.special': 'Скрытые',
|
||||
'settings.appearance.appIcon.whiteNavy': 'Белый · синий кот',
|
||||
'settings.appearance.appIcon.whiteSky': 'Белый · голубой кот',
|
||||
'settings.appearance.appIcon.whiteRose': 'Белый · розовый кот',
|
||||
'settings.appearance.appIcon.whiteEmerald': 'Белый · изумрудный кот',
|
||||
'settings.appearance.appIcon.whiteAmber': 'Белый · янтарный кот',
|
||||
'settings.appearance.appIcon.whiteViolet': 'Белый · фиолетовый кот',
|
||||
'settings.appearance.appIcon.rainbow': 'Радужный кот',
|
||||
// Settings > Terminal
|
||||
'settings.terminal.section.theme': 'Тема терминала',
|
||||
'settings.terminal.themeModal.title': 'Выберите тему',
|
||||
'settings.terminal.themeModal.darkThemes': 'Тёмные темы',
|
||||
'settings.terminal.themeModal.lightThemes': 'Светлые темы',
|
||||
'settings.terminal.theme.selectButton': 'Выбрать тему',
|
||||
'settings.terminal.theme.followApp': 'Следовать теме приложения',
|
||||
'settings.terminal.theme.followApp.desc': 'Автоматически подбирать фон терминала под текущую тему приложения для более цельного вида.',
|
||||
'settings.terminal.theme.darkTheme': 'Тема терминала для тёмного режима',
|
||||
'settings.terminal.theme.lightTheme': 'Тема терминала для светлого режима',
|
||||
'settings.terminal.theme.auto': 'Авто (как тема приложения)',
|
||||
'settings.terminal.theme.autoDesc': 'Следует активному пресету темы интерфейса',
|
||||
'settings.terminal.section.font': 'Шрифт',
|
||||
'settings.terminal.section.cursor': 'Курсор',
|
||||
'settings.terminal.section.keyboard': 'Клавиатура',
|
||||
'settings.terminal.section.accessibility': 'Доступность',
|
||||
'settings.terminal.section.behavior': 'Поведение',
|
||||
'settings.terminal.section.scrollback': 'Буфер прокрутки',
|
||||
'settings.terminal.section.keywordHighlight': 'Подсветка ключевых слов',
|
||||
'settings.terminal.font.family': 'Шрифт',
|
||||
'settings.terminal.font.family.desc': 'Семейство шрифта терминала',
|
||||
'settings.terminal.font.cjk': 'Шрифт CJK',
|
||||
'settings.terminal.font.cjk.desc': 'Шрифт для китайских, японских и корейских символов; вариант "Авто" выбирает подходящий шрифт на основе основного',
|
||||
'settings.terminal.font.cjk.option.auto': 'Авто · в паре с основным шрифтом',
|
||||
'settings.terminal.font.cjk.option.sarasaSC': 'Sarasa Mono SC (Iosevka + Source Han SC)',
|
||||
'settings.terminal.font.cjk.option.sarasaTC': 'Sarasa Mono TC (Iosevka + Source Han TC)',
|
||||
'settings.terminal.font.cjk.option.mapleCN': 'Maple Mono CN',
|
||||
'settings.terminal.font.cjk.option.sourceHan': 'Source Han Mono SC',
|
||||
'settings.terminal.font.cjk.option.notoCJK': 'Noto Sans Mono CJK SC',
|
||||
'settings.terminal.font.cjk.option.lxgwWenkai': 'LXGW WenKai Mono',
|
||||
'settings.terminal.font.cjk.option.simSun': 'SimSun',
|
||||
'settings.terminal.font.cjk.option.legacy': '{font} · не рекомендуется (пропорциональный шрифт)',
|
||||
'settings.terminal.font.size': 'Размер шрифта',
|
||||
'settings.terminal.font.size.desc': 'Размер текста терминала',
|
||||
'settings.terminal.font.weight': 'Толщина шрифта',
|
||||
'settings.terminal.font.weight.desc': 'Толщина обычного текста (100-900)',
|
||||
'settings.terminal.font.weight.thin': 'Тонкий',
|
||||
'settings.terminal.font.weight.extraLight': 'Очень светлый',
|
||||
'settings.terminal.font.weight.light': 'Светлый',
|
||||
'settings.terminal.font.weight.normal': 'Обычный',
|
||||
'settings.terminal.font.weight.medium': 'Средний',
|
||||
'settings.terminal.font.weight.semiBold': 'Полужирный',
|
||||
'settings.terminal.font.weight.bold': 'Жирный',
|
||||
'settings.terminal.font.weight.extraBold': 'Очень жирный',
|
||||
'settings.terminal.font.weight.black': 'Максимально жирный',
|
||||
'settings.terminal.font.weightBold': 'Толщина жирного шрифта',
|
||||
'settings.terminal.font.weightBold.desc': 'Толщина жирного текста (100-900)',
|
||||
'settings.terminal.font.smoothing': 'Сглаживание шрифта',
|
||||
'settings.terminal.font.smoothing.desc': 'Использовать родное сглаживание шрифтов macOS',
|
||||
'settings.terminal.font.linePadding': 'Межстрочный отступ',
|
||||
'settings.terminal.font.linePadding.desc': 'Дополнительное пространство между строками (0-10)',
|
||||
'settings.terminal.font.emulationType': 'Тип эмуляции терминала',
|
||||
'settings.terminal.cursor.style': 'Стиль курсора',
|
||||
'settings.terminal.cursor.style.block': 'Блок',
|
||||
'settings.terminal.cursor.style.bar': 'Полоса',
|
||||
'settings.terminal.cursor.style.underline': 'Подчёркивание',
|
||||
'settings.terminal.cursor.blink': 'Мигание курсора',
|
||||
'settings.terminal.keyboard.altAsMeta': 'Использовать Option как клавишу Meta',
|
||||
'settings.terminal.keyboard.altAsMeta.desc':
|
||||
'Использовать Option (Alt) как клавишу Meta вместо ввода специальных символов',
|
||||
'settings.terminal.keyboard.optionArrowWordJump': 'Option+←/→ переход по словам',
|
||||
'settings.terminal.keyboard.optionArrowWordJump.desc':
|
||||
'Отправлять Meta-b / Meta-f при Option+Влево/Вправо, чтобы оболочка перемещалась по словам, вместо стандартного ^[[1;3D / ^[[1;3C',
|
||||
'settings.terminal.accessibility.minimumContrastRatio': 'Минимальный коэффициент контрастности',
|
||||
'settings.terminal.accessibility.minimumContrastRatio.desc':
|
||||
'Подстраивать цвета под требования контрастности (1 = отключено, 21 = максимум)',
|
||||
'settings.terminal.behavior.rightClick': 'Поведение правой кнопки мыши',
|
||||
'settings.terminal.behavior.rightClick.desc': 'Действие при щелчке правой кнопкой в терминале',
|
||||
'settings.terminal.behavior.rightClick.menu': 'Показать меню',
|
||||
'settings.terminal.behavior.rightClick.paste': 'Вставить',
|
||||
'settings.terminal.behavior.rightClick.selectWord': 'Выбрать слово',
|
||||
'settings.terminal.behavior.copyOnSelect': 'Копировать при выделении',
|
||||
'settings.terminal.behavior.copyOnSelect.desc': 'Автоматически копировать выделенный текст. В tmux/vim с режимом мыши удерживайте Option на macOS или Shift на Windows/Linux для выделения',
|
||||
'settings.terminal.behavior.middleClickPaste': 'Вставка средней кнопкой мыши',
|
||||
'settings.terminal.behavior.middleClickPaste.desc':
|
||||
'Вставлять содержимое буфера обмена по щелчку средней кнопкой',
|
||||
'settings.terminal.behavior.middleClick': 'Поведение средней кнопки мыши',
|
||||
'settings.terminal.behavior.middleClick.desc': 'Действие при щелчке средней кнопкой в терминале',
|
||||
'settings.terminal.behavior.middleClick.menu': 'Показать меню',
|
||||
'settings.terminal.behavior.middleClick.paste': 'Вставить',
|
||||
'settings.terminal.behavior.middleClick.disabled': 'Ничего не делать',
|
||||
'settings.terminal.behavior.wordSeparators': 'Разделители слов',
|
||||
'settings.terminal.behavior.wordSeparators.desc':
|
||||
'Символы, которые считаются границами слова при выделении двойным щелчком. Пробел тоже учитывается; добавьте =, запятую или : для строк вроде uid=name.',
|
||||
'settings.terminal.behavior.bracketedPaste': 'Режим bracketed paste',
|
||||
'settings.terminal.behavior.bracketedPaste.desc':
|
||||
'Оборачивать вставляемый текст escape-последовательностями, чтобы оболочка отличала вставку от обычного ввода. Отключите, если видите артефакты вида ^[[200~.',
|
||||
'settings.terminal.behavior.shiftEnterNewline': 'Текст для Shift+Enter',
|
||||
'settings.terminal.behavior.shiftEnterNewline.desc':
|
||||
'Отправлять настроенный текст вместо обычного Enter при нажатии Shift+Enter в терминале.',
|
||||
'settings.terminal.behavior.shiftEnterNewlineText': 'Отправляемый текст',
|
||||
'settings.terminal.behavior.shiftEnterNewlineText.desc':
|
||||
'Используйте \\n для перевода строки, \\t для табуляции и \\\\ для обратной косой черты.',
|
||||
'settings.terminal.behavior.clearWipesScrollback': '`clear` очищает буфер прокрутки',
|
||||
'settings.terminal.behavior.clearWipesScrollback.desc':
|
||||
'Команда `clear` также будет очищать буфер прокрутки (поведение POSIX по умолчанию). Отключите, чтобы история оставалась видимой после `clear`.',
|
||||
'settings.terminal.behavior.preserveSelectionOnInput': 'Сохранять выделение при вводе',
|
||||
'settings.terminal.behavior.preserveSelectionOnInput.desc':
|
||||
'Не сбрасывать выделенный мышью текст при вводе. Это удобно, например, чтобы выделить путь и вставить его после префикса команды вроде `sz `.',
|
||||
'settings.terminal.behavior.forcePromptNewLine': 'Переносить приглашение на новую строку',
|
||||
'settings.terminal.behavior.forcePromptNewLine.desc':
|
||||
'Если последняя строка вывода команды не завершена переводом строки, переносить распознанное приглашение оболочки на следующую визуальную строку.',
|
||||
'settings.terminal.behavior.dynamicTabTitle': 'Динамический заголовок вкладки',
|
||||
'settings.terminal.behavior.dynamicTabTitle.desc':
|
||||
'Выберите, когда вкладки сеансов должны следовать заголовкам окна, сообщаемым оболочкой.',
|
||||
'settings.terminal.behavior.dynamicTabTitle.off': 'Отключено',
|
||||
'settings.terminal.behavior.dynamicTabTitle.agent': 'Только агенты',
|
||||
'settings.terminal.behavior.dynamicTabTitle.all': 'Все сеансы',
|
||||
'settings.terminal.behavior.osc52Clipboard': 'Буфер обмена OSC-52',
|
||||
'settings.terminal.behavior.osc52Clipboard.desc':
|
||||
'Разрешить удалённым программам (tmux, vim и т. д.) доступ к локальному буферу обмена через escape-последовательности OSC-52.',
|
||||
'settings.terminal.behavior.osc52Clipboard.off': 'Отключено',
|
||||
'settings.terminal.behavior.osc52Clipboard.writeOnly': 'Только запись',
|
||||
'settings.terminal.behavior.osc52Clipboard.readWrite': 'Чтение и запись',
|
||||
'settings.terminal.behavior.osc52Clipboard.prompt': 'Запись + запрос при чтении',
|
||||
'terminal.osc52.readPrompt.title': 'Запрос чтения буфера обмена',
|
||||
'terminal.osc52.readPrompt.desc': 'Удалённая программа запрашивает чтение вашего буфера обмена. Разрешить?',
|
||||
'terminal.osc52.readPrompt.allow': 'Разрешить',
|
||||
'terminal.osc52.readPrompt.deny': 'Запретить',
|
||||
'settings.terminal.behavior.scrollOnInput': 'Прокручивать при вводе',
|
||||
'settings.terminal.behavior.scrollOnInput.desc': 'Прокручивать терминал вниз при наборе текста',
|
||||
'settings.terminal.behavior.scrollOnOutput': 'Прокручивать при выводе',
|
||||
'settings.terminal.behavior.scrollOnOutput.desc':
|
||||
'Прокручивать терминал вниз при появлении нового вывода',
|
||||
'settings.terminal.behavior.scrollOnKeyPress': 'Прокручивать при нажатии клавиш',
|
||||
'settings.terminal.behavior.scrollOnKeyPress.desc':
|
||||
'Прокручивать терминал вниз при нажатии клавиши (например, Enter)',
|
||||
'settings.terminal.behavior.scrollOnPaste': 'Прокручивать при вставке',
|
||||
'settings.terminal.behavior.scrollOnPaste.desc':
|
||||
'Прокручивать терминал вниз при вставке текста',
|
||||
'settings.terminal.behavior.smoothScrolling': 'Плавная прокрутка',
|
||||
'settings.terminal.behavior.smoothScrolling.desc':
|
||||
'Анимировать прокрутку области терминала вместо мгновенного перехода',
|
||||
'settings.terminal.behavior.linkModifier': 'Клавиша-модификатор для ссылок',
|
||||
'settings.terminal.behavior.linkModifier.desc': 'Удерживайте эту клавишу, чтобы нажимать на ссылки в терминале',
|
||||
'settings.terminal.behavior.linkModifier.none': 'Нет (нажимать напрямую)',
|
||||
'settings.terminal.behavior.linkModifier.ctrl': 'Ctrl',
|
||||
'settings.terminal.behavior.linkModifier.alt': 'Alt / Option',
|
||||
'settings.terminal.behavior.linkModifier.meta': 'Cmd / Win',
|
||||
'settings.terminal.scrollback.desc': 'Ограничение количества строк терминала. Установите 0, чтобы снять ограничение.',
|
||||
'settings.terminal.scrollback.rows': 'Количество строк *',
|
||||
'settings.terminal.section.startupCommand': 'Команда запуска',
|
||||
'settings.terminal.startupCommandDelay.label': 'Задержка команды запуска (мс)',
|
||||
'settings.terminal.startupCommandDelay.desc': 'Сколько ждать после подключения перед отправкой команды запуска. Также используется между строками, если команда запуска многострочная. Увеличьте для медленных соединений.',
|
||||
'settings.terminal.keywordHighlight.title': 'Подсветка ключевых слов',
|
||||
'settings.terminal.keywordHighlight.resetColors': 'Сбросить цвета по умолчанию',
|
||||
'settings.terminal.keywordHighlight.resetDefaults': 'Сбросить встроенные правила по умолчанию',
|
||||
'settings.terminal.keywordHighlight.resetBuiltIn': 'Восстановить стандартную метку и шаблоны',
|
||||
'settings.terminal.keywordHighlight.addCustom': 'Добавить своё правило',
|
||||
'settings.terminal.keywordHighlight.editCustom': 'Редактировать правило',
|
||||
'settings.terminal.keywordHighlight.editBuiltIn': 'Редактировать встроенное правило',
|
||||
'settings.terminal.keywordHighlight.labelField': 'Метка и цвет',
|
||||
'settings.terminal.keywordHighlight.labelPlaceholder': 'Метка (например, Down)',
|
||||
'settings.terminal.keywordHighlight.patternField': 'Шаблоны Regex',
|
||||
'settings.terminal.keywordHighlight.patternPlaceholder': 'Один regex на строку (например, \\bdown\\b)',
|
||||
'settings.terminal.keywordHighlight.patternHint': 'Один regex на строку. Шаблоны сопоставляются без учёта регистра с глобальным флагом.',
|
||||
'settings.terminal.keywordHighlight.invalidPattern': 'Некорректный regex-шаблон',
|
||||
'settings.terminal.keywordHighlight.preview': 'Предпросмотр',
|
||||
'settings.terminal.section.localShell': 'Локальная оболочка',
|
||||
'settings.terminal.localShell.shell': 'Исполняемый файл оболочки',
|
||||
'settings.terminal.localShell.shell.desc': 'Путь к исполняемому файлу оболочки (например, /bin/zsh, pwsh.exe). Оставьте пустым, чтобы использовать системную оболочку по умолчанию.',
|
||||
'settings.terminal.localShell.shell.placeholder': 'Системная по умолчанию',
|
||||
'settings.terminal.localShell.shell.detected': 'Обнаружено',
|
||||
'settings.terminal.localShell.shell.notFound': 'Исполняемый файл оболочки не найден',
|
||||
'settings.terminal.localShell.shell.isDirectory': 'Путь указывает на каталог, а не на исполняемый файл',
|
||||
'settings.terminal.localShell.shell.default': 'Системная по умолчанию',
|
||||
'settings.terminal.localShell.shell.custom': 'Пользовательская...',
|
||||
'settings.terminal.localShell.shell.customPath': 'Путь к исполняемому файлу оболочки',
|
||||
'settings.terminal.localShell.shell.customArgs': 'Аргументы запуска',
|
||||
'settings.terminal.localShell.shell.customArgs.placeholder': 'напр. --login -i',
|
||||
'settings.terminal.localShell.shell.customArgs.desc': 'Аргументы, передаваемые оболочке. Некоторым оболочкам они необходимы — например, msys2 bash требует --login -i для загрузки окружения.',
|
||||
'settings.terminal.localShell.shell.commonPaths': 'Частые пути',
|
||||
'settings.terminal.localShell.shell.pathValid': 'Путь корректен',
|
||||
'settings.terminal.localShell.startDir': 'Начальный каталог',
|
||||
'settings.terminal.localShell.startDir.desc': 'Каталог, в котором будет открываться локальный терминал. Оставьте пустым, чтобы использовать домашний каталог.',
|
||||
'settings.terminal.localShell.startDir.placeholder': 'Домашний каталог',
|
||||
'settings.terminal.localShell.startDir.notFound': 'Каталог не найден',
|
||||
'settings.terminal.localShell.startDir.isFile': 'Путь указывает на файл, а не на каталог',
|
||||
'settings.terminal.section.connection': 'Подключение',
|
||||
'settings.terminal.connection.keepaliveInterval': 'Интервал keepalive',
|
||||
'settings.terminal.connection.keepaliveInterval.desc': 'Как часто (в секундах) отправлять keepalive-пакеты на уровне SSH. Установите 0, чтобы отключить глобально. Учтите, что отдельные хосты могут переопределять это значение в своих настройках.',
|
||||
'settings.terminal.connection.keepaliveCountMax': 'Макс. число пропущенных keepalive',
|
||||
'settings.terminal.connection.keepaliveCountMax.desc': 'Количество пропущенных keepalive, после которого соединение считается мёртвым. Более высокие значения лучше переносят краткие сетевые сбои и медленные ответы SSH-серверов.',
|
||||
'settings.terminal.connection.x11Display': 'Дисплей X11',
|
||||
'settings.terminal.connection.x11Display.desc': 'Необязательный адрес локального дисплея для перенаправления X11. Оставьте пустым, чтобы использовать системное значение по умолчанию.',
|
||||
'settings.terminal.connection.x11Display.placeholder': 'Авто (:0 или DISPLAY)',
|
||||
'settings.terminal.section.serverStats': 'Статистика сервера (Linux)',
|
||||
'settings.terminal.section.systemManager': 'Системный менеджер',
|
||||
'settings.terminal.systemManager.processRefreshInterval': 'Обновление списка процессов',
|
||||
'settings.terminal.systemManager.processRefreshInterval.desc': 'Как часто обновлять список процессов в боковой панели системного менеджера.',
|
||||
'settings.terminal.systemManager.tmuxRefreshInterval': 'Обновление сессий tmux',
|
||||
'settings.terminal.systemManager.tmuxRefreshInterval.desc': 'Как часто обновлять список сессий tmux.',
|
||||
'settings.terminal.systemManager.dockerListRefreshInterval': 'Обновление списка контейнеров Docker',
|
||||
'settings.terminal.systemManager.dockerListRefreshInterval.desc': 'Как часто обновлять список контейнеров Docker.',
|
||||
'settings.terminal.systemManager.dockerStatsRefreshInterval': 'Обновление статистики Docker',
|
||||
'settings.terminal.systemManager.dockerStatsRefreshInterval.desc': 'Как часто обновлять CPU/память/сеть контейнеров Docker.',
|
||||
'settings.terminal.serverStats.show': 'Показывать статистику сервера',
|
||||
'settings.terminal.serverStats.show.desc': 'Показывать загрузку CPU, памяти и диска в строке состояния терминала (только для Linux-серверов).',
|
||||
'settings.terminal.serverStats.refreshInterval': 'Интервал обновления',
|
||||
'settings.terminal.serverStats.refreshInterval.desc': 'Как часто обновлять статистику сервера.',
|
||||
'settings.terminal.serverStats.seconds': 'секунд',
|
||||
|
||||
// Settings > Terminal > Rendering
|
||||
'settings.terminal.section.rendering': 'Рендеринг',
|
||||
'settings.terminal.rendering.renderer': 'Рендерер',
|
||||
'settings.terminal.rendering.renderer.desc': 'Выберите технологию рендеринга терминала. В режиме "Авто" на устройствах с малым объёмом памяти будет использоваться DOM. Изменения применяются к новым терминальным сессиям.',
|
||||
'settings.terminal.rendering.auto': 'Авто',
|
||||
|
||||
// Settings > Terminal > Workspace Focus Indicator
|
||||
'settings.terminal.section.workspaceFocus': 'Индикатор фокуса рабочей области',
|
||||
'settings.terminal.workspaceFocus.style': 'Стиль индикатора фокуса',
|
||||
'settings.terminal.workspaceFocus.style.desc': 'Как показывать, какая панель активна в режиме разделённого вида.',
|
||||
'settings.terminal.workspaceFocus.dim': 'Затемнять неактивные панели',
|
||||
'settings.terminal.workspaceFocus.border': 'Рамка вокруг активной панели',
|
||||
|
||||
// Settings > Terminal > Autocomplete
|
||||
'settings.terminal.section.autocomplete': 'Автодополнение',
|
||||
'settings.terminal.autocomplete.enabled': 'Включить автодополнение',
|
||||
'settings.terminal.autocomplete.enabled.desc': 'Показывать подсказки команд на основе истории и описаний команд во время ввода.',
|
||||
'settings.terminal.autocomplete.ghostText': 'Призрачный текст',
|
||||
'settings.terminal.autocomplete.ghostText.desc': 'Показывать серую встроенную подсказку после курсора (как в fish shell).',
|
||||
'settings.terminal.autocomplete.popupMenu': 'Всплывающее меню',
|
||||
'settings.terminal.autocomplete.popupMenu.desc': 'Показывать плавающий список из нескольких подсказок.',
|
||||
|
||||
// Settings > Shortcuts
|
||||
'settings.shortcuts.section.scheme': 'Схема горячих клавиш',
|
||||
'settings.shortcuts.scheme.label': 'Сочетания клавиш',
|
||||
'settings.shortcuts.scheme.desc': 'Выберите раскладку клавиш для использования в сочетаниях',
|
||||
'settings.shortcuts.scheme.disabled': 'Отключено',
|
||||
'settings.shortcuts.scheme.mac': 'Mac (Cmd)',
|
||||
'settings.shortcuts.scheme.pc': 'PC (Ctrl)',
|
||||
'settings.shortcuts.disableTerminalFontZoom.label': 'Отключить масштаб терминала',
|
||||
'settings.shortcuts.disableTerminalFontZoom.desc': 'Отключает быстрый масштаб текста в терминале, включая Cmd/Ctrl + колесо мыши.',
|
||||
'settings.shortcuts.shellOnlyTabNumberShortcuts.label': 'Сочетания без закреплённых вкладок',
|
||||
'settings.shortcuts.shellOnlyTabNumberShortcuts.desc': 'Если включено, Cmd/Ctrl+[1...9] и Ctrl+Tab переключают только рабочие вкладки (терминалы, рабочие области, редакторы), а не закреплённые Vault и SFTP.',
|
||||
'settings.shortcuts.section.custom': 'Пользовательские сочетания',
|
||||
'settings.shortcuts.resetAll': 'Сбросить все',
|
||||
'settings.shortcuts.recording': 'Нажмите клавиши...',
|
||||
'settings.shortcuts.none': 'Нет',
|
||||
'settings.shortcuts.setDisabled': 'Отключить',
|
||||
'settings.shortcuts.category.tabs': 'Вкладки',
|
||||
'settings.shortcuts.category.terminal': 'Терминал',
|
||||
'settings.shortcuts.category.navigation': 'Навигация',
|
||||
'settings.shortcuts.category.app': 'Приложение',
|
||||
'settings.shortcuts.category.sftp': 'SFTP',
|
||||
// Settings > Shortcuts -> key bings
|
||||
'settings.shortcuts.binding.switch-tab-1-9': 'Переключиться на вкладку [1...9]',
|
||||
'settings.shortcuts.binding.next-tab': 'Следующая вкладка',
|
||||
'settings.shortcuts.binding.prev-tab': 'Предыдущая вкладка',
|
||||
'settings.shortcuts.binding.close-tab': 'Закрыть вкладку',
|
||||
'settings.shortcuts.binding.close-session': 'Закрыть панель сессии',
|
||||
'settings.shortcuts.binding.new-tab': 'Новая локальная вкладка',
|
||||
'settings.shortcuts.binding.copy': 'Копировать из терминала',
|
||||
'settings.shortcuts.binding.paste': 'Вставить в терминал',
|
||||
'settings.shortcuts.binding.paste-selection': 'Вставить выделение в терминал',
|
||||
'settings.shortcuts.binding.select-all': 'Выделить всё содержимое терминала',
|
||||
'settings.shortcuts.binding.clear-buffer': 'Очистить буфер терминала',
|
||||
'settings.shortcuts.binding.search-terminal': 'Открыть поиск по терминалу',
|
||||
'settings.shortcuts.binding.increase-terminal-font-size': 'Увеличить размер шрифта терминала',
|
||||
'settings.shortcuts.binding.decrease-terminal-font-size': 'Уменьшить размер шрифта терминала',
|
||||
'settings.shortcuts.binding.reset-terminal-font-size': 'Сбросить размер шрифта терминала',
|
||||
'settings.shortcuts.binding.move-focus': 'Переместить фокус между разделёнными окнами',
|
||||
'settings.shortcuts.binding.split-horizontal': 'Горизонтальное разделение',
|
||||
'settings.shortcuts.binding.split-vertical': 'Вертикальное разделение',
|
||||
'settings.shortcuts.binding.toggle-pane-zoom': 'Переключить масштаб панели',
|
||||
'settings.shortcuts.binding.open-hosts': 'Открыть список хостов',
|
||||
'settings.shortcuts.binding.open-local': 'Открыть локальный терминал',
|
||||
'settings.shortcuts.binding.open-sftp': 'Открыть SFTP',
|
||||
'settings.shortcuts.binding.open-settings': 'Открыть настройки',
|
||||
'settings.shortcuts.binding.port-forwarding': 'Открыть перенаправление портов',
|
||||
'settings.shortcuts.binding.command-palette': 'Открыть палитру команд',
|
||||
'settings.shortcuts.binding.quick-switch': 'Быстрое переключение',
|
||||
'settings.shortcuts.binding.new-workspace': 'Новая рабочая область',
|
||||
'settings.shortcuts.binding.snippets': 'Открыть сниппеты',
|
||||
'settings.shortcuts.binding.broadcast': 'Переключить режим трансляции',
|
||||
'settings.shortcuts.binding.toggle-side-panel': 'Переключить боковую панель',
|
||||
'settings.shortcuts.binding.sftp-copy': 'Копировать файл',
|
||||
'settings.shortcuts.binding.sftp-cut': 'Вырезать файл',
|
||||
'settings.shortcuts.binding.sftp-paste': 'Вставить файл',
|
||||
'settings.shortcuts.binding.sftp-select-all': 'Выделить все файлы',
|
||||
'settings.shortcuts.binding.sftp-rename': 'Переименовать файл',
|
||||
'settings.shortcuts.binding.sftp-delete': 'Удалить файл',
|
||||
'settings.shortcuts.binding.sftp-refresh': 'Обновить',
|
||||
'settings.shortcuts.binding.sftp-new-folder': 'Создать новую папку',
|
||||
'settings.shortcuts.binding.sftp-open': 'Открыть файл / Войти в директорию',
|
||||
'settings.shortcuts.binding.sftp-go-parent': 'Перейти в родительскую директорию',
|
||||
'settings.shortcuts.binding.sftp-navigate-to': 'Перейти в выбранную директорию',
|
||||
|
||||
// Context menus / common actions
|
||||
'action.newHost': 'Новый хост',
|
||||
'action.newSubfolder': 'Новая подпапка',
|
||||
'action.copyPublicKey': 'Копировать публичный ключ',
|
||||
'action.keyExport': 'Экспорт ключа',
|
||||
'action.edit': 'Редактировать',
|
||||
'action.delete': 'Удалить',
|
||||
'action.duplicate': 'Дублировать',
|
||||
'action.open': 'Открыть',
|
||||
'action.copy': 'Копировать',
|
||||
'action.run': 'Запустить',
|
||||
'action.start': 'Старт',
|
||||
'action.stop': 'Остановить',
|
||||
'action.remove': 'Убрать',
|
||||
'action.convertToHost': 'Преобразовать в хост',
|
||||
|
||||
// Sync
|
||||
'sync.cloudSync': 'Облачная синхронизация',
|
||||
'sync.settings': 'Настройки синхронизации',
|
||||
'sync.active': 'Облачная синхронизация активна',
|
||||
'sync.pending': 'Есть несинхронизированные изменения',
|
||||
'sync.syncing': 'Синхронизация...',
|
||||
'sync.error': 'Ошибка синхронизации',
|
||||
'sync.notConfigured': 'Не настроено',
|
||||
'sync.failed': 'Синхронизация не удалась',
|
||||
'sync.connected': 'Подключено',
|
||||
'sync.syncNow': 'Синхронизировать сейчас',
|
||||
'sync.recentActivity': 'Недавняя активность',
|
||||
'sync.history.uploaded': 'Загружено',
|
||||
'sync.history.downloaded': 'Скачано',
|
||||
'sync.history.resolved': 'Разрешено',
|
||||
'sync.toast.completedMessage': 'Синхронизация успешно завершена',
|
||||
'sync.toast.errorTitle': 'Ошибка синхронизации',
|
||||
'sync.autoSync.failedTitle': 'Синхронизация не удалась',
|
||||
'sync.autoSync.inspectFailedTitle': 'Синхронизация приостановлена',
|
||||
'sync.autoSync.inspectFailedMessage': 'Не удалось подключиться к облаку для проверки изменений. Автосинхронизация повторит попытку при изменении данных или после перезапуска приложения.',
|
||||
'sync.autoSync.syncedTitle': 'Синхронизировано из облака',
|
||||
'sync.autoSync.syncedMessage': 'Ваши данные были обновлены из облака.',
|
||||
'sync.autoSync.noProvider': 'Облачный провайдер не подключён. Откройте Настройки → Синхронизация и облако, чтобы подключить его.',
|
||||
'sync.autoSync.alreadySyncing': 'Синхронизация уже выполняется.',
|
||||
'sync.autoSync.restoreInProgress': 'В другом окне уже выполняется восстановление хранилища. Подождите, пока оно завершится.',
|
||||
'sync.autoSync.interruptedApplyTitle': 'Синхронизация приостановлена — предыдущее восстановление прервано',
|
||||
'sync.autoSync.interruptedApplyMessage': 'Предыдущее восстановление завершилось некорректно, поэтому локальное хранилище может быть в несогласованном состоянии. Откройте Настройки → Синхронизация и облако → Восстановление и примените защитную резервную копию перед возобновлением автосинхронизации.',
|
||||
'sync.autoSync.vaultLocked': 'Хранилище заблокировано. Откройте Настройки → Синхронизация и облако, чтобы разблокировать его.',
|
||||
'sync.autoSync.conflictDetected': 'Обнаружен конфликт синхронизации. Откройте Настройки → Синхронизация и облако, чтобы разрешить его.',
|
||||
'sync.autoSync.syncFailed': 'Синхронизация не удалась',
|
||||
'sync.autoSync.restoredTitle': 'Хранилище восстановлено',
|
||||
'sync.autoSync.restoredMessage': 'Ваше хранилище было восстановлено из облака.',
|
||||
'sync.autoSync.keptLocalTitle': 'Локальное хранилище сохранено',
|
||||
'sync.autoSync.keptLocalMessage': 'Ваше пустое локальное хранилище было сохранено. Облачные данные не применялись.',
|
||||
'sync.autoSync.emptyVaultConflict.title': 'Обнаружено пустое хранилище',
|
||||
'sync.autoSync.emptyVaultConflict.description': 'Ваше локальное хранилище пусто, но в облаке есть данные. Обычно это происходит после обновления или сброса хранилища. Что вы хотите сделать?',
|
||||
'sync.autoSync.emptyVaultConflict.cloudLabel': 'Облако',
|
||||
'sync.autoSync.emptyVaultConflict.restore': 'Восстановить из облака',
|
||||
'sync.autoSync.emptyVaultConflict.restoreDesc': 'Рекомендуется — восстановить ваши хосты, ключи и сниппеты из облачной резервной копии',
|
||||
'sync.autoSync.emptyVaultConflict.keepEmpty': 'Оставить пустым',
|
||||
'sync.autoSync.emptyVaultConflict.keepEmptyDesc': 'Начать заново с пустым хранилищем',
|
||||
'sync.autoSync.emptyVaultConflict.cloudSummary': '{hosts} хостов, {keys} ключей, {snippets} сниппетов, {notes} заметок, {proxyProfiles} прокси',
|
||||
'sync.autoSync.emptyVaultManual': 'Синхронизация невозможна: локальное хранилище пусто. Сначала восстановите его из локальной резервной копии или включите принудительную отправку в панели синхронизации.',
|
||||
|
||||
'sync.blocked.title': 'Синхронизация приостановлена',
|
||||
'sync.blocked.reason.bulkShrink': 'Будет удалено {lost} из {baseCount} сущностей типа {entityType} из облака (сокращение на {percent}%).',
|
||||
'sync.blocked.reason.largeShrink': 'Будет удалено {lost} сущностей типа {entityType} из облака.',
|
||||
'sync.blocked.detail': 'Обычно это вызвано повреждённым локальным состоянием (сбой keychain, частичная загрузка данных). Восстановите данные из локальной резервной копии или выполните принудительную отправку, если вы действительно хотели удалить эти записи.',
|
||||
'sync.blocked.restoreButton': 'Восстановить из локальной резервной копии',
|
||||
'sync.blocked.forcePushButton': 'Всё равно отправить принудительно',
|
||||
|
||||
'sync.forcePush.title': 'Подтвердите принудительную отправку',
|
||||
'sync.forcePush.body': 'Вы собираетесь удалить {lost} сущностей типа {entityType} из облака. Это действие нельзя отменить. Продолжить?',
|
||||
'sync.forcePush.confirm': 'Да, всё равно отправить',
|
||||
'sync.forcePush.cancel': 'Отмена',
|
||||
|
||||
'sync.entityType.hosts': 'хостов',
|
||||
'sync.entityType.keys': 'ключей',
|
||||
'sync.entityType.identities': 'идентификаторов',
|
||||
'sync.entityType.proxyProfiles': 'профилей прокси',
|
||||
'sync.entityType.snippets': 'сниппетов',
|
||||
'sync.entityType.customGroups': 'групп',
|
||||
'sync.entityType.snippetPackages': 'пакетов сниппетов',
|
||||
'sync.entityType.knownHosts': 'записей known_hosts',
|
||||
'sync.entityType.portForwardingRules': 'правил проброса портов',
|
||||
'sync.entityType.groupConfigs': 'конфигураций групп',
|
||||
|
||||
'sync.credentialsUnavailable': 'Это устройство не может расшифровать некоторые сохранённые учётные данные. Перед синхронизацией повторно введите их локально.',
|
||||
'time.never': 'Никогда',
|
||||
'time.justNow': 'Только что',
|
||||
'time.minutesAgo': '{minutes} мин назад',
|
||||
|
||||
// Vault navigation
|
||||
'vault.nav.hosts': 'Хосты',
|
||||
'vault.nav.keychain': 'Связка ключей',
|
||||
'vault.nav.proxies': 'Прокси',
|
||||
'vault.nav.portForwarding': 'Проброс портов',
|
||||
'vault.nav.snippets': 'Сниппеты',
|
||||
'vault.nav.notes': 'Заметки',
|
||||
'vault.nav.knownHosts': 'Известные хосты',
|
||||
'vault.nav.logs': 'Журналы',
|
||||
|
||||
'proxyProfiles.action.add': 'Добавить прокси',
|
||||
'proxyProfiles.search.placeholder': 'Поиск прокси…',
|
||||
'proxyProfiles.section.proxies': 'Прокси',
|
||||
'proxyProfiles.count.items': 'Элементов: {count}',
|
||||
'proxyProfiles.empty.title': 'Нет прокси',
|
||||
'proxyProfiles.empty.desc': 'Создавайте переиспользуемые HTTP-, SOCKS5- или командные прокси и выбирайте их в настройках хоста.',
|
||||
'proxyProfiles.usage': 'Связано: {count}',
|
||||
'proxyProfiles.copyName': '{name} Копия',
|
||||
'proxyProfiles.panel.newTitle': 'Новый прокси',
|
||||
'proxyProfiles.field.name': 'Имя прокси',
|
||||
'proxyProfiles.error.required': 'Имя и параметры прокси обязательны.',
|
||||
'proxyProfiles.error.port': 'Порт должен быть в диапазоне от 1 до 65535.',
|
||||
'proxyProfiles.viewMode': 'Режим просмотра прокси',
|
||||
'proxyProfiles.delete.title': 'Удалить прокси?',
|
||||
'proxyProfiles.delete.desc': 'Удаление "{name}" отвяжет его от {count} настроек хостов или групп.',
|
||||
|
||||
'vault.groups.title': 'Группы',
|
||||
'vault.groups.total': 'Всего: {count}',
|
||||
'vault.groups.hostsCount': 'Хостов: {count}',
|
||||
'vault.groups.newSubgroup': 'Новая подгруппа',
|
||||
'vault.groups.rename': 'Переименовать группу',
|
||||
'vault.groups.delete': 'Удалить группу',
|
||||
'vault.groups.createSubfolder': 'Создать подпапку',
|
||||
'vault.groups.createRoot': 'Создать корневую группу',
|
||||
'vault.groups.createDialog.desc': 'Создайте новую группу для организации хостов.',
|
||||
'vault.groups.renameDialogTitle': 'Переименовать группу',
|
||||
'vault.groups.renameDialog.desc': 'Переименуйте существующую группу.',
|
||||
'vault.groups.deleteDialogTitle': 'Удалить группу',
|
||||
'vault.groups.deleteDialog.desc': 'Группа будет безвозвратно удалена, а все хосты будут перемещены в корень.',
|
||||
'vault.groups.deleteDialog.managedDesc': 'Это управляемая группа SSH-конфига. При её удалении также будут удалены все хосты и снята связь с исходным файлом.',
|
||||
'vault.groups.deleteDialog.deleteHosts': 'Также удалить все хосты в этой группе',
|
||||
'vault.groups.ungrouped': 'Без группы',
|
||||
'vault.groups.field.name': 'Имя группы',
|
||||
'vault.groups.placeholder.example': 'например, Production',
|
||||
'vault.groups.parentLabel': 'Родитель',
|
||||
'vault.groups.pathLabel': 'Путь',
|
||||
'vault.groups.settings': 'Настройки группы',
|
||||
'vault.groups.details': 'Сведения о группе',
|
||||
'vault.groups.details.general': 'Общие',
|
||||
'vault.groups.details.ssh': 'SSH',
|
||||
'vault.groups.details.telnet': 'Telnet',
|
||||
'vault.groups.details.advanced': 'Дополнительно',
|
||||
'vault.groups.details.appearance': 'Внешний вид',
|
||||
'vault.groups.details.mosh': 'Mosh',
|
||||
'vault.groups.details.parentGroup': 'Родительская группа',
|
||||
'vault.groups.details.none': 'Нет',
|
||||
'vault.groups.details.inherited': 'Унаследовано от группы',
|
||||
'vault.groups.details.addProtocol': 'Добавить протокол',
|
||||
'vault.groups.details.removeProtocol': 'Удалить протокол',
|
||||
'vault.groups.details.fontFamily': 'Семейство шрифта',
|
||||
'vault.groups.details.fontSize': 'Размер шрифта',
|
||||
'vault.groups.errors.required': 'Имя группы обязательно.',
|
||||
'vault.groups.errors.invalidChars': "Имя группы не может содержать '/' или '\\\\'.",
|
||||
'vault.groups.errors.duplicatePath': 'Группа с таким именем уже существует в этом расположении.',
|
||||
|
||||
'vault.deleteConfirm.title': 'Удалить "{name}"?',
|
||||
'vault.deleteConfirm.desc': 'Это действие нельзя отменить.',
|
||||
'vault.deleteConfirm.packageDesc': 'Пакет скриптов будет удалён. Скрипты внутри него сохранятся и будут вынесены из пакета.',
|
||||
'vault.deleteConfirm.noteGroupDesc': 'Папка будет удалена. Заметки внутри неё сохранятся и будут вынесены из папки.',
|
||||
'vault.deleteConfirm.portForwardingDesc': 'Правило проброса портов будет удалено.',
|
||||
|
||||
'vault.managedSource.unmanage': 'Снять управление',
|
||||
'vault.managedSource.unmanageSuccess': 'Управление группой успешно снято',
|
||||
|
||||
'vault.hosts.header.entries': 'Записей: {count}',
|
||||
'vault.hosts.header.live': 'Активных: {count}',
|
||||
'deepLink.ssh.invalid': 'Недопустимая ссылка ssh://',
|
||||
'deepLink.telnet.invalid': 'Недопустимая ссылка telnet://',
|
||||
'deepLink.jms.invalid': 'Недопустимая ссылка JumpServer',
|
||||
'deepLink.jms.unsupported': 'Неподдерживаемый протокол JumpServer: {{protocol}}',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const ruSystemManagerMessages: Messages = {
|
||||
'terminal.layer.system': 'Система',
|
||||
|
||||
'systemManager.noSession': 'Нет активного терминального сеанса.',
|
||||
'systemManager.notConnected': 'Подключитесь к хосту для управления процессами и сервисами.',
|
||||
'systemManager.empty': 'Нет данных.',
|
||||
'systemManager.tabs.overview': 'Обзор',
|
||||
'systemManager.tabs.processes': 'Процессы',
|
||||
'systemManager.tabs.tmux': 'tmux',
|
||||
'systemManager.tabs.docker': 'Docker',
|
||||
'systemManager.popup.loading': 'Открытие терминала…',
|
||||
'systemManager.popup.startupFailed': 'Команда запуска не была выполнена успешно. Проверьте, что цель доступна, и повторите попытку.',
|
||||
|
||||
'systemManager.errors.loadProcesses': 'Не удалось загрузить процессы',
|
||||
'systemManager.errors.loadTmux': 'Не удалось загрузить сессии tmux',
|
||||
'systemManager.errors.loadTmuxWindows': 'Не удалось загрузить окна tmux',
|
||||
'systemManager.errors.loadTmuxPanes': 'Не удалось загрузить панели tmux',
|
||||
'systemManager.errors.loadTmuxClients': 'Не удалось загрузить клиентов tmux',
|
||||
'systemManager.errors.actionFailed': 'Не удалось выполнить действие',
|
||||
'systemManager.errors.loadDocker': 'Не удалось загрузить контейнеры',
|
||||
'systemManager.errors.loadDockerStats': 'Не удалось загрузить статистику контейнеров',
|
||||
'systemManager.errors.loadDockerImages': 'Не удалось загрузить образы',
|
||||
'systemManager.errors.loadOverview': 'Не удалось загрузить обзор системы',
|
||||
'systemManager.errors.sshChannelUnavailable': 'Сервер отказался открыть новый канал выполнения. Повторите попытку позже или переподключите этот хост.',
|
||||
|
||||
'systemManager.overview.empty': 'Данных обзора пока нет.',
|
||||
'systemManager.overview.loading': 'Загрузка обзора системы…',
|
||||
'systemManager.overview.memory': 'Память',
|
||||
'systemManager.overview.disk': 'Диск',
|
||||
'systemManager.overview.network': 'Сеть',
|
||||
'systemManager.overview.rx': 'RX',
|
||||
'systemManager.overview.tx': 'TX',
|
||||
'systemManager.overview.cores': '{{count}} ядер',
|
||||
'systemManager.overview.load': 'Нагрузка',
|
||||
'systemManager.overview.uptime': 'Время работы',
|
||||
'systemManager.overview.duration.daysHours': '{{days}} д {{hours}} ч',
|
||||
'systemManager.overview.duration.hoursMinutes': '{{hours}} ч {{minutes}} мин',
|
||||
'systemManager.overview.duration.minutes': '{{minutes}} мин',
|
||||
'systemManager.overview.system': 'Система',
|
||||
'systemManager.overview.kernel': 'Ядро',
|
||||
'systemManager.overview.swap': 'Swap',
|
||||
'systemManager.overview.latency': 'Задержка',
|
||||
'systemManager.overview.cpuCores': 'Ядра CPU',
|
||||
'systemManager.overview.disks': 'Диски',
|
||||
'systemManager.overview.interfaces': 'Сетевые интерфейсы',
|
||||
'systemManager.overview.topProcesses': 'Процессы по памяти',
|
||||
'systemManager.overview.noData': 'Нет данных',
|
||||
'systemManager.overview.noDisks': 'Нет данных дисков',
|
||||
'systemManager.overview.noInterfaces': 'Нет данных интерфейсов',
|
||||
'systemManager.overview.noTopProcesses': 'Нет данных процессов',
|
||||
|
||||
'systemManager.processes.search': 'Поиск процессов…',
|
||||
'systemManager.processes.command': 'Команда',
|
||||
'systemManager.processes.user': 'Пользователь',
|
||||
'systemManager.processes.term': 'Завершить',
|
||||
'systemManager.processes.kill': 'Убить',
|
||||
'systemManager.processes.stop': 'Остановить (SIGSTOP)',
|
||||
'systemManager.processes.cont': 'Продолжить (SIGCONT)',
|
||||
'systemManager.processes.hup': 'Сигнал SIGHUP',
|
||||
'systemManager.processes.renice': 'Renice',
|
||||
'systemManager.processes.renicePrompt': 'Значение nice (-20 до 19)',
|
||||
'systemManager.processes.reniceInvalid': 'Nice должно быть от -20 до 19',
|
||||
'systemManager.processes.confirmKill': 'Отправить SIGKILL процессу {{pid}}?',
|
||||
'systemManager.processes.confirmSignal': 'Отправить SIG{{signal}} процессу {{pid}}?',
|
||||
'systemManager.processes.filter.all': 'Все',
|
||||
'systemManager.processes.filter.running': 'Активные',
|
||||
'systemManager.processes.ppid': 'Родительский PID',
|
||||
'systemManager.processes.rss': 'RSS',
|
||||
'systemManager.processes.vsz': 'Виртуальный размер',
|
||||
'systemManager.processes.elapsed': 'Время работы',
|
||||
'systemManager.processes.stat': 'Состояние',
|
||||
'systemManager.processes.meta': '{{count}} проц.',
|
||||
'systemManager.processes.loading': 'Загрузка процессов…',
|
||||
'systemManager.processes.loadingMore': 'Загрузка следующих процессов…',
|
||||
'systemManager.processes.state.running': 'Активен',
|
||||
'systemManager.processes.state.sleeping': 'Сон',
|
||||
'systemManager.processes.state.stopped': 'Остановлен',
|
||||
'systemManager.processes.state.zombie': 'Зомби',
|
||||
'systemManager.processes.sort.cpu': 'CPU',
|
||||
'systemManager.processes.sort.mem': 'Память',
|
||||
'systemManager.processes.sort.pid': 'PID',
|
||||
'systemManager.processes.sort.command': 'Команда',
|
||||
'systemManager.processes.sort.user': 'Пользователь',
|
||||
|
||||
'systemManager.common.dismiss': 'Закрыть',
|
||||
'systemManager.common.checkingAvailability': 'Проверка доступности…',
|
||||
'systemManager.common.loading': 'Загрузка…',
|
||||
'systemManager.common.loadingDetails': 'Загрузка деталей…',
|
||||
'systemManager.common.loadingStats': 'Загрузка статистики…',
|
||||
|
||||
'systemManager.tmux.new': 'Создать',
|
||||
'systemManager.tmux.search': 'Поиск сессий…',
|
||||
'systemManager.tmux.newSessionTitle': 'Новая сессия tmux',
|
||||
'systemManager.tmux.newSessionDesc': 'Задайте имя сессии и при необходимости команду запуска.',
|
||||
'systemManager.tmux.newSessionTabCustom': 'Своя команда',
|
||||
'systemManager.tmux.newSessionTabSnippet': 'Из сниппета',
|
||||
'systemManager.tmux.pickSnippet': 'Из сниппетов',
|
||||
'systemManager.tmux.pickSnippetEmpty': 'Сниппетов пока нет — добавьте их на панели скриптов или в хранилище.',
|
||||
'systemManager.tmux.selectedSnippet': 'Выбран сниппет: {{label}}',
|
||||
'systemManager.tmux.newSessionName': 'Имя сессии',
|
||||
'systemManager.tmux.newSessionCommand': 'Команда запуска',
|
||||
'systemManager.tmux.newSessionCommandPlaceholder': 'например htop или npm run dev (необяз.)',
|
||||
'systemManager.tmux.newSessionCommandHint': 'Оставьте пустым для сессии с shell по умолчанию.',
|
||||
'systemManager.tmux.creating': 'Создание…',
|
||||
'systemManager.tmux.newSessionPlaceholder': 'my-session',
|
||||
'systemManager.tmux.newSessionRequired': 'Сначала введите имя сессии',
|
||||
'systemManager.tmux.empty': 'Нет сессий tmux',
|
||||
'systemManager.tmux.attach': 'Подключить',
|
||||
'systemManager.tmux.attached': 'Подключена',
|
||||
'systemManager.tmux.detached': 'Отключена',
|
||||
'systemManager.tmux.windows': '{{count}} окон',
|
||||
'systemManager.tmux.created': 'Создана',
|
||||
'systemManager.tmux.activity': 'Активность',
|
||||
'systemManager.tmux.rename': 'Переименовать',
|
||||
'systemManager.tmux.detach': 'Отключить всех',
|
||||
'systemManager.tmux.killSession': 'Завершить сессию',
|
||||
'systemManager.tmux.killServer': 'Остановить сервер',
|
||||
'systemManager.tmux.loadingDetails': 'Загрузка деталей…',
|
||||
'systemManager.tmux.clients': 'Подключённые клиенты',
|
||||
'systemManager.tmux.windowList': 'Окна',
|
||||
'systemManager.tmux.newWindow': 'Новое окно',
|
||||
'systemManager.tmux.newWindowPlaceholder': 'Имя окна (необязательно)',
|
||||
'systemManager.tmux.noWindows': 'Нет окон',
|
||||
'systemManager.tmux.unavailable': 'tmux недоступен на этом хосте',
|
||||
'systemManager.docker.unavailable': 'Docker недоступен на этом хосте',
|
||||
'systemManager.tmux.windowsMismatch': 'В сессии указано {{count}} окон, но list-windows ничего не вернул',
|
||||
'systemManager.tmux.lastCommand': 'последняя команда: {{command}}',
|
||||
'systemManager.tmux.noPanes': 'Нет панелей',
|
||||
'systemManager.tmux.panes': '{{count}} пан.',
|
||||
'systemManager.tmux.active': 'активно',
|
||||
'systemManager.tmux.unnamedWindow': 'Безымянное окно',
|
||||
'systemManager.tmux.unnamedPane': 'Безымянная панель',
|
||||
'systemManager.tmux.attachWindow': 'Подключить к окну',
|
||||
'systemManager.tmux.selectWindow': 'Выбрать окно',
|
||||
'systemManager.tmux.killWindow': 'Закрыть окно',
|
||||
'systemManager.tmux.killPane': 'Закрыть панель',
|
||||
'systemManager.tmux.splitHorizontal': 'Разделить горизонтально',
|
||||
'systemManager.tmux.splitVertical': 'Разделить вертикально',
|
||||
'systemManager.tmux.sendKeys': 'Отправить клавиши',
|
||||
'systemManager.tmux.sendKeysTo': 'Отправить клавиши в окно {{window}} панель {{pane}}',
|
||||
'systemManager.tmux.sendKeysPlaceholder': 'Команда или текст…',
|
||||
'systemManager.tmux.renameSessionPrompt': 'Переименовать сессию',
|
||||
'systemManager.tmux.renameWindowPrompt': 'Переименовать окно',
|
||||
'systemManager.tmux.windowName': 'Имя окна',
|
||||
'systemManager.tmux.confirmKillSession': 'Завершить сессию tmux «{{name}}»?',
|
||||
'systemManager.tmux.confirmDetachSession': 'Отключить всех клиентов от «{{name}}»?',
|
||||
'systemManager.tmux.confirmKillWindow': 'Закрыть окно «{{name}}»?',
|
||||
'systemManager.tmux.confirmKillPane': 'Закрыть панель #{{index}}?',
|
||||
'systemManager.tmux.confirmKillServer': 'Остановить сервер tmux? Все сессии будут завершены.',
|
||||
'systemManager.tmux.meta': '{{count}} сессий',
|
||||
|
||||
'systemManager.docker.title': 'Контейнеры',
|
||||
'systemManager.docker.subTabs.containers': 'Контейнеры',
|
||||
'systemManager.docker.subTabs.images': 'Образы',
|
||||
'systemManager.docker.empty': 'Контейнеры не найдены',
|
||||
'systemManager.docker.imagesEmpty': 'Образы не найдены',
|
||||
'systemManager.docker.search': 'Поиск контейнеров…',
|
||||
'systemManager.docker.searchImages': 'Поиск образов…',
|
||||
'systemManager.docker.filter.all': 'Все',
|
||||
'systemManager.docker.filter.running': 'Запущены',
|
||||
'systemManager.docker.filter.stopped': 'Остановлены',
|
||||
'systemManager.docker.filter.paused': 'На паузе',
|
||||
'systemManager.docker.shell': 'Shell',
|
||||
'systemManager.docker.logs': 'Логи',
|
||||
'systemManager.docker.details': 'Детали',
|
||||
'systemManager.docker.inspect': 'Inspect',
|
||||
'systemManager.docker.imageInspect': 'Inspect образа',
|
||||
'systemManager.docker.confirmRemove': 'Удалить этот контейнер?',
|
||||
'systemManager.docker.confirmKill': 'Принудительно завершить контейнер?',
|
||||
'systemManager.docker.confirmRemoveImage': 'Удалить образ «{{name}}»?',
|
||||
'systemManager.docker.confirmPrune': 'Удалить dangling-образы?',
|
||||
'systemManager.docker.confirmPruneAll': 'Удалить все неиспользуемые образы?',
|
||||
'systemManager.docker.pause': 'Пауза',
|
||||
'systemManager.docker.unpause': 'Возобновить',
|
||||
'systemManager.docker.restart': 'Перезапустить',
|
||||
'systemManager.docker.kill': 'Kill',
|
||||
'systemManager.docker.renamePrompt': 'Имя контейнера',
|
||||
'systemManager.docker.prune': 'Prune',
|
||||
'systemManager.docker.pruneAll': 'Prune all',
|
||||
'systemManager.docker.tag': 'Tag',
|
||||
'systemManager.docker.tagRepoPrompt': 'Имя репозитория',
|
||||
'systemManager.docker.tagNamePrompt': 'Имя тега',
|
||||
'systemManager.docker.meta': '{{count}} конт.',
|
||||
'systemManager.docker.imagesMeta': '{{count}} образов',
|
||||
'systemManager.docker.start': 'Запустить',
|
||||
'systemManager.docker.stop': 'Остановить',
|
||||
|
||||
'systemManager.inspect.status': 'Статус',
|
||||
'systemManager.inspect.image': 'Образ',
|
||||
'systemManager.inspect.created': 'Создан',
|
||||
'systemManager.inspect.started': 'Запущен',
|
||||
'systemManager.inspect.restartPolicy': 'Перезапуск',
|
||||
'systemManager.inspect.command': 'Команда',
|
||||
'systemManager.inspect.ports': 'Порты',
|
||||
'systemManager.inspect.networks': 'Сети',
|
||||
'systemManager.inspect.mounts': 'Тома',
|
||||
'systemManager.inspect.env': 'Окружение',
|
||||
'systemManager.inspect.labels': 'Метки',
|
||||
'systemManager.inspect.tags': 'Теги',
|
||||
'systemManager.inspect.digests': 'Дайджесты',
|
||||
'systemManager.inspect.size': 'Размер',
|
||||
'systemManager.inspect.platform': 'Платформа',
|
||||
'systemManager.inspect.workdir': 'Рабочий каталог',
|
||||
'systemManager.inspect.exposedPorts': 'Открытые порты',
|
||||
'systemManager.inspect.showRaw': 'JSON',
|
||||
'systemManager.inspect.hideRaw': 'Скрыть JSON',
|
||||
};
|
||||
@@ -0,0 +1,784 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const ruTerminalMessages: Messages = {
|
||||
'terminal.sudoHint.pressEnter': 'Нажмите Enter, чтобы вставить пароль sudo',
|
||||
// Connection logs
|
||||
'logs.table.date': 'Дата',
|
||||
'logs.table.user': 'Пользователь',
|
||||
'logs.table.host': 'Хост',
|
||||
'logs.table.saved': 'Сохранено',
|
||||
'logs.empty.title': 'Нет журналов подключений',
|
||||
'logs.empty.desc':
|
||||
'История ваших подключений будет отображаться здесь, когда вы подключаетесь к хостам или открываете локальные терминалы.',
|
||||
'logs.loadMore': 'Загрузить ещё {count} журналов',
|
||||
'logs.ongoing': 'в процессе',
|
||||
'logs.localTerminal': 'Локальный терминал',
|
||||
'logs.action.save': 'Сохранить',
|
||||
'logs.action.unsave': 'Убрать из сохранённых',
|
||||
'logs.action.delete': 'Удалить',
|
||||
|
||||
// Log view
|
||||
'logView.customizeAppearance': 'Настроить внешний вид',
|
||||
'logView.appearance': 'Внешний вид',
|
||||
'logView.readOnly': 'Только чтение',
|
||||
'logView.export': 'Экспорт',
|
||||
|
||||
// Terminal toolbar / search / context menu / auth
|
||||
'terminal.toolbar.openSftp': 'Открыть SFTP',
|
||||
'terminal.toolbar.availableAfterConnect': 'Доступно после подключения',
|
||||
'terminal.toolbar.sendYmodem': 'Отправить через YMODEM',
|
||||
'terminal.toolbar.receiveYmodem': 'Получить через YMODEM',
|
||||
'terminal.toolbar.sftp': 'SFTP',
|
||||
'terminal.toolbar.more': 'Другие действия',
|
||||
'terminal.toolbar.scripts': 'Скрипты',
|
||||
'terminal.toolbar.history': 'История команд',
|
||||
'terminal.toolbar.configureOsc7': 'Настроить отслеживание каталога',
|
||||
'history.scope.label': 'Область истории',
|
||||
'history.tab.host': 'Хост',
|
||||
'history.tab.global': 'Глобальная',
|
||||
'history.searchPlaceholder': 'Поиск по истории...',
|
||||
'history.loading': 'Загрузка удалённой истории...',
|
||||
'history.meta.count': '{count} команд',
|
||||
'history.empty.noSession': 'Откройте удалённую сессию, чтобы просмотреть историю команд.',
|
||||
'history.empty.unsupportedProtocol': 'История команд доступна только для сессий SSH/Mosh/ET.',
|
||||
'history.empty.noHistory': 'История команд на этом хосте не найдена.',
|
||||
'history.empty.noGlobalHistory': 'Глобальной истории команд пока нет. Выполненные команды появятся здесь.',
|
||||
'history.action.refresh': 'Обновить',
|
||||
'history.action.retry': 'Повторить',
|
||||
'history.action.paste': 'Вставить в терминал',
|
||||
'history.action.run': 'Выполнить в терминале',
|
||||
'history.action.saveAsSnippet': 'Сохранить как сниппет',
|
||||
'terminal.toolbar.library': 'Библиотека',
|
||||
'terminal.toolbar.noSnippets': 'Нет доступных сниппетов',
|
||||
'terminal.toolbar.terminalSettings': 'Настройки терминала',
|
||||
'terminal.toolbar.searchTerminal': 'Поиск по терминалу',
|
||||
'terminal.toolbar.search': 'Поиск',
|
||||
'terminal.toolbar.startSessionLog': 'Начать журнал сессии',
|
||||
'terminal.toolbar.stopSessionLog': 'Остановить журнал сессии',
|
||||
'terminal.toolbar.timestampsEnable': 'Показать время',
|
||||
'terminal.toolbar.timestampsDisable': 'Скрыть время',
|
||||
'terminal.toolbar.broadcast': 'Трансляция',
|
||||
'terminal.toolbar.broadcastEnable': 'Включить режим трансляции',
|
||||
'terminal.toolbar.broadcastDisable': 'Отключить режим трансляции',
|
||||
'terminal.toolbar.composeBar': 'Строка ввода',
|
||||
'terminal.composeBar.placeholder': 'Введите команду здесь и нажмите Enter для отправки...',
|
||||
'terminal.composeBar.send': 'Отправить',
|
||||
'terminal.composeBar.close': 'Закрыть строку ввода',
|
||||
'terminal.composeBar.broadcasting': 'Трансляция во все сессии',
|
||||
'terminal.composeBar.resize': 'Изменить высоту строки ввода',
|
||||
'terminal.composeBar.manageSnippets': 'Управление быстрыми сниппетами',
|
||||
'terminal.composeBar.searchSnippets': 'Поиск сниппетов...',
|
||||
'terminal.composeBar.noPinnedSnippets': 'Закрепите сниппеты через + для быстрого доступа',
|
||||
'terminal.composeBar.noMatchingSnippets': 'Сниппеты не найдены',
|
||||
'terminal.composeBar.pinnedCount': 'Закреплено: {count}',
|
||||
'terminal.composeBar.unpinSnippet': 'Убрать {label} из панели',
|
||||
'terminal.composeBar.snippetClickHint': 'Клик — вставить · Shift+клик — отправить',
|
||||
'terminal.toolbar.focus': 'Фокус',
|
||||
'terminal.toolbar.focusMode': 'Режим фокуса',
|
||||
'terminal.toolbar.detach': 'Открепить в отдельную вкладку',
|
||||
'terminal.toolbar.encoding': 'Кодировка терминала',
|
||||
'terminal.toolbar.encoding.utf8': 'UTF-8',
|
||||
'terminal.toolbar.encoding.gb18030': 'GB18030',
|
||||
'terminal.toolbar.closeSession': 'Закрыть сессию',
|
||||
'terminal.toolbar.hostHighlight.title': 'Подсветка ключевых слов хоста',
|
||||
'terminal.toolbar.hostHighlight.noRules': 'Для этого хоста не задано пользовательских правил подсветки',
|
||||
'terminal.toolbar.hostHighlight.addRule': 'Добавить новое правило',
|
||||
'terminal.toolbar.hostHighlight.labelPlaceholder': 'Метка (например, Error)',
|
||||
'terminal.toolbar.hostHighlight.patternPlaceholder': 'Regex-шаблон (например, \\bfailed\\b)',
|
||||
'terminal.toolbar.hostHighlight.invalidPattern': 'Некорректный regex-шаблон',
|
||||
'terminal.toolbar.hostHighlight.clearAll': 'Очистить все',
|
||||
'terminal.toolbar.hostHighlight.changeColor': 'Изменить цвет подсветки для',
|
||||
'terminal.toolbar.hostHighlight.selectColor': 'Выбрать цвет для нового правила',
|
||||
'terminal.statusbar.copyHostname.label': 'Копировать адрес хоста',
|
||||
'terminal.statusbar.copyHostname.tooltip': 'Копировать адрес хоста ({hostname})',
|
||||
'terminal.statusbar.copyHostname.toast': 'Адрес хоста скопирован: {hostname}',
|
||||
'terminal.statusbar.copyHostname.error': 'Не удалось скопировать адрес хоста в буфер обмена',
|
||||
'terminal.serverStats.cpu': 'Использование CPU',
|
||||
'terminal.serverStats.cpuCores': 'Использование ядер CPU',
|
||||
'terminal.serverStats.memory': 'Использование памяти',
|
||||
'terminal.serverStats.memoryDetails': 'Сведения о памяти',
|
||||
'terminal.serverStats.memUsed': 'Использовано',
|
||||
'terminal.serverStats.memBuffers': 'Буферы',
|
||||
'terminal.serverStats.memCached': 'Кэш',
|
||||
'terminal.serverStats.memFree': 'Свободно',
|
||||
'terminal.serverStats.swap': 'Swap',
|
||||
'terminal.serverStats.swapUsed': 'Использовано swap',
|
||||
'terminal.serverStats.swapFree': 'Свободный swap',
|
||||
'terminal.serverStats.swapTotal': 'Всего',
|
||||
'terminal.serverStats.topProcesses': 'Топ процессов по памяти',
|
||||
'terminal.serverStats.disk': 'Использование диска (корень)',
|
||||
'terminal.serverStats.diskDetails': 'Смонтированные диски',
|
||||
'terminal.serverStats.network': 'Скорость сети',
|
||||
'terminal.serverStats.networkDetails': 'Сетевые интерфейсы',
|
||||
'terminal.serverStats.noData': 'Данные недоступны',
|
||||
'terminal.dragDrop.localTitle': 'Перетащите для вставки путей',
|
||||
'terminal.dragDrop.localMessage': 'Пути к файлам будут вставлены в терминал',
|
||||
'terminal.dragDrop.remoteTitle': 'Перетащите для загрузки файлов',
|
||||
'terminal.dragDrop.remoteZmodemMessage': 'Файлы будут загружены через ZMODEM (PTY)',
|
||||
'terminal.dragDrop.remoteSftpMessage': 'Файлы будут загружены через SFTP',
|
||||
'terminal.dragDrop.noFiles': 'Нет файлов для загрузки',
|
||||
'terminal.dragDrop.notConnected': 'Нельзя перетащить файлы — терминал не подключён',
|
||||
'terminal.dragDrop.errorTitle': 'Ошибка перетаскивания',
|
||||
'terminal.dragDrop.errorMessage': 'Не удалось обработать перетащенные файлы',
|
||||
'terminal.search.placeholder': 'Поиск...',
|
||||
'terminal.search.noResults': 'Ничего не найдено',
|
||||
'terminal.search.prevMatch': 'Предыдущее совпадение (Shift+Enter)',
|
||||
'terminal.search.nextMatch': 'Следующее совпадение (Enter)',
|
||||
'terminal.menu.copy': 'Копировать',
|
||||
'terminal.menu.paste': 'Вставить',
|
||||
'terminal.menu.uploadClipboardImage': 'Загрузить изображение из буфера',
|
||||
'terminal.menu.addSelectionToAI': 'Добавить в чат',
|
||||
'terminal.menu.pasteSelection': 'Вставить выделенное',
|
||||
'terminal.menu.selectAll': 'Выбрать всё',
|
||||
'terminal.menu.reconnect': 'Переподключиться',
|
||||
'terminal.menu.sendYmodem': 'Отправить через YMODEM',
|
||||
'terminal.menu.receiveYmodem': 'Получить через YMODEM',
|
||||
'terminal.menu.splitHorizontal': 'Разделить по горизонтали',
|
||||
'terminal.menu.splitVertical': 'Разделить по вертикали',
|
||||
'terminal.menu.clearBuffer': 'Очистить буфер',
|
||||
'terminal.menu.closeTerminal': 'Закрыть терминал',
|
||||
'terminal.menu.rename': 'Переименовать',
|
||||
'terminal.menu.detach': 'Открепить из рабочей области',
|
||||
'terminal.menu.detachSession': 'Открепить {name}',
|
||||
'terminal.osc7Setup.title': 'Настроить отслеживание каталога',
|
||||
'terminal.osc7Setup.desc': 'Netcatty добавит хуки OSC 7 для текущего удалённого пользователя. Это помогает SFTP следовать за каталогом терминала после sudo или su.',
|
||||
'terminal.osc7Setup.targets': 'Возможные файлы для обновления',
|
||||
'terminal.osc7Setup.command': 'Команда для запуска',
|
||||
'terminal.osc7Setup.run': 'Запустить настройку',
|
||||
'terminal.osc7Setup.running': 'Настройка...',
|
||||
'terminal.osc7Setup.configured': 'Отслеживание каталога настроено',
|
||||
'terminal.osc7Setup.failed': 'Не удалось настроить отслеживание каталога',
|
||||
'terminal.osc7Setup.sent': 'Настройка отслеживания каталога отправлена в терминал',
|
||||
'terminal.ymodem.selectFile': 'Выберите файл для отправки',
|
||||
'terminal.ymodem.allFiles': 'Все файлы',
|
||||
'terminal.ymodem.started': 'YMODEM отправляет {fileName}',
|
||||
'terminal.ymodem.complete': 'YMODEM отправил {fileName}',
|
||||
'terminal.ymodem.failed': 'Не удалось отправить через YMODEM',
|
||||
'terminal.ymodem.selectReceiveDirectory': 'Выберите папку для полученных файлов',
|
||||
'terminal.ymodem.receiveStarted': 'YMODEM получает...',
|
||||
'terminal.ymodem.receiveComplete': 'YMODEM получил {fileName}',
|
||||
'terminal.ymodem.receiveCompleteMultiple': 'YMODEM получил файлов: {count}',
|
||||
'terminal.ymodem.receiveEmpty': 'Файлы YMODEM не получены',
|
||||
'terminal.ymodem.receiveFailed': 'Не удалось получить через YMODEM',
|
||||
'terminal.ymodem.unavailable': 'YMODEM недоступен',
|
||||
'terminal.clipboardImageUpload.noImage': 'В буфере обмена нет изображения',
|
||||
'terminal.clipboardImageUpload.failed': 'Не удалось загрузить изображение из буфера обмена',
|
||||
'terminal.selection.addToAI': 'Добавить в чат',
|
||||
'terminal.selection.addToAIDesc': 'Прикрепить выбранный вывод терминала к черновику AI',
|
||||
'terminal.auth.password': 'Пароль',
|
||||
'terminal.auth.sshKey': 'SSH-ключ',
|
||||
'terminal.auth.username': 'Имя пользователя',
|
||||
'terminal.auth.username.placeholder': 'root',
|
||||
'terminal.auth.passwordLabel': 'Пароль',
|
||||
'terminal.auth.password.placeholder': 'Введите пароль',
|
||||
'terminal.auth.passphrase': 'Парольная фраза',
|
||||
'terminal.auth.passphrase.placeholder': 'Необязательная парольная фраза для выбранного приватного ключа',
|
||||
'terminal.auth.certificate': 'Сертификат',
|
||||
'terminal.auth.selectKey': 'Выбрать ключ',
|
||||
'terminal.auth.retryMessage': 'Ошибка аутентификации. Проверьте учётные данные и повторите попытку.',
|
||||
'terminal.auth.retryLog': 'Ошибка аутентификации. Повторите попытку.',
|
||||
'terminal.auth.noKeysHint': 'Нет доступных ключей. Добавьте ключи в связке ключей.',
|
||||
'terminal.auth.continueSave': 'Продолжить и сохранить',
|
||||
'terminal.auth.credentialsUnavailable': 'Сохранённые учётные данные не могут быть расшифрованы на этом устройстве. Пожалуйста, введите и сохраните их заново.',
|
||||
'terminal.auth.jumpCredentialsUnavailable': 'У jump-хоста сохранены учётные данные, которые нельзя расшифровать на этом устройстве. Откройте настройки хоста и введите их заново.',
|
||||
'terminal.auth.proxyCredentialsUnavailable': 'Учётные данные прокси не могут быть расшифрованы на этом устройстве. Откройте настройки хоста и заново введите пароль прокси.',
|
||||
'terminal.auth.keyUnavailableFallbackPassword': 'Сохранённый SSH-ключ недоступен на этом устройстве. Выполняется переход на аутентификацию по паролю.',
|
||||
'terminal.progress.timeoutIn': 'Тайм-аут через {seconds}с',
|
||||
'terminal.progress.waitingForUserInput': 'Ожидание ввода пользователя',
|
||||
'terminal.progress.disconnected': 'Отключено',
|
||||
'terminal.progress.cancelling': 'Отмена...',
|
||||
'terminal.progress.startOver': 'Начать заново',
|
||||
'terminal.progress.enterReconnectHint': 'Нажмите Enter, чтобы подключиться снова',
|
||||
'terminal.connection.dismissDisconnectedDialog': 'Закрыть уведомление об отключении',
|
||||
'terminal.connection.chainOf': 'Цепочка {current} из {total}',
|
||||
'terminal.connection.showLogs': 'Показать журналы',
|
||||
'terminal.connection.hideLogs': 'Скрыть журналы',
|
||||
'terminal.connection.protocol.ssh': 'SSH',
|
||||
'terminal.connection.protocol.telnet': 'Telnet',
|
||||
'terminal.connection.protocol.mosh': 'Mosh',
|
||||
'terminal.connection.protocol.serial': 'Serial',
|
||||
'terminal.connection.protocol.local': 'Локальная оболочка',
|
||||
'terminal.hostKey.unknownTitle': 'Подтвердите этот ключ хоста',
|
||||
'terminal.hostKey.changedTitle': 'Ключ хоста изменился',
|
||||
'terminal.hostKey.unknownDescription': 'Подлинность {host} пока не может быть установлена.',
|
||||
'terminal.hostKey.changedDescription': 'Сохранённый ключ для {host} больше не совпадает с этим сервером.',
|
||||
'terminal.hostKey.fingerprintLabel': 'Отпечаток {keyType} — SHA256:',
|
||||
'terminal.hostKey.savedFingerprintLabel': 'Сохранённый отпечаток',
|
||||
'terminal.hostKey.unknownHint': 'Запомните его, если этот отпечаток принадлежит серверу, к которому вы ожидали подключиться.',
|
||||
'terminal.hostKey.changedHint': 'Продолжайте только если вы ожидали, что этот хост изменится.',
|
||||
'terminal.hostKey.addAndContinue': 'Добавить и продолжить',
|
||||
'terminal.hostKey.updateAndContinue': 'Обновить и продолжить',
|
||||
'terminal.themeModal.title': 'Внешний вид терминала',
|
||||
'terminal.themeModal.tab.theme': 'Тема',
|
||||
'terminal.themeModal.tab.font': 'Шрифт',
|
||||
'terminal.themeModal.tab.custom': 'Пользовательское',
|
||||
'terminal.themeModal.globalTheme': 'Глобальная тема',
|
||||
'terminal.themeModal.globalFont': 'Глобальный шрифт',
|
||||
'terminal.themeModal.fontSize': 'Размер шрифта',
|
||||
'terminal.themeModal.fontWeight': 'Толщина шрифта',
|
||||
'terminal.themeModal.livePreview': 'Предпросмотр в реальном времени',
|
||||
'terminal.themeModal.themeType': 'Тема {type}',
|
||||
'terminal.hiddenTheme.title': 'Текущая скрытая тема',
|
||||
'terminal.hiddenTheme.desc': 'Эта тема скрыта из ручного выбора и будет заменена, когда вы выберете другую тему.',
|
||||
'topTabs.toggleTheme.systemExitTitle': 'Активна системная тема',
|
||||
'topTabs.toggleTheme.systemExitMessage': 'Откройте настройки, чтобы выбрать фиксированную светлую или тёмную тему.',
|
||||
'topTabs.toggleTheme.openSettings': 'Открыть настройки',
|
||||
|
||||
// Custom Themes
|
||||
'terminal.customTheme.section': 'Пользовательские темы',
|
||||
'terminal.customTheme.yourThemes': 'Ваши темы',
|
||||
'terminal.customTheme.new': 'Новая тема',
|
||||
'terminal.customTheme.newDesc': 'Клонировать текущую тему и настроить её',
|
||||
'terminal.customTheme.newTitle': 'Новая пользовательская тема',
|
||||
'terminal.customTheme.editTitle': 'Редактировать тему',
|
||||
'terminal.customTheme.import': 'Импорт .itermcolors',
|
||||
'terminal.customTheme.importDesc': 'Импорт из файла цветовой схемы iTerm2',
|
||||
'terminal.customTheme.importError': 'Не удалось разобрать выбранный файл. Убедитесь, что это корректный XML-файл .itermcolors.',
|
||||
'terminal.customTheme.delete': 'Удалить тему',
|
||||
'terminal.customTheme.confirmDelete': 'Подтвердить удаление',
|
||||
'terminal.customTheme.name': 'Название',
|
||||
'terminal.customTheme.namePlaceholder': 'Моя пользовательская тема',
|
||||
'terminal.customTheme.type': 'Тип',
|
||||
'terminal.customTheme.group.general': 'Общие',
|
||||
'terminal.customTheme.group.normal': 'Обычные цвета',
|
||||
'terminal.customTheme.group.bright': 'Яркие цвета',
|
||||
'terminal.customTheme.color.background': 'Фон',
|
||||
'terminal.customTheme.color.foreground': 'Текст',
|
||||
'terminal.customTheme.color.cursor': 'Курсор',
|
||||
'terminal.customTheme.color.selection': 'Выделение',
|
||||
'terminal.customTheme.color.black': 'Чёрный',
|
||||
'terminal.customTheme.color.red': 'Красный',
|
||||
'terminal.customTheme.color.green': 'Зелёный',
|
||||
'terminal.customTheme.color.yellow': 'Жёлтый',
|
||||
'terminal.customTheme.color.blue': 'Синий',
|
||||
'terminal.customTheme.color.magenta': 'Пурпурный',
|
||||
'terminal.customTheme.color.cyan': 'Голубой',
|
||||
'terminal.customTheme.color.white': 'Белый',
|
||||
'terminal.customTheme.color.brightBlack': 'Яркий чёрный',
|
||||
'terminal.customTheme.color.brightRed': 'Яркий красный',
|
||||
'terminal.customTheme.color.brightGreen': 'Яркий зелёный',
|
||||
'terminal.customTheme.color.brightYellow': 'Яркий жёлтый',
|
||||
'terminal.customTheme.color.brightBlue': 'Яркий синий',
|
||||
'terminal.customTheme.color.brightMagenta': 'Яркий пурпурный',
|
||||
'terminal.customTheme.color.brightCyan': 'Яркий голубой',
|
||||
'terminal.customTheme.color.brightWhite': 'Яркий белый',
|
||||
|
||||
// Cloud Sync Settings
|
||||
'cloudSync.gate.title': 'Синхронизация с end-to-end шифрованием',
|
||||
'cloudSync.gate.desc':
|
||||
'Ваши данные шифруются локально перед синхронизацией. Облачные провайдеры никогда не видят ваши данные в открытом виде. Задайте мастер-ключ, чтобы включить безопасную синхронизацию.',
|
||||
'cloudSync.gate.masterKey': 'Мастер-ключ',
|
||||
'cloudSync.gate.confirmMasterKey': 'Подтвердите мастер-ключ',
|
||||
'cloudSync.gate.placeholder': 'Введите надёжный пароль',
|
||||
'cloudSync.gate.confirmPlaceholder': 'Подтвердите пароль',
|
||||
'cloudSync.gate.mismatch': 'Пароли не совпадают',
|
||||
'cloudSync.gate.warning':
|
||||
'Я понимаю, что если забуду мастер-ключ, мои данные нельзя будет восстановить. Сброс пароля невозможен.',
|
||||
'cloudSync.gate.enableVault': 'Включить зашифрованное хранилище',
|
||||
'cloudSync.gate.enabledToast': 'Зашифрованное хранилище включено',
|
||||
'cloudSync.gate.setupFailed': 'Не удалось настроить мастер-ключ',
|
||||
'cloudSync.passwordStrength.tooShort': 'Слишком короткий',
|
||||
'cloudSync.passwordStrength.weak': 'Слабый',
|
||||
'cloudSync.passwordStrength.moderate': 'Средний',
|
||||
'cloudSync.passwordStrength.strong': 'Сильный',
|
||||
'cloudSync.passwordStrength.veryStrong': 'Очень сильный',
|
||||
'cloudSync.provider.notConnected': 'Не подключено',
|
||||
'cloudSync.provider.sync': 'Синхронизация',
|
||||
'cloudSync.provider.connect': 'Подключить',
|
||||
'cloudSync.provider.connecting': 'Подключение...',
|
||||
'cloudSync.provider.webdav': 'WebDAV',
|
||||
'cloudSync.provider.webdav.desc': 'Подключение к самостоятельно размещённому WebDAV endpoint',
|
||||
'cloudSync.provider.s3': 'Совместимое с S3',
|
||||
'cloudSync.provider.s3.desc': 'Подключение к объектному хранилищу, совместимому с S3',
|
||||
'cloudSync.provider.comingSoon': 'Скоро',
|
||||
'cloudSync.webdav.title': 'Настройки WebDAV',
|
||||
'cloudSync.webdav.desc': 'Настройка WebDAV endpoint для зашифрованной синхронизации.',
|
||||
'cloudSync.webdav.endpoint': 'URL endpoint',
|
||||
'cloudSync.webdav.authType': 'Тип аутентификации',
|
||||
'cloudSync.webdav.auth.basic': 'Basic',
|
||||
'cloudSync.webdav.auth.digest': 'Digest',
|
||||
'cloudSync.webdav.auth.token': 'Токен',
|
||||
'cloudSync.webdav.username': 'Имя пользователя',
|
||||
'cloudSync.webdav.password': 'Пароль',
|
||||
'cloudSync.webdav.token': 'Токен',
|
||||
'cloudSync.webdav.showSecret': 'Показать секрет',
|
||||
'cloudSync.webdav.allowInsecure': 'Разрешить небезопасное соединение (игнорировать ошибки сертификата)',
|
||||
'cloudSync.webdav.validation.endpoint': 'Введите корректный WebDAV endpoint.',
|
||||
'cloudSync.webdav.validation.credentials': 'Имя пользователя и пароль обязательны.',
|
||||
'cloudSync.webdav.validation.token': 'Токен обязателен.',
|
||||
'cloudSync.s3.title': 'Настройки S3',
|
||||
'cloudSync.s3.desc': 'Подключение к объектному хранилищу, совместимому с S3, для зашифрованной синхронизации.',
|
||||
'cloudSync.s3.endpoint': 'URL endpoint',
|
||||
'cloudSync.s3.region': 'Регион',
|
||||
'cloudSync.s3.bucket': 'Бакет',
|
||||
'cloudSync.s3.accessKeyId': 'ID ключа доступа',
|
||||
'cloudSync.s3.secretAccessKey': 'Секретный ключ доступа',
|
||||
'cloudSync.s3.sessionToken': 'Токен сессии (необязательно)',
|
||||
'cloudSync.s3.prefix': 'Префикс ключа (необязательно)',
|
||||
'cloudSync.s3.forcePathStyle': 'Принудительно использовать path-style URL (для MinIO/R2 и т. д.)',
|
||||
'cloudSync.s3.allowInsecure': 'Разрешить небезопасное соединение (игнорировать ошибки сертификата)',
|
||||
'cloudSync.s3.showSecret': 'Показать секреты',
|
||||
'cloudSync.s3.validation.required': 'Endpoint, регион, бакет, access key и secret обязательны.',
|
||||
'cloudSync.smb.title': 'Настройки SMB',
|
||||
'cloudSync.smb.desc': 'Подключение к файловой SMB/CIFS-шаре для зашифрованной синхронизации.',
|
||||
'cloudSync.smb.share': 'Путь к шаре',
|
||||
'cloudSync.smb.username': 'Имя пользователя',
|
||||
'cloudSync.smb.password': 'Пароль',
|
||||
'cloudSync.smb.domain': 'Домен (необязательно)',
|
||||
'cloudSync.smb.domainPlaceholder': 'например, WORKGROUP',
|
||||
'cloudSync.smb.port': 'Порт (необязательно)',
|
||||
'cloudSync.smb.showSecret': 'Показать пароль',
|
||||
'cloudSync.smb.validation.share': 'Путь к шаре обязателен.',
|
||||
'cloudSync.smb.validation.port': 'Порт должен быть числом от 1 до 65535.',
|
||||
'cloudSync.connect.smb.success': 'SMB успешно подключён',
|
||||
'cloudSync.connect.smb.failedTitle': 'Ошибка подключения SMB',
|
||||
'cloudSync.provider.smb': 'SMB-шара',
|
||||
'cloudSync.connect.webdav.success': 'WebDAV успешно подключён',
|
||||
'cloudSync.connect.webdav.failedTitle': 'Ошибка подключения WebDAV',
|
||||
'cloudSync.connect.s3.success': 'S3 успешно подключён',
|
||||
'cloudSync.connect.s3.failedTitle': 'Ошибка подключения S3',
|
||||
'cloudSync.lastSync.never': 'Никогда',
|
||||
'cloudSync.lastSync.justNow': 'Только что',
|
||||
'cloudSync.lastSync.minutesAgo': '{minutes} мин назад',
|
||||
'cloudSync.changeKey': 'Изменить ключ',
|
||||
'cloudSync.providers.title': 'Облачные провайдеры',
|
||||
'cloudSync.syncAll': 'Синхронизировать всех подключённых провайдеров',
|
||||
'cloudSync.autoSync.title': 'Автосинхронизация',
|
||||
'cloudSync.autoSync.desc': 'Автоматически синхронизировать при внесении изменений',
|
||||
'cloudSync.strategy.title': 'Стратегия синхронизации',
|
||||
'cloudSync.strategy.desc': 'Выберите, что делать, когда изменились и локальные, и облачные данные.',
|
||||
'cloudSync.strategy.smartMerge': 'Умное объединение (рекомендуется)',
|
||||
'cloudSync.strategy.smartMergeDesc': 'По возможности объединять изменения с обеих сторон; если Netcatty не сможет безопасно выбрать, он попросит вас решить вручную.',
|
||||
'cloudSync.strategy.preferCloud': 'Приоритет облака',
|
||||
'cloudSync.strategy.preferCloudDesc': 'Когда изменились обе стороны, скачать облачную версию и заменить локальные изменения.',
|
||||
'cloudSync.strategy.preferLocal': 'Приоритет локальных данных',
|
||||
'cloudSync.strategy.preferLocalDesc': 'Когда изменились обе стороны, загрузить локальную версию и заменить облачные изменения.',
|
||||
'cloudSync.status.title': 'Статус синхронизации',
|
||||
'cloudSync.status.localVersion': 'Локальная версия',
|
||||
'cloudSync.status.remoteVersion': 'Удалённая версия',
|
||||
'cloudSync.history.title': 'История синхронизации',
|
||||
'cloudSync.history.upload': 'Загрузка',
|
||||
'cloudSync.history.download': 'Скачивание',
|
||||
'cloudSync.history.resolved': 'Разрешено',
|
||||
'cloudSync.history.error': 'Ошибка',
|
||||
'cloudSync.localBackups.title': 'История локальных резервных копий',
|
||||
'cloudSync.localBackups.desc': 'Netcatty сохраняет локальные точки восстановления перед сменой версии приложения и перед восстановлением хранилища.',
|
||||
'cloudSync.localBackups.retentionTitle': 'Хранение резервных копий',
|
||||
'cloudSync.localBackups.retentionDesc': 'Выберите, сколько локальных резервных копий должен хранить Netcatty.',
|
||||
'cloudSync.localBackups.maxCount': 'Макс. число копий',
|
||||
'cloudSync.localBackups.maxSaved': 'Хранение резервных копий: {count}',
|
||||
'cloudSync.localBackups.maxInvalid': 'Введите число от 1 до 100.',
|
||||
'cloudSync.localBackups.empty': 'Локальных резервных копий пока нет.',
|
||||
'cloudSync.localBackups.reason.appVersionChange': 'Перед сменой версии приложения',
|
||||
'cloudSync.localBackups.reason.beforeRestore': 'Перед восстановлением',
|
||||
'cloudSync.localBackups.versionChange': '{from} -> {to}',
|
||||
'cloudSync.localBackups.counts': '{hosts} хостов, {keys} ключей, {snippets} сниппетов, {notes} заметок',
|
||||
'cloudSync.localBackups.restore': 'Восстановить',
|
||||
'cloudSync.localBackups.restoreSuccess': 'Локальная резервная копия восстановлена.',
|
||||
'cloudSync.localBackups.restoreFailedTitle': 'Ошибка восстановления',
|
||||
'cloudSync.localBackups.restoreMissing': 'Резервная копия не найдена.',
|
||||
'cloudSync.localBackups.protectiveBackupFailed': 'Не удалось создать защитную резервную копию, поэтому восстановление было прервано для защиты ваших текущих данных. Устраните основную проблему (например, доступ к keychain) и попробуйте снова. Подробности: {message}',
|
||||
'cloudSync.localBackups.restoreConfirmTitle': 'Восстановить эту резервную копию?',
|
||||
'cloudSync.localBackups.restoreConfirmDesc': 'Ваши текущие хосты, ключи, сниппеты и настройки будут заменены содержимым этой резервной копии. Перед этим автоматически создаётся защитный снимок текущих данных.',
|
||||
'cloudSync.localBackups.restoreConfirmButton': 'Восстановить',
|
||||
'cloudSync.localBackups.restoreConfirmCancel': 'Отмена',
|
||||
'cloudSync.localBackups.unavailableTitle': 'Локальные резервные копии недоступны',
|
||||
'cloudSync.localBackups.unavailableDesc': 'Эта платформа не предоставляет Netcatty безопасное хранилище ключей, поэтому локальные резервные копии нельзя записывать безопасно. Установите Netcatty в систему с поддерживаемым keychain, чтобы включить историю локальных резервных копий.',
|
||||
'cloudSync.localBackups.lockedTitle': 'Требуется мастер-ключ',
|
||||
'cloudSync.localBackups.lockedDesc': 'Настройте или разблокируйте мастер-ключ перед восстановлением резервной копии, чтобы восстановленные учётные данные оставались зашифрованными.',
|
||||
'cloudSync.revisionHistory.viewButton': 'История',
|
||||
'cloudSync.revisionHistory.title': 'История версий хранилища',
|
||||
'cloudSync.revisionHistory.description': 'Просматривайте и восстанавливайте предыдущие версии вашего хранилища из истории ревизий Gist.',
|
||||
'cloudSync.revisionHistory.empty': 'Ревизии не найдены.',
|
||||
'cloudSync.revisionHistory.current': 'Текущая',
|
||||
'cloudSync.revisionHistory.revision': 'Ревизия',
|
||||
'cloudSync.revisionHistory.revisionPreview': 'Содержимое ревизии',
|
||||
'cloudSync.revisionHistory.device': 'Устройство',
|
||||
'cloudSync.revisionHistory.hosts': 'Хосты',
|
||||
'cloudSync.revisionHistory.keys': 'Ключи',
|
||||
'cloudSync.revisionHistory.snippets': 'Сниппеты',
|
||||
'cloudSync.revisionHistory.notes': 'Заметки',
|
||||
'cloudSync.revisionHistory.identities': 'Идентификаторы',
|
||||
'cloudSync.revisionHistory.restoreButton': 'Восстановить эту версию',
|
||||
'cloudSync.revisionHistory.restored': 'Хранилище восстановлено из выбранной ревизии.',
|
||||
'cloudSync.revisionHistory.revisionNotFound': 'Ревизия не найдена или не содержит данных хранилища.',
|
||||
'cloudSync.revisionHistory.decryptFailed': 'Не удалось расшифровать эту ревизию. Возможно, она была зашифрована другим мастер-паролем.',
|
||||
'cloudSync.changeKey.title': 'Изменить мастер-ключ',
|
||||
'cloudSync.changeKey.current': 'Текущий мастер-ключ',
|
||||
'cloudSync.changeKey.new': 'Новый мастер-ключ',
|
||||
'cloudSync.changeKey.confirmNew': 'Подтвердите новый мастер-ключ',
|
||||
'cloudSync.changeKey.currentPlaceholder': 'Введите текущий мастер-ключ',
|
||||
'cloudSync.changeKey.newPlaceholder': 'Введите новый мастер-ключ',
|
||||
'cloudSync.changeKey.confirmPlaceholder': 'Подтвердите новый мастер-ключ',
|
||||
'cloudSync.changeKey.fillAll': 'Пожалуйста, заполните все поля',
|
||||
'cloudSync.changeKey.minLength': 'Новый мастер-ключ должен содержать не менее 8 символов',
|
||||
'cloudSync.changeKey.notMatch': 'Новые мастер-ключи не совпадают',
|
||||
'cloudSync.changeKey.incorrectCurrent': 'Неверный текущий мастер-ключ',
|
||||
'cloudSync.changeKey.failed': 'Не удалось изменить мастер-ключ',
|
||||
'cloudSync.changeKey.desc': 'Это заново зашифрует ваше хранилище. Убедитесь, что вы помните новый ключ.',
|
||||
'cloudSync.changeKey.showKeys': 'Показать ключи',
|
||||
'cloudSync.changeKey.updatedToast': 'Мастер-ключ обновлён',
|
||||
'cloudSync.changeKey.updateButton': 'Обновить ключ',
|
||||
'cloudSync.unlock.title': 'Введите мастер-ключ',
|
||||
'cloudSync.unlock.masterKey': 'Мастер-ключ',
|
||||
'cloudSync.unlock.desc':
|
||||
'Введите мастер-ключ один раз, чтобы включить зашифрованную синхронизацию. Он будет безопасно сохранён в системном keychain.',
|
||||
'cloudSync.unlock.placeholder': 'Введите мастер-ключ',
|
||||
'cloudSync.unlock.empty': 'Пожалуйста, введите мастер-ключ',
|
||||
'cloudSync.unlock.incorrect': 'Неверный мастер-ключ',
|
||||
'cloudSync.unlock.failed': 'Не удалось разблокировать хранилище',
|
||||
'cloudSync.unlock.showKey': 'Показать ключ',
|
||||
'cloudSync.unlock.notNow': 'Не сейчас',
|
||||
'cloudSync.unlock.readyToast': 'Хранилище готово',
|
||||
'cloudSync.unlock.unlockButton': 'Разблокировать',
|
||||
'cloudSync.header.vaultReady': 'Хранилище готово',
|
||||
'cloudSync.header.preparingVault': 'Подготовка хранилища...',
|
||||
'cloudSync.header.providersConnected': 'Подключено провайдеров: {count}',
|
||||
'cloudSync.githubFlow.title': 'Подключить GitHub',
|
||||
'cloudSync.githubFlow.desc': 'Скопируйте код ниже и введите его на GitHub, чтобы авторизовать Netcatty.',
|
||||
'cloudSync.githubFlow.copyCode': 'Скопировать код',
|
||||
'cloudSync.githubFlow.copied': 'Скопировано!',
|
||||
'cloudSync.githubFlow.openGitHub': 'Открыть GitHub',
|
||||
'cloudSync.githubFlow.waiting': 'Ожидание авторизации...',
|
||||
'cloudSync.conflict.title': 'Обнаружен конфликт версий',
|
||||
'cloudSync.conflict.desc': 'Выберите, какую версию сохранить',
|
||||
'cloudSync.conflict.local': 'ЛОКАЛЬНАЯ',
|
||||
'cloudSync.conflict.cloud': 'ОБЛАЧНАЯ',
|
||||
'cloudSync.conflict.detailsTitle': 'Изменённые данные',
|
||||
'cloudSync.conflict.detailsCounts': 'Локально {local} · Облако {cloud} · Конфликты {conflicts}',
|
||||
'cloudSync.conflict.entity.hosts': 'Хосты',
|
||||
'cloudSync.conflict.entity.keys': 'Ключи',
|
||||
'cloudSync.conflict.entity.identities': 'Идентификаторы',
|
||||
'cloudSync.conflict.entity.proxyProfiles': 'Профили прокси',
|
||||
'cloudSync.conflict.entity.snippets': 'Сниппеты',
|
||||
'cloudSync.conflict.entity.notes': 'Заметки',
|
||||
'cloudSync.conflict.entity.noteGroups': 'Группы заметок',
|
||||
'cloudSync.conflict.entity.customGroups': 'Группы',
|
||||
'cloudSync.conflict.entity.snippetPackages': 'Пакеты сниппетов',
|
||||
'cloudSync.conflict.entity.portForwardingRules': 'Проброс портов',
|
||||
'cloudSync.conflict.entity.groupConfigs': 'Настройки групп',
|
||||
'cloudSync.conflict.entity.settings': 'Настройки',
|
||||
'cloudSync.conflict.keepLocal': 'Перезаписать облако (сохранить локальную)',
|
||||
'cloudSync.conflict.useCloud': 'Скачать из облака (перезаписать локальную)',
|
||||
'cloudSync.connect.browserContinue': 'Завершите авторизацию в браузере',
|
||||
'cloudSync.connect.browserCancelled': 'Предыдущая авторизация в браузере была отменена',
|
||||
'cloudSync.connect.github.success': 'GitHub успешно подключён',
|
||||
'cloudSync.connect.github.failedTitle': 'Ошибка подключения GitHub',
|
||||
'cloudSync.connect.github.timeout': 'Время подключения к GitHub истекло. Проверьте сеть или настройки прокси.',
|
||||
'cloudSync.connect.github.networkError': 'Не удалось связаться с GitHub. Проверьте сеть или настройки прокси.',
|
||||
'cloudSync.connect.google.failedTitle': 'Ошибка подключения Google',
|
||||
'cloudSync.connect.onedrive.failedTitle': 'Ошибка подключения OneDrive',
|
||||
'cloudSync.sync.success': 'Синхронизировано с {provider}',
|
||||
'cloudSync.sync.failed': 'Синхронизация не удалась',
|
||||
'cloudSync.sync.failedTitle': 'Синхронизация не удалась',
|
||||
'cloudSync.sync.errorTitle': 'Ошибка синхронизации',
|
||||
'cloudSync.resolve.downloaded': 'Скачаны данные из облака',
|
||||
'cloudSync.resolve.uploaded': 'Загружены локальные данные',
|
||||
'cloudSync.resolve.failedTitle': 'Не удалось разрешить конфликт',
|
||||
'cloudSync.clearLocal.title': 'Очистить локальные данные',
|
||||
'cloudSync.clearLocal.desc': 'Сбросить локальную версию и историю синхронизации. При следующей синхронизации данные будут скачаны из облака.',
|
||||
'cloudSync.clearLocal.button': 'Очистить',
|
||||
'cloudSync.clearLocal.dialog.title': 'Очистить локальные данные хранилища?',
|
||||
'cloudSync.clearLocal.dialog.desc': 'Локальная версия будет сброшена до 0, а история синхронизации очищена. При следующей синхронизации данные будут скачаны из облака и заменят локальные.',
|
||||
'cloudSync.clearLocal.dialog.cancel': 'Отмена',
|
||||
'cloudSync.clearLocal.dialog.confirm': 'Очистить локальные данные',
|
||||
'cloudSync.clearLocal.toast.title': 'Локальные данные очищены',
|
||||
'cloudSync.clearLocal.toast.desc': 'Локальная версия сброшена до 0. Выполните синхронизацию для загрузки из облака.',
|
||||
|
||||
// Keychain
|
||||
'keychain.filter.key': 'Ключ',
|
||||
'keychain.filter.certificate': 'Сертификат',
|
||||
'keychain.action.generateKey': 'Создать ключ',
|
||||
'keychain.action.importKey': 'Импорт. ключ',
|
||||
'keychain.action.newIdentity': 'Новый ид-катор',
|
||||
'keychain.action.importCertificate': 'Импорт. сертификат',
|
||||
'keychain.view.grid': 'Сетка',
|
||||
'keychain.view.list': 'Список',
|
||||
'keychain.section.keys': 'Ключи',
|
||||
'keychain.section.identities': 'Идентификаторы',
|
||||
'keychain.count.items': '{count} запис(ей)',
|
||||
'keychain.empty.title': 'Настройте свои ключи',
|
||||
'keychain.empty.desc': 'Импортируйте или создайте SSH-ключи для безопасной аутентификации.',
|
||||
'keychain.panel.generateKey': 'Сгенерировать ключ',
|
||||
'keychain.panel.newKey': 'Новый ключ',
|
||||
'keychain.panel.keyDetails': 'Сведения о ключе',
|
||||
'keychain.panel.editKey': 'Редактировать ключ',
|
||||
'keychain.panel.editIdentity': 'Редактировать идентификатор',
|
||||
'keychain.panel.newIdentity': 'Новый идентификатор',
|
||||
'keychain.panel.keyExport': 'Экспорт ключа',
|
||||
'keychain.validation.labelRequired': 'Пожалуйста, введите метку для ключа',
|
||||
'keychain.validation.labelAndPrivateKeyRequired': 'Метка и приватный ключ обязательны',
|
||||
'keychain.validation.labelAndUsernameRequired': 'Метка и имя пользователя обязательны',
|
||||
'keychain.error.generationUnavailable': 'Генератор ключей не работает - пожалуйста, убедитесь, что приложение работает в Electron',
|
||||
'keychain.error.generateKeyPairFailed': 'Не удалось сгенерировать пару ключей',
|
||||
'keychain.error.generateKeyFailed': 'Не удалось сгенерировать ключ',
|
||||
'keychain.error.keyGenerationTitle': 'Генерация ключа',
|
||||
'keychain.export.exportTo': 'Экспортировать в *',
|
||||
'keychain.export.selectHost': 'Выберите хост',
|
||||
'keychain.export.location': 'Расположение ~ $1 *',
|
||||
'keychain.export.filename': 'Имя файла ~ $2 *',
|
||||
'keychain.export.note': 'Экспорт ключей сейчас поддерживается только в системах {unix}. Используйте раздел {advanced} для настройки скрипта экспорта.',
|
||||
'keychain.export.script': 'Скрипт *',
|
||||
'keychain.export.scriptPlaceholder': 'Скрипт экспорта...',
|
||||
'keychain.export.missingCredentials': 'У хоста нет сохранённого пароля или ключа. Сначала добавьте в хост учётные данные с паролем.',
|
||||
'keychain.export.successTitle': 'Экспорт выполнен успешно',
|
||||
'keychain.export.successMessage': 'Публичный ключ экспортирован и привязан к {host}',
|
||||
'keychain.export.failedTitle': 'Ошибка экспорта',
|
||||
'keychain.export.failedMessage': 'Не удалось экспортировать ключ: {error}',
|
||||
'keychain.export.failedPrefix': 'Ошибка экспорта: {error}',
|
||||
'keychain.export.exitCode': 'Команда завершилась с кодом {code}',
|
||||
'keychain.export.exporting': 'Экспорт...',
|
||||
'keychain.export.exportAndAttach': 'Экспортировать и привязать',
|
||||
'keychain.export.title': 'Экспорт ключа',
|
||||
'keychain.export.exportToRequired': 'Экспортировать в *',
|
||||
'keychain.export.selectHostPlaceholder': 'Выберите хост...',
|
||||
'keychain.export.locationLabel': 'Расположение ~ $1 *',
|
||||
'keychain.export.filenameLabel': 'Имя файла ~ $2 *',
|
||||
'keychain.export.advanced': 'Дополнительно',
|
||||
'keychain.export.note.supportsOnly': 'Экспорт ключей сейчас поддерживается только в',
|
||||
'keychain.export.note.systems': 'системах.',
|
||||
'keychain.export.note.use': 'Используйте',
|
||||
'keychain.export.note.customize': 'раздел для настройки скрипта экспорта.',
|
||||
'keychain.export.scriptRequired': 'Скрипт *',
|
||||
'keychain.export.exportToHost': 'Экспортировать на хост',
|
||||
'keychain.export.failedGeneric': 'Ошибка экспорта: {message}',
|
||||
'keychain.field.label': 'Метка',
|
||||
'keychain.field.labelRequired': 'Метка *',
|
||||
'keychain.field.labelPlaceholder': 'Метка ключа',
|
||||
'keychain.field.privateKeyRequired': 'Приватный ключ *',
|
||||
'keychain.field.publicKey': 'Публичный ключ',
|
||||
'keychain.field.certificatePlaceholder': 'Содержимое сертификата (необязательно)',
|
||||
'keychain.generate.keyType': 'Тип ключа',
|
||||
'keychain.generate.keySize': 'Размер ключа',
|
||||
'keychain.generate.labelPlaceholder': 'Метка ключа',
|
||||
'keychain.generate.passphrasePlaceholder': 'Парольная фраза (необязательно)',
|
||||
'keychain.generate.savePassphrase': 'Сохранить парольную фразу',
|
||||
'keychain.generate.generate': 'Сгенерировать',
|
||||
'keychain.generate.generateSave': 'Сгенерировать и сохранить',
|
||||
'keychain.import.dropHint': 'Перетащите сюда файл ключа',
|
||||
'keychain.import.importFromFile': 'Импортировать из файла',
|
||||
'keychain.import.saveKey': 'Сохранить ключ',
|
||||
'keychain.import.importedKeyLabel': 'Импортированный ключ',
|
||||
'keychain.identity.usernameRequired': 'Имя пользователя *',
|
||||
'keychain.identity.method.passwordOnly': 'Пароль',
|
||||
'keychain.identity.summary.password': 'Пароль аутентификации',
|
||||
'keychain.identity.summary.key': 'Ключ аутентификации',
|
||||
'keychain.identity.summary.certificate': 'Сертификат аутентификации',
|
||||
'keychain.identity.summary.passwordAndKey': 'Пароль и ключ аутентификации',
|
||||
'keychain.identity.summary.passwordAndCertificate': 'Пароль и сертификат аутентификации',
|
||||
'keychain.identity.summary.none': 'Нет учётных данных',
|
||||
'keychain.identity.selectCredential': 'Выберите {kind}',
|
||||
'keychain.identity.save': 'Сохранить',
|
||||
'keychain.identity.update': 'Обновить',
|
||||
'keychain.keyDialog.newTitle': 'Новый ключ',
|
||||
'keychain.keyDialog.newDesc': 'Добавить новый SSH-ключ',
|
||||
'keychain.keyDialog.editTitle': 'Редактировать ключ',
|
||||
'keychain.keyDialog.editDesc': 'Обновить этот SSH-ключ',
|
||||
'keychain.keyDialog.updateKey': 'Обновить ключ',
|
||||
|
||||
// Tabs
|
||||
'tabs.closeSessionAria': 'Закрыть сессию',
|
||||
'tabs.closeLogViewAria': 'Закрыть просмотр журнала',
|
||||
'tabs.logPrefix': 'Журнал:',
|
||||
'tabs.logLocal': 'Локальный',
|
||||
'tabs.copyTab': 'Копировать вкладку',
|
||||
'tabs.copyTabToNewWindow': 'Копировать вкладку в новое окно',
|
||||
'tabs.copyTabToNewWindowFailed': 'Не удалось открыть вкладку в новом окне',
|
||||
'tabs.closeOthers': 'Закрыть остальные',
|
||||
'tabs.closeToRight': 'Закрыть вкладки справа',
|
||||
'tabs.closeAll': 'Закрыть все',
|
||||
'keychain.edit.labelRequired': 'Метка *',
|
||||
'keychain.edit.keyLabelPlaceholder': 'Метка ключа',
|
||||
'keychain.edit.privateKeyRequired': 'Приватный ключ *',
|
||||
'keychain.edit.publicKey': 'Публичный ключ',
|
||||
'keychain.edit.certificate': 'Сертификат',
|
||||
'keychain.edit.certificatePlaceholder': 'Содержимое сертификата (необязательно)',
|
||||
'keychain.edit.filePath': 'Путь к файлу',
|
||||
'keychain.edit.keyExport': 'Экспорт ключа',
|
||||
'keychain.edit.exportToHost': 'Экспортировать на хост',
|
||||
|
||||
// Snippets
|
||||
'snippets.searchPlaceholder': 'Поиск сниппетов...',
|
||||
'snippets.action.newSnippet': 'Новый сниппет',
|
||||
'snippets.action.newPackage': 'Новый пакет',
|
||||
'snippets.action.import': 'Импорт',
|
||||
'snippets.action.selectSnippets': 'Выбрать сниппеты',
|
||||
'snippets.panel.newTitle': 'Новый сниппет',
|
||||
'snippets.panel.editTitle': 'Редактировать сниппет',
|
||||
'snippets.panel.resizeWidth': 'Изменить ширину панели',
|
||||
'snippets.field.description': 'Описание действия',
|
||||
'snippets.field.descriptionPlaceholder': 'Например: проверить сетевую нагрузку',
|
||||
'snippets.field.package': 'Добавить пакет',
|
||||
'snippets.field.packagePlaceholder': 'Выберите или создайте пакет',
|
||||
'snippets.field.createPackage': 'Создать пакет',
|
||||
'snippets.field.scriptRequired': 'Скрипт *',
|
||||
'snippets.scriptEditor.expand': 'Открыть в окне',
|
||||
'snippets.scriptEditor.resize': 'Изменить высоту редактора',
|
||||
'snippets.scriptEditor.modalTitle': 'Редактировать скрипт',
|
||||
'snippets.variables.dialogTitle': 'Переменные сниппета',
|
||||
'snippets.variables.dialogDesc': 'Заполните значения для "{label}" перед запуском.',
|
||||
'snippets.variables.hint': 'Значения вставляются в скрипт как есть (без shell-экранирования).',
|
||||
'snippets.variables.preview': 'Предпросмотр',
|
||||
'snippets.variables.placeholder': 'Введите значение',
|
||||
'snippets.variables.placeholderDefault': 'По умолчанию: {value}',
|
||||
'snippets.variables.required': 'Эта переменная обязательна',
|
||||
'snippets.variables.run': 'Запустить',
|
||||
'snippets.field.variablesHelp': 'Используйте {{name}} или {{name:default}} для плейсхолдеров в скрипте.',
|
||||
'snippets.field.variablesDetected': 'Переменные',
|
||||
'snippets.field.variableDefault': 'по умолчанию {value}',
|
||||
'snippets.targets.title': 'Цели',
|
||||
'snippets.targets.add': 'Добавить цели',
|
||||
'snippets.history.title': 'История оболочки',
|
||||
'snippets.history.subtitle': '{count} команд',
|
||||
'snippets.history.emptyTitle': 'История оболочки пока пуста',
|
||||
'snippets.history.emptyDesc': 'Здесь будут появляться выполненные вами команды',
|
||||
'snippets.history.loadMore': 'Загрузить ещё',
|
||||
'snippets.history.separator': '•',
|
||||
'snippets.history.labelPlaceholder': 'Задайте метку для этого сниппета',
|
||||
'snippets.history.saveAsSnippet': 'Сохранить как сниппет',
|
||||
'snippets.history.time.justNow': 'только что',
|
||||
'snippets.history.time.minutesAgo': '{count}м назад',
|
||||
'snippets.history.time.hoursAgo': '{count}ч назад',
|
||||
'snippets.history.time.daysAgo': '{count}д назад',
|
||||
'snippets.breadcrumb.allPackages': 'Все пакеты',
|
||||
'snippets.breadcrumb.separator': '›',
|
||||
'snippets.empty.title': 'Создать сниппет',
|
||||
'snippets.empty.desc': 'Сохраняйте самые используемые команды как сниппеты, чтобы повторно использовать их в один клик.',
|
||||
'snippets.search.noResults.title': 'Нет совпадений',
|
||||
'snippets.search.noResults.desc': 'Ни один сниппет или пакет не соответствует запросу "{query}". Попробуйте другой поисковый запрос или очистите поиск для просмотра.',
|
||||
'snippets.section.packages': 'Пакеты',
|
||||
'snippets.section.snippets': 'Сниппеты',
|
||||
'snippets.package.count': '{count} сниппет(ов)',
|
||||
'snippets.commandFallback': 'Команда',
|
||||
'snippets.view.grid': 'Сетка',
|
||||
'snippets.view.list': 'Список',
|
||||
'snippets.selection.selected': 'Выбрано: {count}',
|
||||
'snippets.selection.selectVisible': 'Выбрать видимые',
|
||||
'snippets.selection.deselectAll': 'Снять выбор',
|
||||
'snippets.selection.exportSelected': 'Экспорт выбранных ({count})',
|
||||
'snippets.export.snippet': 'Экспорт сниппета',
|
||||
'snippets.export.package': 'Экспорт пакета',
|
||||
'snippets.export.toast.empty': 'Нет сниппетов для экспорта.',
|
||||
'snippets.export.toast.successTitle': 'Экспорт готов',
|
||||
'snippets.export.toast.success': 'Экспортировано сниппетов: {count}.',
|
||||
'snippets.import.toast.empty': 'Не найдено сниппетов для импорта.',
|
||||
'snippets.import.toast.failedTitle': 'Импорт не удался',
|
||||
'snippets.import.toast.invalidDesc': 'Это невалидный файл сниппетов Netcatty.',
|
||||
'snippets.import.toast.successTitle': 'Импорт завершён',
|
||||
'snippets.import.toast.summary': 'Импортировано: {imported}, перезаписано: {overwritten}, пропущено: {skipped}.',
|
||||
'snippets.import.modal.title': 'Импорт сниппетов',
|
||||
'snippets.import.modal.desc': 'Выберите один или несколько JSON-файлов сниппетов Netcatty. Netcatty сначала покажет результат разбора и ничего не импортирует без подтверждения.',
|
||||
'snippets.import.modal.exampleTitle': 'Пример JSON',
|
||||
'snippets.import.modal.noFile': 'Файл ещё не выбран. Используйте пример ниже или обычный JSON-массив сниппетов, затем выберите один или несколько файлов.',
|
||||
'snippets.import.modal.chooseFile': 'Выбрать файл',
|
||||
'snippets.import.modal.downloadExamples': 'Скачать примеры',
|
||||
'snippets.import.modal.multipleFiles': 'Выбрано файлов: {count}',
|
||||
'snippets.import.modal.parsedSummary': '{files} файл(ов), {total} сниппет(ов), {packages} пакет(ов), повторяющихся команд: {conflicts}. Привязки к хостам игнорируются.',
|
||||
'snippets.import.modal.confirm': 'Подтвердить импорт',
|
||||
'snippets.import.conflict.title': 'Импортировать сниппеты?',
|
||||
'snippets.import.conflict.desc': 'Файл «{file}» содержит {total} сниппет(ов), из них повторяющихся команд: {conflicts}.',
|
||||
'snippets.import.conflict.hostBindingsNote': 'Привязки сниппетов к хостам не импортируются и не экспортируются.',
|
||||
'snippets.import.conflict.skip': 'Пропустить повторы',
|
||||
'snippets.import.conflict.overwrite': 'Перезаписать повторы',
|
||||
'snippets.packageDialog.title': 'Новый пакет',
|
||||
'snippets.packageDialog.parent': 'Родитель: {parent}',
|
||||
'snippets.packageDialog.root': 'Корень',
|
||||
'snippets.packageDialog.placeholder': 'например, ops/maintenance',
|
||||
'snippets.packageDialog.hint': 'Используйте "/" для создания вложенных пакетов.',
|
||||
|
||||
// Snippets Rename Dialog
|
||||
'snippets.renameDialog.title': 'Переименовать пакет',
|
||||
'snippets.renameDialog.currentPath': 'Текущий путь: {path}',
|
||||
'snippets.renameDialog.placeholder': 'Введите новое имя',
|
||||
'snippets.renameDialog.error.empty': 'Имя пакета не может быть пустым',
|
||||
'snippets.renameDialog.error.duplicate': 'Пакет с таким именем уже существует',
|
||||
'snippets.renameDialog.error.invalidChars': 'Имя пакета может содержать только буквы, цифры, дефисы и подчёркивания',
|
||||
|
||||
'snippets.field.noAutoRun': 'Только вставить (не выполнять автоматически)',
|
||||
'snippets.field.multiLineRunMode': 'Многострочный запуск',
|
||||
'snippets.field.multiLineRunMode.paste': 'Отправить сразу',
|
||||
'snippets.field.multiLineRunMode.lineDelay': 'Отправлять построчно',
|
||||
'snippets.field.multiLineRunModeHint': 'Построчный режим подходит для логинов с подсказками и макросов устройств.',
|
||||
// Snippet Shortkey
|
||||
'snippets.field.shortkey': 'Сочетание клавиш',
|
||||
'snippets.shortkey.placeholder': 'Нажмите, чтобы задать сочетание',
|
||||
'snippets.shortkey.recording': 'Нажмите сочетание клавиш...',
|
||||
'snippets.shortkey.hint': 'Нажмите это сочетание в терминале, чтобы быстро отправить команду.',
|
||||
'snippets.shortkey.clear': 'Очистить сочетание',
|
||||
'snippets.shortkey.error.systemConflict': 'Это сочетание конфликтует с системным сочетанием',
|
||||
'snippets.shortkey.error.snippetConflict': 'Это сочетание уже используется сниппетом: {name}',
|
||||
|
||||
// Serial Port
|
||||
'serial.button': 'Серийный',
|
||||
'serial.modal.title': 'Подключение к последовательному порту',
|
||||
'serial.modal.desc': 'Настройте параметры подключения к последовательному порту',
|
||||
'serial.field.port': 'Последовательный порт',
|
||||
'serial.field.selectPort': 'Выберите порт...',
|
||||
'serial.field.baudRate': 'Скорость передачи',
|
||||
'serial.field.dataBits': 'Биты данных',
|
||||
'serial.field.stopBits': 'Стоп-биты',
|
||||
'serial.field.stopBits15Warning': '1.5 stop bits may not be supported on all Windows devices',
|
||||
'serial.field.parity': 'Чётность',
|
||||
'serial.field.flowControl': 'Управление потоком',
|
||||
'serial.noPorts': 'Последовательные порты не обнаружены. Подключите устройство и обновите список.',
|
||||
'serial.field.customPort': 'Путь к пользовательскому порту',
|
||||
'serial.field.customPortPlaceholder': 'например, /dev/ttys001 или COM1',
|
||||
'serial.type.hardware': 'Аппаратный',
|
||||
'serial.type.pseudo': 'Псевдотерминал',
|
||||
'serial.type.custom': 'Пользовательский',
|
||||
'serial.parity.none': 'Нет',
|
||||
'serial.parity.even': 'Чётная',
|
||||
'serial.parity.odd': 'Нечётная',
|
||||
'serial.parity.mark': 'Mark',
|
||||
'serial.parity.space': 'Space',
|
||||
'serial.flowControl.none': 'Нет',
|
||||
'serial.flowControl.xon/xoff': 'XON/XOFF (программный)',
|
||||
'serial.flowControl.rts/cts': 'RTS/CTS (аппаратный)',
|
||||
'serial.field.localEcho': 'Принудительное локальное эхо',
|
||||
'serial.field.localEchoDesc': 'Локально отображать вводимые символы (для устройств без удалённого эха)',
|
||||
'serial.field.lineMode': 'Построчный режим',
|
||||
'serial.field.lineModeDesc': 'Буферизовать ввод и отправлять по Enter (вместо посимвольной отправки)',
|
||||
'serial.field.charset': 'Кодировка',
|
||||
'serial.connectionError': 'Не удалось подключиться к последовательному порту',
|
||||
'serial.field.baudRatePlaceholder': 'Выберите или введите скорость...',
|
||||
'serial.field.baudRateEmpty': 'Введите пользовательскую скорость передачи',
|
||||
'serial.field.customBaudRate': 'Используется пользовательская скорость передачи',
|
||||
'serial.field.saveConfig': 'Сохранить конфигурацию',
|
||||
'serial.field.saveConfigDesc': 'Сохраните эту последовательную конфигурацию в хостах для быстрого доступа',
|
||||
'serial.field.configLabel': 'Имя конфигурации',
|
||||
'serial.field.configLabelPlaceholder': 'например, Arduino Uno',
|
||||
'serial.connectAndSave': 'Подключить и сохранить',
|
||||
'serial.edit.title': 'Настройки последовательного порта',
|
||||
|
||||
// Keyboard Interactive Authentication (2FA/MFA)
|
||||
'keyboard.interactive.title': 'Требуется аутентификация',
|
||||
'keyboard.interactive.desc': 'Сервер требует дополнительную аутентификацию.',
|
||||
'keyboard.interactive.descWithHost': 'Сервер {hostname} требует дополнительную аутентификацию.',
|
||||
'keyboard.interactive.response': 'Ответ',
|
||||
'keyboard.interactive.enterCode': 'Введите код подтверждения',
|
||||
'keyboard.interactive.enterResponse': 'Введите ответ',
|
||||
'keyboard.interactive.submit': 'Отправить',
|
||||
'keyboard.interactive.verifying': 'Проверка...',
|
||||
'keyboard.interactive.savePassword': 'Сохранить пароль',
|
||||
|
||||
// Passphrase Modal for encrypted SSH keys
|
||||
'passphrase.title': 'Парольная фраза SSH-ключа',
|
||||
'passphrase.desc': 'Введите парольную фразу для {keyName}',
|
||||
'passphrase.descWithHost': 'Введите парольную фразу для {keyName}, чтобы подключиться к {hostname}',
|
||||
'passphrase.label': 'Парольная фраза',
|
||||
'passphrase.keyPath': 'Ключ',
|
||||
'passphrase.unlock': 'Разблокировать',
|
||||
'passphrase.unlocking': 'Разблокировка...',
|
||||
'passphrase.skip': 'Пропустить',
|
||||
'passphrase.remember': 'Запомнить эту парольную фразу',
|
||||
|
||||
// Text Editor
|
||||
'sftp.editor.wordWrap': 'Перенос строк',
|
||||
'sftp.editor.maximize': 'Развернуть',
|
||||
'sftp.editor.unsavedTitle': 'Несохранённые изменения',
|
||||
'sftp.editor.unsavedMessage': 'В файле {fileName} есть несохранённые изменения. Сохранить перед закрытием?',
|
||||
'sftp.editor.discardChanges': 'Отбросить',
|
||||
'sftp.editor.saveAndClose': 'Сохранить и закрыть',
|
||||
'sftp.editor.quitBlockedByDirty': 'Есть несохранённые редакторы — перед выходом сохраните изменения или отбросьте их',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,802 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const ruVaultMessages: Messages = {
|
||||
// Notes
|
||||
'notes.search.placeholder': 'Поиск заметок, хостов, тегов...',
|
||||
'notes.action.newNote': 'Новая заметка',
|
||||
'notes.action.newGroup': 'Новая папка',
|
||||
'notes.action.importMarkdown': 'Импорт Markdown',
|
||||
'notes.action.exportNote': 'Экспорт заметки',
|
||||
'notes.action.exportGroup': 'Экспорт папки',
|
||||
'notes.action.exportAll': 'Экспорт заметок',
|
||||
'notes.import.toast.success': 'Импортировано заметок: {count}',
|
||||
'notes.import.toast.noNotes': 'Markdown-файлы не были импортированы.',
|
||||
'notes.import.toast.failed': 'Не удалось импортировать Markdown-файлы.',
|
||||
'notes.import.toast.inProgress': 'Импорт Markdown уже выполняется.',
|
||||
'notes.import.toast.skipped': 'Пропущено неподдерживаемых файлов: {count}.',
|
||||
'notes.export.toast.success': 'Экспортировано заметок: {count}',
|
||||
'notes.export.toast.empty': 'Нет заметок для экспорта.',
|
||||
'notes.export.toast.failed': 'Не удалось экспортировать заметки.',
|
||||
'notes.codeBlock.copied': 'Скопировано',
|
||||
'notes.codeBlock.copyFailed': 'Не удалось скопировать блок кода.',
|
||||
'notes.group.prompt': 'Путь папки',
|
||||
'notes.group.all': 'Блокнот',
|
||||
'notes.group.ungrouped': 'Без группы',
|
||||
'notes.section.notebooks': 'Блокнот',
|
||||
'notes.section.notes': 'Заметки',
|
||||
'notes.mode.edit': 'Правка',
|
||||
'notes.mode.split': 'Разделить',
|
||||
'notes.mode.preview': 'Просмотр',
|
||||
'notes.title.placeholder': 'Заголовок заметки',
|
||||
'notes.editor.placeholder': 'Пишите Markdown-заметки здесь...',
|
||||
'notes.preview.empty': 'Пока нечего просматривать.',
|
||||
'notes.empty.title': 'Заметок пока нет',
|
||||
'notes.empty.desc': 'Создавайте Markdown-заметки для инструкций, напоминаний и передачи серверов.',
|
||||
'notes.empty.group': 'В этом блокноте пока нет заметок.',
|
||||
'notes.search.noResults': 'Заметки не найдены.',
|
||||
|
||||
// Vault hosts header/actions
|
||||
'vault.hosts.search.placeholder': 'Найти хост или ssh user@hostname / ssh -p 2222 user@hostname...',
|
||||
'vault.hosts.connect': 'Подключиться',
|
||||
'vault.view.grid': 'Сетка',
|
||||
'vault.view.list': 'Список',
|
||||
'vault.view.tree': 'Дерево',
|
||||
'vault.tree.expandAll': 'Развернуть все',
|
||||
'vault.tree.collapseAll': 'Свернуть все',
|
||||
'vault.hosts.newHost': 'Новый хост',
|
||||
'vault.hosts.newGroup': 'Новая группа',
|
||||
'vault.hosts.import': 'Импорт',
|
||||
'vault.hosts.export': 'Экспорт',
|
||||
'vault.hosts.export.toast.success': 'Экспортировано {count} хостов в CSV',
|
||||
'vault.hosts.export.toast.successWithSkipped': 'Экспортировано {count} хостов в CSV ({skipped} неподдерживаемых хостов пропущено)',
|
||||
'vault.hosts.export.toast.noHosts': 'Нет хостов для экспорта',
|
||||
'vault.hosts.allHosts': 'Все хосты',
|
||||
'vault.hosts.pinned': 'Закреплённые',
|
||||
'vault.hosts.recentlyConnected': 'Недавно подключённые',
|
||||
'vault.hosts.pinToTop': 'Закрепить сверху',
|
||||
'vault.hosts.unpin': 'Открепить',
|
||||
'vault.hosts.copyCredentials': 'Копировать учётные данные',
|
||||
'vault.hosts.copyCredentials.toast.success': 'Учётные данные скопированы в буфер обмена',
|
||||
'vault.hosts.copyCredentials.toast.noPassword': 'Для этого хоста нет сохранённого пароля',
|
||||
'vault.hosts.multiSelect': 'Множественный выбор',
|
||||
'vault.hosts.selected': 'Выбрано: {count}',
|
||||
'vault.hosts.selectAll': 'Выбрать все',
|
||||
'vault.hosts.deselectAll': 'Снять выделение',
|
||||
'vault.hosts.deleteSelected': 'Удалить ({count})',
|
||||
'vault.hosts.deleteMultiple.success': 'Удалено хостов: {count}',
|
||||
'vault.hosts.connectSelected': 'Подключить ({count})',
|
||||
'vault.hosts.connectMultiple.success': 'Подключение хостов: {count}',
|
||||
'vault.hosts.moveToGroup.success': 'Хост {host} перемещён в {group}',
|
||||
'vault.hosts.empty.title': 'Настройте свои хосты',
|
||||
'vault.hosts.empty.desc': 'Сохраняйте хосты, чтобы быстро подключаться к серверам, виртуальным машинам и контейнерам.',
|
||||
|
||||
// Vault import
|
||||
'vault.import.title': 'Добавить данные в хранилище',
|
||||
'vault.import.desc':
|
||||
'Перенесите свои подключения из популярных клиентов. Выберите формат файла, чтобы начать миграцию.',
|
||||
'vault.import.chooseFormat': 'Выберите формат файла',
|
||||
'vault.import.csv.tip': 'Массовый импорт: используйте шаблон CSV.',
|
||||
'vault.import.csv.downloadTemplate': 'Скачать шаблон CSV',
|
||||
'vault.import.toast.start': 'Импорт из {format}...',
|
||||
'vault.import.toast.completedTitle': 'Импорт завершён',
|
||||
'vault.import.toast.failedTitle': 'Ошибка импорта',
|
||||
'vault.import.toast.noEntries': 'В {format} не найдено импортируемых записей.',
|
||||
'vault.import.toast.noNewHosts': 'Из {format} не импортировано новых хостов.',
|
||||
'vault.import.toast.summary':
|
||||
'Импортировано {count} хостов (пропущено {skipped}, дубликатов {duplicates}).',
|
||||
'vault.import.toast.firstIssue': 'Первая проблема: {issue}',
|
||||
'vault.import.sshConfig.chooseMode': 'Выберите, как импортировать ваш файл SSH-конфига.',
|
||||
'vault.import.sshConfig.modeQuestion': 'Как вы хотите выполнить импорт?',
|
||||
'vault.import.sshConfig.importOnly': 'Только импорт',
|
||||
'vault.import.sshConfig.importOnlyDesc': 'Одноразовый импорт. Изменения не будут синхронизироваться обратно в файл.',
|
||||
'vault.import.sshConfig.managed': 'Управляемая синхронизация',
|
||||
'vault.import.sshConfig.managedDesc': 'Поддерживать синхронизацию. Изменения будут сохраняться обратно в файл.',
|
||||
'vault.import.sshConfig.managedGroup': 'ssh config',
|
||||
'vault.import.sshConfig.managedSuccess': 'Импортировано {count} хостов. Файл теперь находится под управлением.',
|
||||
'vault.import.sshConfig.alreadyManaged': 'Этот файл уже находится под управлением.',
|
||||
'vault.import.sshConfig.alreadyManagedDesc': 'Этот файл уже управляется в группе "{group}". Если хотите импортировать его заново, сначала удалите существующий управляемый источник.',
|
||||
'vault.import.sshConfig.noFilePath': 'Невозможно управлять этим файлом.',
|
||||
'vault.import.sshConfig.noFilePathDesc': 'Не удалось определить путь к файлу. Для управляемой синхронизации нужен доступ к файловой системе.',
|
||||
|
||||
// Known Hosts
|
||||
'knownHosts.search.placeholder': 'Поиск известных хостов...',
|
||||
'knownHosts.action.scanSystem': 'Сканировать систему',
|
||||
'knownHosts.action.importFile': 'Импортировать файл',
|
||||
'knownHosts.action.browseFile': 'Выбрать файл',
|
||||
'knownHosts.empty.title': 'Нет известных хостов',
|
||||
'knownHosts.empty.desc':
|
||||
'Известные хосты — это SSH-серверы, к которым вы подключались раньше. Импортируйте системный файл known_hosts, чтобы начать.',
|
||||
'knownHosts.results.showingLimited':
|
||||
'Показано {shown} из {total} хостов. Используйте поиск, чтобы найти нужные хосты.',
|
||||
'knownHosts.toast.scanUnavailable': 'Сканирование системы недоступно на этой платформе.',
|
||||
'knownHosts.toast.scanNoFile': 'Системный файл known_hosts не найден.',
|
||||
'knownHosts.toast.scanNoEntries': 'В known_hosts не найдено пригодных записей.',
|
||||
'knownHosts.toast.scanImported': 'Импортировано новых хостов: {count}.',
|
||||
'knownHosts.toast.scanNoNew': 'Новых хостов не найдено.',
|
||||
'knownHosts.toast.scanFailed': 'Не удалось просканировать системный known_hosts.',
|
||||
|
||||
// Port Forwarding
|
||||
'pf.empty.title': 'Настройте проброс портов',
|
||||
'pf.empty.desc': 'Сохраняйте правила проброса портов для доступа к базам данных, веб-приложениям и другим сервисам.',
|
||||
'pf.title': 'Проброс портов',
|
||||
'pf.rulesCount': 'Правил: {count}',
|
||||
'pf.wizard.editTitle': 'Редактировать проброс портов',
|
||||
'pf.wizard.newTitle': 'Новый проброс портов',
|
||||
'pf.wizard.saveChanges': 'Сохранить изменения',
|
||||
'pf.wizard.done': 'Готово',
|
||||
'pf.wizard.continue': 'Продолжить',
|
||||
'pf.wizard.cancel': 'Отмена',
|
||||
'pf.wizard.skipWizard': 'Пропустить мастер',
|
||||
'pf.error.hostNotFound': 'Хост не найден',
|
||||
'pf.toast.titleWithLabel': 'Проброс портов: {label}',
|
||||
'pf.type.local': 'Локальный',
|
||||
'pf.type.remote': 'Удалённый',
|
||||
'pf.type.dynamic': 'Динамический',
|
||||
'pf.type.menu.local': 'Локальный проброс',
|
||||
'pf.type.menu.remote': 'Удалённый проброс',
|
||||
'pf.type.menu.dynamic': 'Динамический проброс',
|
||||
'pf.type.local.desc': 'Локальный проброс позволяет обращаться к прослушиваемому порту удалённого сервера так, как будто он локальный.',
|
||||
'pf.type.remote.desc': 'Удалённый проброс открывает порт на удалённой машине и перенаправляет подключения на локальный (текущий) хост.',
|
||||
'pf.type.dynamic.desc': 'Динамический проброс портов превращает Netcatty в SOCKS-прокси-сервер.',
|
||||
'pf.wizard.type.title': 'Выберите тип проброса портов:',
|
||||
'pf.wizard.localConfig.title': 'Укажите локальный порт и адрес привязки:',
|
||||
'pf.wizard.localConfig.desc': 'Этот порт будет открыт на локальном (текущем) устройстве и будет принимать трафик.',
|
||||
'pf.wizard.localConfig.localPort': 'Номер локального порта *',
|
||||
'pf.wizard.bindAddress': 'Адрес привязки',
|
||||
'pf.wizard.remoteHost.title': 'Выберите удалённый хост:',
|
||||
'pf.wizard.remoteHost.desc': 'Выберите хост, на котором будет открыт порт. Трафик с этого порта будет перенаправляться на конечный хост.',
|
||||
'pf.wizard.remoteConfig.title': 'Укажите порт и адрес привязки:',
|
||||
'pf.wizard.remoteConfig.desc': 'Трафик будет перенаправляться с указанного порта и адреса интерфейса выбранного хоста.',
|
||||
'pf.wizard.remoteConfig.remotePort': 'Номер удалённого порта *',
|
||||
'pf.wizard.destination.title': 'Выберите конечный хост:',
|
||||
'pf.wizard.destination.desc.local': 'Введите удалённый адрес назначения, к которому вы хотите получить доступ через туннель.',
|
||||
'pf.wizard.destination.desc.remote': 'Адрес назначения и порт, на которые будет перенаправляться трафик.',
|
||||
'pf.wizard.destination.address': 'Адрес назначения *',
|
||||
'pf.wizard.destination.addressPlaceholder': 'например, 127.0.0.1 или 192.168.1.100',
|
||||
'pf.wizard.destination.port': 'Номер порта назначения *',
|
||||
'pf.wizard.sshServer.title': 'Выберите SSH-сервер:',
|
||||
'pf.wizard.sshServer.desc.dynamic': 'Выберите SSH-сервер, который будет работать как SOCKS-прокси.',
|
||||
'pf.wizard.sshServer.desc.default': 'Выберите SSH-сервер, который будет туннелировать ваш трафик к адресу назначения.',
|
||||
'pf.wizard.label.title': 'Выберите метку:',
|
||||
'pf.wizard.label.placeholder.dynamic': 'например, SOCKS Proxy',
|
||||
'pf.wizard.label.placeholder.default': 'например, MySQL Production',
|
||||
'pf.wizard.label.placeholder.remoteRule': 'например, Remote Rule',
|
||||
'pf.wizard.placeholders.portExample': 'например, {port}',
|
||||
'pf.action.newForwarding': 'Новое правило',
|
||||
'pf.form.labelPlaceholder': 'Метка правила',
|
||||
'pf.form.intermediateHost': 'Промежуточный хост *',
|
||||
'pf.form.createRule': 'Создать правило',
|
||||
'pf.form.openWizard': 'Открыть мастер',
|
||||
'pf.form.openWizardTitle': 'Открыть мастер проброса портов',
|
||||
'pf.view.grid': 'Сетка',
|
||||
'pf.view.list': 'Список',
|
||||
'pf.rule.summary.local': 'Локально {bindAddress}:{localPort} -> SSH-туннель -> удалённо {remoteHost}:{remotePort}',
|
||||
'pf.rule.summary.remote': 'Удалённо {bindAddress}:{localPort} -> SSH-туннель -> локально {remoteHost}:{remotePort}',
|
||||
'pf.rule.summary.dynamic': 'SOCKS на {bindAddress}:{localPort}',
|
||||
'pf.tooltip.relayHost': 'Промежуточный хост',
|
||||
'pf.tooltip.hostLabel': 'Хост',
|
||||
'pf.tooltip.hostAddress': 'Адрес',
|
||||
'pf.tooltip.noHost': 'Промежуточный хост не настроен',
|
||||
'pf.tooltip.localDesc': 'Локальный проброс портов: доступ к удалённым сервисам через SSH-туннель',
|
||||
'pf.tooltip.remoteDesc': 'Удалённый проброс портов: публикация локальных сервисов на удалённом хосте',
|
||||
'pf.tooltip.dynamicDesc': 'Динамический SOCKS-прокси: маршрутизация трафика через SSH-туннель',
|
||||
'pf.deleteActive.title': 'Удалить активное правило проброса портов?',
|
||||
'pf.deleteActive.desc': 'Правило проброса портов "{label}" сейчас активно. При удалении туннель будет сначала остановлен.',
|
||||
'pf.deleteActive.confirm': 'Остановить и удалить',
|
||||
'pf.form.autoStart': 'Автозапуск',
|
||||
'pf.form.autoStartDesc': 'Автоматически запускать это правило при запуске приложения',
|
||||
|
||||
// SFTP
|
||||
'sftp.newFolder': 'Новая папка',
|
||||
'sftp.newFile': 'Новый файл',
|
||||
'sftp.filter': 'Фильтр',
|
||||
'sftp.filter.placeholder': 'Фильтр по имени файла...',
|
||||
'sftp.bookmark.add': 'Добавить путь в закладки',
|
||||
'sftp.bookmark.remove': 'Удалить закладку',
|
||||
'sftp.bookmark.list': 'Закладки путей',
|
||||
'sftp.bookmark.addGlobal': '+Глобальная',
|
||||
'sftp.bookmark.addGlobalTooltip': 'Сохранить как глобальную закладку (общую для всех хостов)',
|
||||
'sftp.bookmark.empty': 'Пока нет закладок',
|
||||
'sftp.columns.name': 'Имя',
|
||||
'sftp.columns.modified': 'Изменён',
|
||||
'sftp.columns.size': 'Размер',
|
||||
'sftp.columns.kind': 'Тип',
|
||||
'sftp.columns.actions': 'Действия',
|
||||
'sftp.emptyDirectory': 'Пустой каталог',
|
||||
'sftp.nav.up': 'Наверх',
|
||||
'sftp.nav.home': 'Перейти в домашний каталог',
|
||||
'sftp.nav.refresh': 'Обновить',
|
||||
'sftp.upload': 'Загрузить',
|
||||
'sftp.uploadFiles': 'Загрузить файлы',
|
||||
'sftp.uploadFolder': 'Загрузить папку',
|
||||
'sftp.dragDropToUpload': 'Перетащите сюда файлы для загрузки',
|
||||
'sftp.retry': 'Повторить',
|
||||
'sftp.context.open': 'Открыть',
|
||||
'sftp.context.navigateTo': 'Перейти к',
|
||||
'sftp.context.moveTo': 'Переместить в...',
|
||||
'sftp.context.moveToParent': 'Переместить в родительский каталог',
|
||||
'sftp.moveTo.title': 'Переместить в каталог',
|
||||
'sftp.moveTo.placeholder': 'Введите путь к целевому каталогу',
|
||||
'sftp.moveTo.confirm': 'Переместить',
|
||||
'sftp.moveTo.pathNotFound': 'Каталог не найден или недоступен',
|
||||
'sftp.context.download': 'Скачать',
|
||||
'sftp.context.copyToOtherPane': 'Копировать в другую панель',
|
||||
'sftp.copyCurrentPath': 'Копировать текущий путь',
|
||||
'sftp.copyCurrentPath.success': 'Текущий путь скопирован',
|
||||
'sftp.copyCurrentPath.error': 'Не удалось скопировать текущий путь',
|
||||
'sftp.viewMode.label': 'Режим просмотра',
|
||||
'sftp.viewMode.list': 'Список',
|
||||
'sftp.viewMode.tree': 'Дерево',
|
||||
'sftp.viewMode.switchToList': 'Переключиться на список',
|
||||
'sftp.viewMode.switchToTree': 'Переключиться на дерево',
|
||||
'sftp.tree.loadError': 'Не удалось загрузить каталог',
|
||||
'sftp.tree.loading': 'Загрузка...',
|
||||
'sftp.kind.folder': 'Папка',
|
||||
'sftp.context.rename': 'Переименовать',
|
||||
'sftp.context.permissions': 'Права доступа',
|
||||
'sftp.context.delete': 'Удалить',
|
||||
'sftp.context.refresh': 'Обновить',
|
||||
'sftp.context.uploadFiles': 'Загрузить файл(ы)...',
|
||||
'sftp.context.uploadFilesHere': 'Загрузить файлы сюда...',
|
||||
'sftp.context.uploadFolder': 'Загрузить папку...',
|
||||
'sftp.context.uploadFolderHere': 'Загрузить папку сюда...',
|
||||
'sftp.context.downloadSelected': 'Скачать выбранное ({count})',
|
||||
'sftp.context.deleteSelected': 'Удалить выбранное ({count})',
|
||||
'sftp.dropFilesHere': 'Перетащите сюда файлы',
|
||||
'sftp.itemsCount': '{count} записей',
|
||||
'sftp.selectedCount': '{count} выбрано',
|
||||
'sftp.path.doubleClickToEdit': 'Дважды щёлкните, чтобы изменить путь',
|
||||
'sftp.showHiddenPaths': 'Скрытые пути',
|
||||
'sftp.task.waiting': 'Ожидание...',
|
||||
'sftp.transfer.preparing': 'подготовка...',
|
||||
'sftp.status.loading': 'Загрузка...',
|
||||
'sftp.status.uploading': 'Загрузка...',
|
||||
'sftp.status.ready': 'Готово',
|
||||
'sftp.transfers': 'Передачи',
|
||||
'sftp.transfers.active': '{count} активн(ый/ых)',
|
||||
'sftp.transfers.clearCompleted': 'Очистить завершённые',
|
||||
'sftp.transfers.calculatingTotal': 'Вычисление общего размера...',
|
||||
'sftp.transfers.filesCount': '{count} файл(ов)',
|
||||
'sftp.transfers.filesProgress': '{current}/{total} файл(ов)',
|
||||
'sftp.transfers.expandChildren': 'Показать файлы',
|
||||
'sftp.transfers.collapseChildren': 'Скрыть файлы',
|
||||
'sftp.transfers.expandChildList': 'Показать детали',
|
||||
'sftp.transfers.collapseChildList': 'Скрыть',
|
||||
'sftp.transfers.retryAction': 'Повторить',
|
||||
'sftp.transfers.dismissAction': 'Скрыть',
|
||||
'sftp.transfers.openTargetFolder': 'Открыть целевую папку',
|
||||
'sftp.transfers.openTargetFolderError': 'Не удалось открыть целевую папку',
|
||||
'sftp.transfers.copyTargetPath': 'Копировать целевой путь',
|
||||
'sftp.transfers.copyTargetPathSuccess': 'Целевой путь скопирован',
|
||||
'sftp.transfers.copyTargetPathError': 'Не удалось скопировать целевой путь',
|
||||
'sftp.transfers.resizeNameColumn': 'Изменить ширину столбца имени файла',
|
||||
'sftp.transfers.dragToResize': 'Перетащите для изменения размера',
|
||||
'sftp.goUp': 'Наверх',
|
||||
'sftp.goToTerminalCwd': 'Перейти в каталог терминала',
|
||||
'sftp.followTerminalCwd': 'Следовать за каталогом терминала',
|
||||
'sftp.followTerminalCwd.enable': 'Включить следование за каталогом терминала',
|
||||
'sftp.followTerminalCwd.disable': 'Отключить следование за каталогом терминала',
|
||||
'sftp.encoding.label': 'Кодировка имён файлов',
|
||||
'sftp.encoding.auto': 'Авто',
|
||||
'sftp.encoding.utf8': 'UTF-8',
|
||||
'sftp.encoding.gb18030': 'GB18030',
|
||||
'sftp.goHome': 'Перейти в домашний каталог',
|
||||
'sftp.folderName': 'Имя папки',
|
||||
'sftp.folderName.placeholder': 'Введите имя папки',
|
||||
'sftp.fileName': 'Имя файла',
|
||||
'sftp.fileName.placeholder': 'Введите имя файла',
|
||||
'sftp.prompt.newFolderName': 'Имя новой папки?',
|
||||
'sftp.rename.title': 'Переименовать',
|
||||
'sftp.rename.newName': 'Новое имя',
|
||||
'sftp.rename.placeholder': 'Введите новое имя',
|
||||
'sftp.confirm.deleteOne': 'Удалить "{name}"?',
|
||||
'sftp.deleteConfirm.single': 'Удалить "{name}"?',
|
||||
'sftp.deleteConfirm.title': 'Удалить {count} элемент(ов)?',
|
||||
'sftp.deleteConfirm.desc': 'Это действие нельзя отменить. Будет удалено следующее:',
|
||||
'sftp.deleteConfirm.descSingle': 'Это действие нельзя отменить.',
|
||||
'sftp.deleteConfirm.host': 'Хост',
|
||||
'sftp.deleteConfirm.path': 'Путь',
|
||||
'sftp.error.loadFailed': 'Не удалось загрузить каталог',
|
||||
'sftp.error.downloadFailed': 'Ошибка скачивания',
|
||||
'sftp.error.uploadFailed': 'Ошибка загрузки',
|
||||
'sftp.error.deleteFailed': 'Ошибка удаления',
|
||||
'sftp.error.createFolderFailed': 'Не удалось создать папку',
|
||||
'sftp.error.createFileFailed': 'Не удалось создать файл',
|
||||
'sftp.error.invalidFileName': 'Имя файла содержит недопустимые символы: {chars}',
|
||||
'sftp.error.reservedName': 'Это имя файла зарезервировано системой',
|
||||
'sftp.overwrite.title': 'Файл уже существует',
|
||||
'sftp.overwrite.desc': 'Файл с именем "{name}" уже существует. Хотите заменить его?',
|
||||
'sftp.overwrite.confirm': 'Заменить',
|
||||
'sftp.error.renameFailed': 'Не удалось переименовать',
|
||||
'sftp.picker.title': 'Выберите хост',
|
||||
'sftp.picker.desc': 'Выберите хост для панели {side}',
|
||||
'sftp.picker.searchPlaceholder': 'Поиск хостов...',
|
||||
'sftp.picker.local.title': 'Локальная файловая система',
|
||||
'sftp.picker.local.desc': 'Просмотр локальных файлов',
|
||||
'sftp.picker.local.badge': 'Локально',
|
||||
'sftp.picker.connected.section': 'Подключённые',
|
||||
'sftp.picker.connected.badge': 'Онлайн',
|
||||
'sftp.picker.connected.connecting': 'Подключение',
|
||||
'sftp.picker.noMatch': 'Подходящие хосты не найдены',
|
||||
'sftp.permissions.title': 'Изменить права доступа',
|
||||
'sftp.permissions.owner': 'Владелец',
|
||||
'sftp.permissions.group': 'Группа',
|
||||
'sftp.permissions.others': 'Остальные',
|
||||
'sftp.permissions.octal': 'Восьмеричный',
|
||||
'sftp.permissions.symbolic': 'Символьный',
|
||||
'sftp.permissions.success': 'Права доступа успешно обновлены',
|
||||
'sftp.permissions.failed': 'Не удалось обновить права доступа',
|
||||
'sftp.pane.local': 'Локально',
|
||||
'sftp.pane.remote': 'Удалённо',
|
||||
'sftp.pane.selectHost': 'Выберите хост',
|
||||
'sftp.pane.selectHostToStart': 'Выберите хост для начала',
|
||||
'sftp.pane.chooseFilesystem': 'Выберите локальную или удалённую файловую систему для просмотра',
|
||||
'sftp.tabs.addTab': 'Добавить новую вкладку',
|
||||
'sftp.tabs.closeTab': 'Закрыть вкладку',
|
||||
'sftp.tabs.newTab': 'Новая вкладка',
|
||||
'sftp.tabs.copyDefaultPath': 'Копировать вкладку (путь по умолчанию)',
|
||||
'sftp.tabs.copyCurrentPath': 'Копировать и перейти к текущему пути',
|
||||
'sftp.conflict.title': 'Конфликт файлов',
|
||||
'sftp.conflict.desc': 'В месте назначения уже существует файл с таким именем',
|
||||
'sftp.conflict.alreadyExistsSuffix': 'уже существует',
|
||||
'sftp.conflict.existingFile': 'Существующий файл',
|
||||
'sftp.conflict.newFile': 'Новый файл',
|
||||
'sftp.conflict.size': 'Размер:',
|
||||
'sftp.conflict.modified': 'Изменён:',
|
||||
'sftp.conflict.applyToAll': 'Применить это действие ко всем оставшимся конфликтам ({count})',
|
||||
'sftp.conflict.action.stop': 'Остановить',
|
||||
'sftp.conflict.action.skip': 'Пропустить',
|
||||
'sftp.conflict.action.keepBoth': 'Сохранить оба',
|
||||
'sftp.conflict.action.duplicate': 'Дублировать',
|
||||
'sftp.conflict.action.merge': 'Объединить',
|
||||
'sftp.conflict.action.replace': 'Заменить',
|
||||
|
||||
// SFTP Upload Phases
|
||||
'sftp.upload.phase.compressing': 'Сжатие',
|
||||
'sftp.upload.phase.uploading': 'Загрузка',
|
||||
'sftp.upload.phase.extracting': 'Распаковка',
|
||||
'sftp.upload.phase.compressed': 'Сжато',
|
||||
|
||||
// SFTP File Opener
|
||||
'sftp.context.copyPath': 'Копировать путь к файлу',
|
||||
'sftp.context.openWithDefault': 'Открыть в системном приложении',
|
||||
'sftp.context.openWith': 'Открыть с помощью...',
|
||||
'sftp.context.edit': 'Редактировать',
|
||||
'sftp.context.preview': 'Предпросмотр',
|
||||
'sftp.opener.title': 'Открыть с помощью',
|
||||
'sftp.opener.desc': 'Выберите приложение для открытия этого файла',
|
||||
'sftp.opener.builtInEditor': 'Встроенный редактор',
|
||||
'sftp.opener.editDescription': 'Редактировать текстовые файлы',
|
||||
'sftp.opener.builtInImageViewer': 'Встроенный просмотрщик изображений',
|
||||
'sftp.opener.previewDescription': 'Просмотр изображений',
|
||||
'sftp.opener.systemApp': 'Выбрать приложение...',
|
||||
'sftp.opener.systemAppDescription': 'Выберите приложение на вашем компьютере',
|
||||
'sftp.opener.onlySystemApp': 'Этот файл можно открыть только во внешнем приложении',
|
||||
'sftp.opener.noAppsAvailable': 'Нет доступных приложений',
|
||||
'sftp.opener.noExtension': 'файлы без расширения',
|
||||
'sftp.opener.setDefault': 'Всегда использовать это для файлов {ext}',
|
||||
'sftp.opener.confirmTitle': 'Установить по умолчанию?',
|
||||
'sftp.opener.confirmDescription': 'Хотите всегда использовать {app} для файлов {ext}?',
|
||||
'sftp.opener.yesRemember': 'Да, запомнить выбор',
|
||||
'sftp.opener.justOnce': 'Только один раз',
|
||||
'sftp.opener.confirm.title': 'Установить приложение по умолчанию',
|
||||
'sftp.opener.confirm.desc': 'Хотите всегда открывать файлы .{ext} этим приложением?',
|
||||
'sftp.editor.title': 'Текстовый редактор',
|
||||
'sftp.editor.save': 'Сохранить на удалённый сервер',
|
||||
'sftp.editor.saving': 'Сохранение...',
|
||||
'sftp.editor.saved': 'Успешно сохранено',
|
||||
'sftp.editor.saveFailed': 'Не удалось сохранить файл',
|
||||
'sftp.editor.unsavedChanges': 'У вас есть несохранённые изменения. Всё равно закрыть?',
|
||||
'sftp.editor.syntaxHighlight': 'Подсветка синтаксиса',
|
||||
'sftp.preview.title': 'Просмотр изображения',
|
||||
'sftp.preview.zoomIn': 'Увеличить',
|
||||
'sftp.preview.zoomOut': 'Уменьшить',
|
||||
'sftp.preview.resetZoom': 'Сбросить масштаб',
|
||||
'sftp.preview.fitToWindow': 'Подогнать по окну',
|
||||
|
||||
// Settings > SFTP File Associations
|
||||
'settings.tab.sftpFileAssociations': 'SFTP',
|
||||
'settings.sftp.transferConcurrency': 'Параллелизм передачи',
|
||||
'settings.sftp.transferConcurrency.desc': 'Количество файлов, передаваемых параллельно при загрузке или скачивании папок. Более высокие значения могут ускорить работу, но способны перегрузить некоторые серверы.',
|
||||
'settings.sftp.defaultOpener': 'Приложение для открытия по умолчанию',
|
||||
'settings.sftp.defaultOpener.desc': 'Выберите приложение по умолчанию для открытия файлов без конкретной ассоциации',
|
||||
'settings.sftp.defaultOpener.ask': 'Всегда спрашивать',
|
||||
'settings.sftp.defaultOpener.askDesc': 'Каждый раз показывать диалог выбора приложения',
|
||||
'settings.sftp.defaultOpener.builtInDesc': 'По умолчанию открывать текстовые файлы во встроенном редакторе',
|
||||
'settings.sftp.defaultOpener.systemApp': 'Выбрать приложение...',
|
||||
'settings.sftp.defaultOpener.systemAppDesc': 'По умолчанию открывать файлы в конкретном приложении',
|
||||
'settings.sftpFileAssociations.title': 'Ассоциации файлов SFTP',
|
||||
'settings.sftpFileAssociations.desc': 'Настройка приложений по умолчанию для открытия файлов по расширению',
|
||||
'settings.sftpFileAssociations.extension': 'Расширение',
|
||||
'settings.sftpFileAssociations.application': 'Приложение',
|
||||
'settings.sftpFileAssociations.noAssociations': 'Ассоциации файлов не настроены',
|
||||
'settings.sftpFileAssociations.remove': 'Удалить',
|
||||
'settings.sftpFileAssociations.removeConfirm': 'Удалить ассоциацию для .{ext}?',
|
||||
|
||||
// Settings > SFTP Behavior
|
||||
'settings.sftp.doubleClickBehavior': 'Поведение двойного щелчка',
|
||||
'settings.sftp.doubleClickBehavior.desc': 'Выберите действие при двойном щелчке по файлу в SFTP-режиме',
|
||||
'settings.sftp.doubleClickBehavior.open': 'Открыть файл',
|
||||
'settings.sftp.doubleClickBehavior.transfer': 'Передать в другую панель',
|
||||
'settings.sftp.doubleClickBehavior.openDesc': 'Открыть файл в приложении по умолчанию',
|
||||
'settings.sftp.doubleClickBehavior.transferDesc': 'Передать файл на активный хост другой панели',
|
||||
'settings.sshDeepLink.title': 'SSH- и Telnet-ссылки',
|
||||
'settings.sshDeepLink.enable': 'Открывать ssh:// и telnet:// ссылки в Netcatty',
|
||||
'settings.sshDeepLink.enableDesc': 'Разрешить Netcatty обрабатывать ssh:// и telnet:// ссылки из браузеров и других приложений.',
|
||||
'settings.jmsDeepLink.title': 'Ссылки JumpServer',
|
||||
'settings.jmsDeepLink.enable': 'Обрабатывать ссылки jms://',
|
||||
'settings.jmsDeepLink.enableDesc': 'Зарегистрировать Netcatty как обработчик клиентских ссылок JumpServer jms://. Может конфликтовать с официальным клиентом JumpServer.',
|
||||
|
||||
// Settings > SFTP Auto Sync
|
||||
'settings.sftp.autoSync': 'Автосинхронизация с удалённым сервером',
|
||||
'settings.sftp.autoSync.desc': 'Автоматически синхронизировать изменения файлов обратно на удалённый сервер при открытии файлов во внешних приложениях',
|
||||
'settings.sftp.autoSync.enable': 'Включить автосинхронизацию',
|
||||
'settings.sftp.autoSync.enableDesc': 'Когда вы сохраняете файл во внешнем приложении, изменения автоматически загружаются на удалённый сервер',
|
||||
|
||||
// Settings > SFTP Auto Open Sidebar
|
||||
'settings.sftp.autoOpenSidebar': 'Автооткрытие боковой панели при подключении',
|
||||
'settings.sftp.autoOpenSidebar.desc': 'Автоматически открывать боковую панель файлового браузера SFTP при подключении к хосту',
|
||||
'settings.sftp.autoOpenSidebar.enable': 'Включить автооткрытие боковой панели',
|
||||
'settings.sftp.autoOpenSidebar.enableDesc': 'Боковая панель SFTP будет автоматически открываться при подключении терминальной сессии к удалённому хосту',
|
||||
'settings.sftp.followTerminalCwd': 'Следовать за каталогом терминала',
|
||||
'settings.sftp.followTerminalCwd.desc': 'Автоматически синхронизировать боковую панель SFTP с рабочим каталогом терминала (переключатель на панели инструментов)',
|
||||
'settings.sftp.followTerminalCwd.enable': 'Включать следование по умолчанию',
|
||||
'settings.sftp.followTerminalCwd.enableDesc': 'При открытой боковой панели SFTP режим следования включён по умолчанию и обновляется после команд cd в терминале',
|
||||
|
||||
'settings.sftp.defaultViewMode': 'Режим просмотра по умолчанию',
|
||||
'settings.sftp.defaultViewMode.desc': 'Выберите режим просмотра по умолчанию при открытии новой вкладки SFTP. Настройки конкретного хоста имеют приоритет.',
|
||||
'settings.sftp.defaultViewMode.list': 'Список',
|
||||
'settings.sftp.defaultViewMode.listDesc': 'Показывать файлы в виде плоского списка для текущего каталога',
|
||||
'settings.sftp.defaultViewMode.tree': 'Дерево',
|
||||
'settings.sftp.defaultViewMode.treeDesc': 'Показывать файлы в иерархической древовидной структуре',
|
||||
|
||||
'sftp.autoSync.success': 'Файл синхронизирован с удалённым сервером: {fileName}',
|
||||
'sftp.autoSync.error': 'Не удалось синхронизировать файл: {error}',
|
||||
|
||||
// SFTP Folder Upload Progress
|
||||
'sftp.upload.progress': 'Загрузка файлов {current} из {total}...',
|
||||
'sftp.upload.uploading': 'Загрузка...',
|
||||
'sftp.upload.compressing': 'Сжатие...',
|
||||
'sftp.upload.extracting': 'Распаковка...',
|
||||
'sftp.upload.scanning': 'Сканирование файлов...',
|
||||
'sftp.upload.completed': 'Завершено',
|
||||
'sftp.upload.compressed': 'Сжатая передача',
|
||||
'sftp.upload.currentFile': 'Текущий: {fileName}',
|
||||
'sftp.upload.cancelled': 'Загрузка отменена',
|
||||
'sftp.upload.cancel': 'Отмена',
|
||||
'sftp.upload.completedToPath': 'Загружено в {path}',
|
||||
|
||||
// SFTP Download
|
||||
'sftp.download.completed': 'Скачано',
|
||||
'sftp.download.cancelled': 'Скачивание отменено',
|
||||
|
||||
// SFTP Reconnecting
|
||||
'sftp.reconnecting.title': 'Переподключение...',
|
||||
'sftp.reconnecting.desc': 'Соединение потеряно, выполняется попытка переподключения',
|
||||
'sftp.reconnected': 'Соединение восстановлено',
|
||||
'sftp.error.reconnectFailed': 'Не удалось переподключиться. Попробуйте ещё раз.',
|
||||
'sftp.error.connectionLostManual': 'Соединение потеряно. Пожалуйста, переподключитесь вручную.',
|
||||
'sftp.error.connectionLostReconnecting': 'Соединение потеряно. Переподключение...',
|
||||
'sftp.error.sessionLost': 'SFTP-сессия потеряна. Пожалуйста, переподключитесь.',
|
||||
|
||||
// Settings > SFTP Show Hidden Files
|
||||
'settings.sftp.showHiddenFiles': 'Показывать скрытые файлы',
|
||||
'settings.sftp.showHiddenFiles.desc': 'Показывать скрытые файлы (dotfiles в Unix/macOS и файлы с атрибутом hidden в Windows) в файловом браузере SFTP.',
|
||||
'settings.sftp.showHiddenFiles.enable': 'Показывать скрытые файлы',
|
||||
'settings.sftp.showHiddenFiles.enableDesc': 'Показывать скрытые файлы при просмотре как локальной, так и удалённой файловой системы',
|
||||
|
||||
// Settings > SFTP Compressed Upload
|
||||
'settings.sftp.compressedUpload': 'Передача со сжатием папок',
|
||||
'settings.sftp.compressedUpload.desc': 'Сжимать папки перед загрузкой, чтобы значительно сократить время передачи.',
|
||||
'settings.sftp.compressedUpload.enable': 'Включить сжатие папок',
|
||||
'settings.sftp.compressedUpload.enableDesc': 'Автоматически сжимать папки с помощью tar перед передачей. Требует поддержки tar на сервере. Если она недоступна, будет использована обычная передача.',
|
||||
|
||||
// Quick Switcher
|
||||
'qs.search.placeholder': 'Поиск хостов или вкладок',
|
||||
'qs.jumpTo': 'Перейти к',
|
||||
'qs.localTerminal': 'Локальный терминал',
|
||||
'qs.localShells': 'Локальные оболочки',
|
||||
'qs.default': 'По умолчанию',
|
||||
|
||||
// Select Host panel
|
||||
'selectHost.title': 'Выберите хост',
|
||||
'selectHost.noHostsFound': 'Хосты не найдены',
|
||||
'selectHost.newHost': 'Новый хост',
|
||||
'selectHost.continue': 'Продолжить',
|
||||
'selectHost.continueWithCount': 'Продолжить (выбрано: {count})',
|
||||
|
||||
// Quick Connect
|
||||
'quickConnect.knownHost.title': 'Вы уверены, что хотите подключиться?',
|
||||
'quickConnect.knownHost.authenticity': 'Подлинность {hostname} не может быть установлена.',
|
||||
'quickConnect.knownHost.fingerprintLabel': '{keyType} fingerprint is SHA256:',
|
||||
'quickConnect.knownHost.addQuestion': 'Хотите добавить его в список известных хостов?',
|
||||
'quickConnect.knownHost.addAndContinue': 'Добавить и продолжить',
|
||||
'quickConnect.addKey': 'Добавить ключ',
|
||||
'quickConnect.warning.unparsedOptions': 'Некоторые аргументы SSH были проигнорированы: {options}',
|
||||
|
||||
// Terminal
|
||||
'terminal.connectionErrorTitle': 'Ошибка подключения',
|
||||
|
||||
// Protocol select dialog
|
||||
'protocolSelect.chooseProtocol': 'Выберите протокол',
|
||||
'protocolSelect.port': 'порт:',
|
||||
|
||||
// Host Details
|
||||
'hostDetails.title.details': 'Сведения о хосте',
|
||||
'hostDetails.title.new': 'Новый хост',
|
||||
'vault.panel.resizeWidth': 'Изменить ширину панели',
|
||||
'hostDetails.saveAria': 'Сохранить',
|
||||
'hostDetails.section.address': 'Адрес',
|
||||
'hostDetails.hostname.placeholder': 'IP или имя хоста',
|
||||
'hostDetails.section.general': 'Общие',
|
||||
'hostDetails.section.sftp': 'Настройки SFTP',
|
||||
'hostDetails.sftp.sudo': 'Режим sudo',
|
||||
'hostDetails.sftp.sudo.desc': 'Автоматически получать привилегии Root с помощью сохранённого пароля',
|
||||
'hostDetails.sftp.sudo.passwordWarning': 'Для режима sudo требуется пароль. Укажите его выше или убедитесь, что сервер разрешает sudo без пароля.',
|
||||
'hostDetails.sftp.encoding': 'Кодировка имён файлов',
|
||||
'hostDetails.sftp.encoding.desc': 'Выберите кодировку, используемую для декодирования и отправки имён файлов SFTP.',
|
||||
'hostDetails.label.placeholder': 'Метка (например, Production Server)',
|
||||
'hostDetails.notes.label': 'Заметки',
|
||||
'hostDetails.notes.placeholder': 'Оборудование, проект, клиент, регион, роль...',
|
||||
'hostDetails.notes.help': 'Поддерживается Markdown. Не храните здесь пароли и закрытые ключи.',
|
||||
'hostDetails.notes.tab.edit': 'Редактировать',
|
||||
'hostDetails.notes.tab.preview': 'Просмотр',
|
||||
'hostDetails.notes.preview.empty': 'Пока нечего просматривать.',
|
||||
'hostDetails.notes.toggle.show': 'Показать редактор заметок',
|
||||
'hostDetails.notes.toggle.hide': 'Скрыть редактор заметок',
|
||||
'hostDetails.group.placeholder': 'Родительская группа',
|
||||
'hostDetails.section.credentials': 'Учётные данные',
|
||||
'hostDetails.section.portCredentials': 'Порт и учётные данные',
|
||||
'hostDetails.section.appearance': 'Внешний вид',
|
||||
'hostDetails.distro.title': 'Дистрибутив Linux',
|
||||
'hostDetails.distro.desc': 'Автоопределение при подключении или ручная замена значка дистрибутива.',
|
||||
'hostDetails.icon.title': 'Значок хоста',
|
||||
'hostDetails.icon.sectionTitle': 'Цвет и значок',
|
||||
'hostDetails.icon.desc': 'Выберите источник значка и цвет для этого хоста.',
|
||||
'hostDetails.icon.source': 'Источник',
|
||||
'hostDetails.icon.manualLabel': 'Значок вручную',
|
||||
'hostDetails.icon.tab.brand': 'Бренд',
|
||||
'hostDetails.icon.tab.type': 'Тип',
|
||||
'hostDetails.icon.mode.auto': 'Авто',
|
||||
'hostDetails.icon.mode.custom': 'Свой',
|
||||
'hostDetails.icon.reset': 'Сбросить значок',
|
||||
'hostDetails.icon.colorLabel': 'Цвет значка',
|
||||
'hostDetails.icon.colorDefault': 'По умолчанию',
|
||||
'hostDetails.icon.colorAuto': 'Авто',
|
||||
'hostDetails.icon.colorManual': 'Вручную',
|
||||
'hostDetails.icon.colorChoice': 'Цвет',
|
||||
'hostDetails.icon.autoColorLabel': 'Текущий цвет',
|
||||
'hostDetails.icon.preview': 'Предпросмотр значка',
|
||||
'hostDetails.icon.showLibrary': 'Показать библиотеку значков',
|
||||
'hostDetails.icon.hideLibrary': 'Скрыть библиотеку значков',
|
||||
'hostDetails.icon.autoUsesDistro': 'Использует значок дистрибутива Linux и выбранный цвет для этого хоста.',
|
||||
'hostDetails.icon.customOverridesDistro': 'Встроенный значок заменяет значок дистрибутива Linux для этого хоста.',
|
||||
'hostDetails.icon.option.server': 'Сервер',
|
||||
'hostDetails.icon.option.terminal': 'Терминал',
|
||||
'hostDetails.icon.option.database': 'База данных',
|
||||
'hostDetails.icon.option.cloud': 'Облако',
|
||||
'hostDetails.icon.option.router': 'Маршрутизатор',
|
||||
'hostDetails.icon.option.shield': 'Защита',
|
||||
'hostDetails.icon.option.code': 'Код',
|
||||
'hostDetails.icon.option.box': 'Узел',
|
||||
'hostDetails.icon.option.globe': 'Глобус',
|
||||
'hostDetails.icon.option.cpu': 'CPU',
|
||||
'hostDetails.icon.option.hard-drive': 'Хранилище',
|
||||
'hostDetails.icon.option.network': 'Сеть',
|
||||
'hostDetails.icon.option.wifi': 'Wi-Fi',
|
||||
'hostDetails.icon.option.lock': 'Замок',
|
||||
'hostDetails.icon.option.key': 'Ключ',
|
||||
'hostDetails.icon.option.monitor': 'Монитор',
|
||||
'hostDetails.icon.option.container': 'Контейнер',
|
||||
'hostDetails.icon.option.activity': 'Активность',
|
||||
'hostDetails.icon.option.zap': 'Быстрый',
|
||||
'hostDetails.icon.option.server-cog': 'Настройки сервера',
|
||||
'hostDetails.icon.color.blue': 'Синий',
|
||||
'hostDetails.icon.color.green': 'Зеленый',
|
||||
'hostDetails.icon.color.red': 'Красный',
|
||||
'hostDetails.icon.color.amber': 'Янтарный',
|
||||
'hostDetails.icon.color.purple': 'Фиолетовый',
|
||||
'hostDetails.icon.color.cyan': 'Голубой',
|
||||
'hostDetails.icon.color.orange': 'Оранжевый',
|
||||
'hostDetails.icon.color.slate': 'Серый',
|
||||
'hostDetails.icon.color.violet': 'Фиолетово-синий',
|
||||
'hostDetails.icon.color.pink': 'Розовый',
|
||||
'hostDetails.icon.color.rose': 'Розово-красный',
|
||||
'hostDetails.icon.color.lime': 'Лаймовый',
|
||||
'hostDetails.icon.color.teal': 'Бирюзовый',
|
||||
'hostDetails.icon.color.sky': 'Небесный',
|
||||
'hostDetails.icon.color.indigo': 'Индиго',
|
||||
'hostDetails.icon.color.zinc': 'Цинковый',
|
||||
'hostDetails.icon.color.custom': 'Свой цвет',
|
||||
'hostDetails.distro.mode': 'Источник',
|
||||
'hostDetails.distro.mode.auto': 'Автоопределение',
|
||||
'hostDetails.distro.mode.manual': 'Ручное переопределение',
|
||||
'hostDetails.distro.detectedLabel': 'Текущий',
|
||||
'hostDetails.distro.manualLabel': 'Переопределить',
|
||||
'hostDetails.distro.pending': 'Определится после первого подключения',
|
||||
'hostDetails.distro.unknown': 'Неизвестно',
|
||||
'hostDetails.distro.option.linux': 'Обычный Linux',
|
||||
'hostDetails.distro.option.ubuntu': 'Ubuntu',
|
||||
'hostDetails.distro.option.debian': 'Debian',
|
||||
'hostDetails.distro.option.centos': 'CentOS',
|
||||
'hostDetails.distro.option.rocky': 'Rocky Linux',
|
||||
'hostDetails.distro.option.fedora': 'Fedora',
|
||||
'hostDetails.distro.option.arch': 'Arch Linux',
|
||||
'hostDetails.distro.option.alpine': 'Alpine',
|
||||
'hostDetails.distro.option.amazon': 'Amazon Linux',
|
||||
'hostDetails.distro.option.opensuse': 'openSUSE / SLES',
|
||||
'hostDetails.distro.option.redhat': 'Red Hat / RHEL',
|
||||
'hostDetails.distro.option.almalinux': 'AlmaLinux',
|
||||
'hostDetails.distro.option.alinux': 'Alibaba Cloud Linux',
|
||||
'hostDetails.distro.option.openeuler': 'openEuler',
|
||||
'hostDetails.distro.option.oracle': 'Oracle Linux',
|
||||
'hostDetails.distro.option.kali': 'Kali Linux',
|
||||
'hostDetails.distro.option.cisco': 'Cisco',
|
||||
'hostDetails.distro.option.juniper': 'Juniper Networks',
|
||||
'hostDetails.distro.option.huawei': 'Huawei',
|
||||
'hostDetails.distro.option.h3c': 'H3C',
|
||||
'hostDetails.distro.option.hpe': 'HPE',
|
||||
'hostDetails.distro.option.mikrotik': 'MikroTik',
|
||||
'hostDetails.distro.option.fortinet': 'Fortinet',
|
||||
'hostDetails.distro.option.paloalto': 'Palo Alto Networks',
|
||||
'hostDetails.distro.option.zyxel': 'ZyXEL',
|
||||
'hostDetails.distro.option.ruijie': 'Ruijie',
|
||||
'hostDetails.section.mosh': 'Mosh',
|
||||
'hostDetails.username.placeholder': 'Имя пользователя',
|
||||
'hostDetails.password.placeholder': 'Пароль',
|
||||
'hostDetails.password.show': 'Показать пароль',
|
||||
'hostDetails.password.hide': 'Скрыть пароль',
|
||||
'hostDetails.password.save': 'Сохранить пароль',
|
||||
'hostDetails.identity.suggestions': 'Идентификаторы',
|
||||
'hostDetails.identity.missing': 'Идентификатор не найден',
|
||||
'hostDetails.credential.keyCertificate': 'Ключ, сертификат, локальный файл ключа',
|
||||
'hostDetails.credential.key': 'Ключ',
|
||||
'hostDetails.credential.certificate': 'Сертификат',
|
||||
'hostDetails.credential.localKeyFile': 'Локальный файл ключа',
|
||||
'hostDetails.credential.localKeyFilePlaceholder': '~/.ssh/id_ed25519',
|
||||
'hostDetails.credential.browseKeyFile': 'Обзор...',
|
||||
'hostDetails.credential.missing': 'Учётные данные не найдены',
|
||||
'hostDetails.keys.search': 'Поиск ключей...',
|
||||
'hostDetails.keys.empty': 'Нет доступных ключей',
|
||||
'hostDetails.certs.search': 'Поиск сертификатов...',
|
||||
'hostDetails.certs.empty': 'Нет доступных сертификатов',
|
||||
'hostDetails.agentForwarding': 'Проброс SSH Agent',
|
||||
'hostDetails.agentForwarding.desc': 'Разрешить удалённому серверу использовать ваши локальные SSH-ключи (например, для операций git)',
|
||||
'hostDetails.agentForwarding.agentNotRunning': 'SSH Agent недоступен',
|
||||
'hostDetails.agentForwarding.agentNotRunningHint': 'SSH Agent не обнаружен. Включите OpenSSH Authentication Agent в службах Windows или используйте совместимый агент, например Bitwarden, 1Password или gpg-agent.',
|
||||
'hostDetails.section.agentForwarding': 'SSH Agent',
|
||||
'hostDetails.x11Forwarding': 'Проброс X11-приложений',
|
||||
'hostDetails.x11Forwarding.desc': 'Показывать удалённые графические приложения на вашем локальном рабочем столе, если запущен локальный X-сервер.',
|
||||
'hostDetails.section.x11Forwarding': 'Проброс X11',
|
||||
'hostDetails.section.deviceType': 'Тип устройства',
|
||||
'hostDetails.deviceType': 'Режим сетевого устройства',
|
||||
'hostDetails.deviceType.desc': 'Включайте для сетевого оборудования (коммутаторов, маршрутизаторов, межсетевых экранов), подключённого по SSH. Команды отправляются как есть, без обёртки оболочки, что совместимо с CLI вендоров вроде Huawei VRP и Cisco IOS.',
|
||||
'hostDetails.deviceType.warning': 'Команды AI-агента будут отправляться напрямую без отслеживания кода выхода. Включайте только для устройств, на которых нет стандартной оболочки.',
|
||||
'hostDetails.section.sshAlgorithms': 'SSH-алгоритмы',
|
||||
'hostDetails.section.terminalBehavior': 'Поведение терминала',
|
||||
'hostDetails.lineTimestamps': 'Показывать время вывода',
|
||||
'hostDetails.lineTimestamps.desc': 'Показывать локальное время рядом с видимыми строками вывода для этого хоста, не изменяя текст терминала.',
|
||||
'hostDetails.legacyAlgorithms': 'Разрешить устаревшие алгоритмы',
|
||||
'hostDetails.legacyAlgorithms.desc': 'Включить устаревшие SSH-алгоритмы (diffie-hellman-group1, ssh-dss, 3des-cbc и т. д.) для подключения к старому сетевому оборудованию.',
|
||||
'hostDetails.legacyAlgorithms.warning': 'У этих алгоритмов есть известные слабые места безопасности. Включайте только для устаревших устройств, которые не поддерживают современную криптографию.',
|
||||
'hostDetails.skipEcdsaHostKey': 'Пропустить ECDSA host key',
|
||||
'hostDetails.skipEcdsaHostKey.desc': 'Некоторые старые коммутаторы Huawei / Cisco выдают нестандартные подписи ECDSA host-key, из-за чего соединение падает с "signature verification failed". Включение этой опции убирает все ecdsa-sha2-* из предложения клиента, и согласование переходит к RSA / Ed25519.',
|
||||
'hostDetails.algorithms.advanced': 'Дополнительные настройки алгоритмов',
|
||||
'hostDetails.algorithms.advanced.desc': 'Заменить предлагаемый список алгоритмов для любой категории для конкретного хоста. Не трогать категорию = использовать значение по умолчанию; выбранное подмножество полностью заменяет список по умолчанию. Неверные значения могут сделать хост недоступным.',
|
||||
'hostDetails.algorithms.inheritedNotice': 'В текущей группе заданы переопределения алгоритмов для: {categories}. Кнопка «Сбросить» здесь возвращает к спискам группы, а не к значениям NetCatty по умолчанию. Чтобы игнорировать ограничение группы, очистите переопределение в настройках алгоритмов группы.',
|
||||
'hostDetails.algorithms.customized': 'настроено',
|
||||
'hostDetails.algorithms.reset': 'Сбросить',
|
||||
'hostDetails.algorithms.category.kex': 'Обмен ключами (KEX)',
|
||||
'hostDetails.algorithms.category.cipher': 'Шифр',
|
||||
'hostDetails.algorithms.category.hmac': 'MAC (HMAC)',
|
||||
'hostDetails.algorithms.category.serverHostKey': 'Host Key',
|
||||
'hostDetails.algorithms.category.compress': 'Сжатие',
|
||||
'hostDetails.section.keepalive': 'Keepalive',
|
||||
'hostDetails.keepalive.override': 'Переопределить глобальный keepalive',
|
||||
'hostDetails.keepalive.desc': 'Использовать для этого хоста собственную политику keepalive вместо глобальной настройки. Полезно для старых маршрутизаторов и коммутаторов, чей SSH-сервер не отвечает на запросы keepalive@openssh.com. Установите интервал 0, чтобы полностью отключить keepalive для этого хоста.',
|
||||
'hostDetails.keepalive.interval': 'Интервал (секунды)',
|
||||
'hostDetails.keepalive.countMax': 'Макс. число пропущенных keepalive',
|
||||
'hostDetails.keepalive.disabledHint': 'Интервал = 0 отключает keepalive для этого хоста. Для определения разорванного соединения сессия будет полагаться на TCP-таймауты.',
|
||||
'hostDetails.backspaceBehavior': 'Поведение Backspace',
|
||||
'hostDetails.backspaceBehavior.default': 'По умолчанию',
|
||||
'hostDetails.jumpHosts': 'Прокси через хосты',
|
||||
'hostDetails.jumpHosts.hops': '{count} hop(s)',
|
||||
'hostDetails.jumpHosts.direct': 'Напрямую',
|
||||
'hostDetails.jumpHosts.configure': 'Настроить прокси-хосты',
|
||||
'hostDetails.proxy': 'Прокси через HTTP/SOCKS5/Command',
|
||||
'hostDetails.proxy.none': 'Нет',
|
||||
'hostDetails.proxy.edit': 'Редактировать прокси',
|
||||
'hostDetails.proxy.configure': 'Настроить прокси',
|
||||
'hostDetails.proxyPanel.title': 'Прокси через HTTP/SOCKS5/Command',
|
||||
'hostDetails.proxyPanel.hostPlaceholder': 'Прокси-хост',
|
||||
'hostDetails.proxyPanel.command': 'ProxyCommand',
|
||||
'hostDetails.proxyPanel.commandPlaceholder': 'cloudflared access ssh --hostname %h',
|
||||
'hostDetails.proxyPanel.commandHelp': 'Используйте %h для целевого хоста, %p для целевого порта и %% для символа процента.',
|
||||
'hostDetails.proxyPanel.credentials': 'Учётные данные',
|
||||
'hostDetails.proxyPanel.usernamePlaceholder': 'Имя пользователя',
|
||||
'hostDetails.proxyPanel.passwordPlaceholder': 'Пароль',
|
||||
'hostDetails.proxyPanel.identities': 'Идентификаторы',
|
||||
'hostDetails.proxyPanel.keychainIdentity': 'Идентификатор из связки ключей',
|
||||
'hostDetails.proxyPanel.manualCredentials': 'Ввести вручную',
|
||||
'hostDetails.proxyPanel.remove': 'Удалить прокси',
|
||||
'hostDetails.proxyPanel.savedProxy': 'Сохранённый прокси',
|
||||
'hostDetails.proxyPanel.selectSaved': 'Выбрать сохранённый прокси',
|
||||
'hostDetails.proxyPanel.customProxy': 'Пользовательский прокси',
|
||||
'hostDetails.proxyPanel.missing': 'Отсутствует',
|
||||
'hostDetails.proxyPanel.missingSaved': 'Сохранённый прокси отсутствует',
|
||||
'hostDetails.proxyPanel.missingIdentity': 'Идентификатор из связки ключей отсутствует',
|
||||
'hostDetails.proxyPanel.incompleteIdentity': 'Для идентификатора прокси нужны имя пользователя и пароль',
|
||||
'hostDetails.proxyPanel.unreadableIdentity': 'Пароль идентификатора прокси нельзя прочитать на этом устройстве',
|
||||
'hostDetails.proxyPanel.error.required': 'Требуются хост и порт прокси или ProxyCommand.',
|
||||
'hostDetails.envVars': 'Переменные окружения',
|
||||
'hostDetails.envVars.add': 'Добавить переменную окружения',
|
||||
'hostDetails.envVars.title': 'Переменные окружения',
|
||||
'hostDetails.envVars.desc': 'Задайте переменную окружения для {host}.',
|
||||
'hostDetails.envVars.note': 'Некоторые SSH-серверы по умолчанию разрешают только переменные с префиксом LC_ и LANG_.',
|
||||
'hostDetails.envVars.variable': 'Переменная',
|
||||
'hostDetails.envVars.value': 'Значение',
|
||||
'hostDetails.envVars.newVariable': 'Новая переменная',
|
||||
'hostDetails.envVars.variableName': 'Имя переменной',
|
||||
'hostDetails.chain.title': 'Редактировать цепочку',
|
||||
'hostDetails.chain.desc': 'Добавление ещё одного хоста создаст подключение к {host}.',
|
||||
'hostDetails.chain.addHost': 'Добавить хост',
|
||||
'hostDetails.chain.target': 'Цель',
|
||||
'hostDetails.chain.availableHosts': 'Доступные хосты',
|
||||
'hostDetails.chain.clear': 'Очистить',
|
||||
'hostDetails.group.title': 'Новая группа',
|
||||
'hostDetails.group.general': 'Общие',
|
||||
'hostDetails.group.namePlaceholder': 'Имя группы',
|
||||
'hostDetails.group.parentPlaceholder': 'Родительская группа',
|
||||
'hostDetails.group.cloudSync': 'Облачная синхронизация',
|
||||
'hostDetails.group.addProtocol': 'Добавить протокол',
|
||||
'hostDetails.startupCommand': 'Команда запуска',
|
||||
'hostDetails.startupCommand.placeholder': 'Команда для запуска при подключении (например, cd /app && ls)',
|
||||
'hostDetails.startupCommand.help':
|
||||
'This command will be executed automatically after SSH connection is established.',
|
||||
'hostDetails.startupCommand.runMode': 'Многострочный запуск',
|
||||
'hostDetails.startupCommand.runMode.help': 'Выберите, как отправлять многострочные команды запуска после подключения.',
|
||||
'hostDetails.startupCommand.runMode.lineDelay': 'Отправлять построчно',
|
||||
'hostDetails.startupCommand.runMode.paste': 'Отправить сразу',
|
||||
'hostDetails.otherProtocols': 'Другие протоколы',
|
||||
'hostDetails.telnetOn': 'Telnet на',
|
||||
'hostDetails.port': 'порт',
|
||||
'hostDetails.telnet.credentials': 'Учётные данные',
|
||||
'hostDetails.telnet.identity.placeholder': 'Ручной идентификатор',
|
||||
'hostDetails.telnet.username': 'Имя пользователя Telnet',
|
||||
'hostDetails.telnet.password': 'Пароль Telnet',
|
||||
'hostDetails.charset.placeholder': 'Кодировка (например, UTF-8)',
|
||||
'hostDetails.telnet.add': 'Добавить протокол Telnet',
|
||||
'hostDetails.telnet.setDefault': 'Подключаться по Telnet по умолчанию',
|
||||
'hostDetails.tags': 'Теги',
|
||||
'hostDetails.group': 'Группа',
|
||||
'hostDetails.selectGroup': 'Выберите группу',
|
||||
'hostDetails.addTag': 'Добавить тег...',
|
||||
'hostDetails.createTag': 'Создать тег',
|
||||
'hostDetails.createGroup': 'Создать группу',
|
||||
|
||||
// Host form (legacy modal)
|
||||
'hostForm.title.edit': 'Редактировать хост',
|
||||
'hostForm.title.new': 'Новый хост',
|
||||
'hostForm.desc.edit': 'Обновите параметры подключения для этого хоста',
|
||||
'hostForm.desc.new': 'Создайте новую запись SSH-хоста',
|
||||
'hostForm.field.label': 'Метка',
|
||||
'hostForm.placeholder.label': 'Мой production-сервер',
|
||||
'hostForm.field.hostname': 'Имя хоста / IP',
|
||||
'hostForm.placeholder.hostname': '192.168.1.1',
|
||||
'hostForm.field.port': 'Порт',
|
||||
'hostForm.field.username': 'Имя пользователя',
|
||||
'hostForm.field.osType': 'Тип ОС',
|
||||
'hostForm.placeholder.selectOs': 'Выберите ОС',
|
||||
'hostForm.field.group': 'Группа',
|
||||
'hostForm.placeholder.group': 'например, AWS, DigitalOcean',
|
||||
'hostForm.field.tags': 'Теги',
|
||||
'hostForm.placeholder.addTag': 'Добавить тег...',
|
||||
'hostForm.auth.method': 'Метод аутентификации',
|
||||
'hostForm.auth.password': 'Пароль',
|
||||
'hostForm.auth.sshKey': 'SSH-ключ',
|
||||
'hostForm.auth.selectKey': 'Выберите SSH-ключ',
|
||||
'hostForm.auth.noKeys': 'Нет доступных ключей',
|
||||
'hostForm.auth.noKeysHint': 'В связке ключей не найдено SSH-ключей. Сначала создайте один.',
|
||||
'hostForm.saveHost': 'Сохранить хост',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { DEFAULT_KEY_BINDINGS } from "../../../domain/models/keyBindings.ts";
|
||||
import { HOST_ICON_COLORS, HOST_ICON_IDS } from "../../../domain/hostIcon.ts";
|
||||
import zhCN from "./zh-CN.ts";
|
||||
import ru from "./ru.ts";
|
||||
|
||||
const LOCALIZED_SETTINGS_LOCALES = [
|
||||
{ name: "zh-CN", messages: zhCN },
|
||||
{ name: "ru", messages: ru },
|
||||
];
|
||||
|
||||
test("localized settings include names for every default shortcut", () => {
|
||||
for (const locale of LOCALIZED_SETTINGS_LOCALES) {
|
||||
const missing = DEFAULT_KEY_BINDINGS
|
||||
.map((binding) => `settings.shortcuts.binding.${binding.id}`)
|
||||
.filter((key) => !locale.messages[key]);
|
||||
|
||||
assert.deepEqual(missing, [], `${locale.name} is missing shortcut labels`);
|
||||
}
|
||||
});
|
||||
|
||||
test("localized settings include workspace focus indicator labels", () => {
|
||||
const keys = [
|
||||
"settings.terminal.section.workspaceFocus",
|
||||
"settings.terminal.workspaceFocus.style",
|
||||
"settings.terminal.workspaceFocus.style.desc",
|
||||
"settings.terminal.workspaceFocus.dim",
|
||||
"settings.terminal.workspaceFocus.border",
|
||||
];
|
||||
|
||||
for (const locale of LOCALIZED_SETTINGS_LOCALES) {
|
||||
const missing = keys.filter((key) => !locale.messages[key]);
|
||||
assert.deepEqual(missing, [], `${locale.name} is missing workspace focus labels`);
|
||||
}
|
||||
});
|
||||
|
||||
test("localized settings include network proxy labels", () => {
|
||||
const keys = [
|
||||
"settings.system.networkProxy.title",
|
||||
"settings.system.networkProxy.description",
|
||||
"settings.system.networkProxy.mode",
|
||||
"settings.system.networkProxy.mode.system",
|
||||
"settings.system.networkProxy.mode.direct",
|
||||
"settings.system.networkProxy.mode.custom",
|
||||
"settings.system.networkProxy.url",
|
||||
"settings.system.networkProxy.url.placeholder",
|
||||
"settings.system.networkProxy.url.desc",
|
||||
"settings.system.networkProxy.bypass",
|
||||
"settings.system.networkProxy.bypass.placeholder",
|
||||
"settings.system.networkProxy.bypass.desc",
|
||||
"settings.system.networkProxy.hint",
|
||||
];
|
||||
|
||||
for (const locale of LOCALIZED_SETTINGS_LOCALES) {
|
||||
const missing = keys.filter((key) => !locale.messages[key]);
|
||||
assert.deepEqual(missing, [], `${locale.name} is missing network proxy labels`);
|
||||
}
|
||||
});
|
||||
|
||||
test("localized settings include terminal font weight option labels", () => {
|
||||
const keys = [
|
||||
"settings.terminal.font.weight.thin",
|
||||
"settings.terminal.font.weight.extraLight",
|
||||
"settings.terminal.font.weight.light",
|
||||
"settings.terminal.font.weight.normal",
|
||||
"settings.terminal.font.weight.medium",
|
||||
"settings.terminal.font.weight.semiBold",
|
||||
"settings.terminal.font.weight.bold",
|
||||
"settings.terminal.font.weight.extraBold",
|
||||
"settings.terminal.font.weight.black",
|
||||
];
|
||||
|
||||
for (const locale of LOCALIZED_SETTINGS_LOCALES) {
|
||||
const missing = keys.filter((key) => !locale.messages[key]);
|
||||
assert.deepEqual(missing, [], `${locale.name} is missing font weight labels`);
|
||||
}
|
||||
});
|
||||
|
||||
test("localized vault messages include host icon labels", () => {
|
||||
const keys = [
|
||||
"hostDetails.icon.title",
|
||||
"hostDetails.icon.desc",
|
||||
"hostDetails.icon.mode.auto",
|
||||
"hostDetails.icon.mode.custom",
|
||||
"hostDetails.icon.reset",
|
||||
"hostDetails.icon.showLibrary",
|
||||
"hostDetails.icon.hideLibrary",
|
||||
"hostDetails.icon.autoUsesDistro",
|
||||
"hostDetails.icon.customOverridesDistro",
|
||||
...HOST_ICON_IDS.map((id) => `hostDetails.icon.option.${id}`),
|
||||
...HOST_ICON_COLORS.map((color) => `hostDetails.icon.color.${color.id}`),
|
||||
];
|
||||
|
||||
for (const locale of LOCALIZED_SETTINGS_LOCALES) {
|
||||
const missing = keys.filter((key) => !locale.messages[key]);
|
||||
assert.deepEqual(missing, [], `${locale.name} is missing host icon labels`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import en from "./en.ts";
|
||||
import ru from "./ru.ts";
|
||||
import zhCN from "./zh-CN.ts";
|
||||
|
||||
const osc7Keys = [
|
||||
"terminal.toolbar.configureOsc7",
|
||||
"terminal.osc7Setup.title",
|
||||
"terminal.osc7Setup.desc",
|
||||
"terminal.osc7Setup.targets",
|
||||
"terminal.osc7Setup.command",
|
||||
"terminal.osc7Setup.run",
|
||||
"terminal.osc7Setup.running",
|
||||
"terminal.osc7Setup.configured",
|
||||
"terminal.osc7Setup.failed",
|
||||
"terminal.osc7Setup.sent",
|
||||
] as const;
|
||||
|
||||
test("OSC 7 setup copy exists in every bundled locale", () => {
|
||||
for (const [locale, messages] of Object.entries({ en, ru, zhCN })) {
|
||||
for (const key of osc7Keys) {
|
||||
assert.equal(typeof messages[key], "string", `${locale} is missing ${key}`);
|
||||
assert.notEqual(messages[key], "", `${locale} has empty ${key}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export type Messages = Record<string, string>;
|
||||
+15
-2102
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,492 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhCNAiMessages: Messages = {
|
||||
// AI Settings
|
||||
'ai.agentSettings': 'Agent 设置',
|
||||
'ai.chat.preparing': '准备中…',
|
||||
'ai.chat.compactingContext': '正在压缩较早的上下文…',
|
||||
'ai.chat.compactingStep': '正在为下一步整理上下文…',
|
||||
'ai.chat.compactionRetry': '请求过大,正在压缩上下文并重试…',
|
||||
'ai.chat.compactionBanner': '上下文已压缩:{before}K → {after}K tokens',
|
||||
'ai.title': 'AI',
|
||||
'ai.description': '配置 AI 提供商、Agent 和安全设置',
|
||||
'ai.providers': '提供商',
|
||||
'ai.agents': 'Agent',
|
||||
'ai.providers.empty': '尚未配置提供商。添加一个提供商以开始使用。',
|
||||
'ai.providers.add': '添加提供商',
|
||||
'ai.providers.active': '活跃',
|
||||
'ai.providers.apiKeyConfigured': 'API Key 已配置',
|
||||
'ai.providers.noApiKey': '未设置 API Key',
|
||||
'ai.providers.configure': '配置',
|
||||
'ai.providers.remove': '移除',
|
||||
'ai.providers.name': '显示名称',
|
||||
'ai.providers.name.placeholder': '例如 我的提供商',
|
||||
'ai.providers.style': '协议风格',
|
||||
'ai.providers.style.anthropic': 'Anthropic 兼容',
|
||||
'ai.providers.style.openai': 'OpenAI 兼容',
|
||||
'ai.providers.style.google': 'Google 兼容',
|
||||
'ai.providers.style.inherited': '默认',
|
||||
'ai.providers.style.help': '决定请求使用哪种 API 格式。当第三方端点的协议与其提供商类型不一致时,可手动覆盖。',
|
||||
'ai.providers.icon.change': '修改图标',
|
||||
'ai.providers.icon.upload': '上传图片',
|
||||
'ai.providers.icon.reset': '恢复默认',
|
||||
'ai.providers.icon.close': '收起',
|
||||
'ai.providers.icon.uploadedNote': '自定义图标(64×64 WebP)',
|
||||
'ai.providers.icon.errorType': '请选择图片文件。',
|
||||
'ai.providers.apiKey': 'API Key',
|
||||
'ai.providers.apiKey.placeholder': '输入 API Key',
|
||||
'ai.providers.apiKey.decrypting': '解密中...',
|
||||
'ai.providers.baseUrl': 'Base URL',
|
||||
'ai.providers.skipTLSVerify': '跳过 TLS 证书验证(用于自签名证书)',
|
||||
'ai.providers.defaultModel': '默认模型',
|
||||
'ai.providers.defaultModel.placeholder': '例如 gpt-4o, claude-sonnet-4-20250514',
|
||||
'ai.providers.contextWindow': '上下文窗口',
|
||||
'ai.providers.contextWindow.placeholder': '例如 128000',
|
||||
'ai.providers.contextWindow.help': '留空时优先使用模型列表返回的值;如果没有,Netcatty 会使用安全默认值。',
|
||||
'ai.providers.contextWindow.error': '请输入正整数,或留空。',
|
||||
'ai.providers.refreshModels': '刷新模型列表',
|
||||
'ai.providers.searchModel': '搜索或输入模型 ID...',
|
||||
'ai.providers.filterModels': '筛选模型...',
|
||||
'ai.providers.loadingModels': '加载模型中...',
|
||||
'ai.providers.noMatchingModels': '没有匹配的模型',
|
||||
'ai.providers.clickToLoadModels': '点击加载模型',
|
||||
'ai.providers.showingModels': '显示前 100 个,共 {count} 个模型。输入以筛选。',
|
||||
'ai.providers.advancedParams': '高级参数',
|
||||
'ai.providers.advancedParams.hint': '留空则使用提供商默认值。',
|
||||
'ai.providers.advancedParams.maxTokens.placeholder': '例如 4096',
|
||||
'ai.providers.advancedParams.default': '提供商默认',
|
||||
|
||||
// AI Codex
|
||||
'ai.codex': 'Codex',
|
||||
'ai.codex.title': 'Codex CLI',
|
||||
'ai.codex.description': '接入 OpenAI Codex。可以在这里登录 ChatGPT,也可以在设置里启用兼容 OpenAI 的 API Key 和自定义接口地址。',
|
||||
'ai.codex.detecting': '检测中...',
|
||||
'ai.codex.notFound': '未找到',
|
||||
'ai.codex.awaitingLogin': '等待登录',
|
||||
'ai.codex.connectedChatGPT': '已通过 ChatGPT 连接',
|
||||
'ai.codex.connectedApiKey': '已通过 API Key 连接',
|
||||
'ai.codex.connectedCustomConfig': '使用 ~/.codex/config.toml 自定义 provider',
|
||||
'ai.codex.customConfigIncomplete': '检测到自定义配置(缺少环境变量)',
|
||||
'ai.codex.customConfigHint': '使用 ~/.codex/config.toml 中配置的自定义 provider "{provider}",无需 ChatGPT 登录。',
|
||||
'ai.codex.customConfigMissingEnvKey': '警告:环境变量 {envKey} 未在当前 shell 中设置。请 export 它(或从包含该变量的 shell 启动 netcatty),否则 Codex 无法鉴权。',
|
||||
'ai.codex.notConnected': '未连接',
|
||||
'ai.codex.statusUnknown': '状态未知',
|
||||
'ai.codex.path': '路径:',
|
||||
'ai.codex.notFoundHint': '在 PATH 中未找到 codex。请安装或在下方指定可执行文件路径。',
|
||||
'ai.codex.customPathPlaceholder': '例如 /usr/local/bin/codex',
|
||||
'ai.codex.check': '检查',
|
||||
'ai.codex.resetPath': '重置',
|
||||
'ai.codex.openLogin': '打开登录',
|
||||
'ai.codex.logout': '退出登录',
|
||||
'ai.codex.connectChatGPT': '连接 ChatGPT',
|
||||
'ai.codex.refreshStatus': '刷新状态',
|
||||
|
||||
// AI Claude Code
|
||||
'ai.claude.title': 'Claude Code',
|
||||
'ai.claude.description': 'Anthropic 的智能编程助手。需要系统中已安装 Claude Code CLI。',
|
||||
'ai.claude.detecting': '检测中...',
|
||||
'ai.claude.detected': '已检测到',
|
||||
'ai.claude.notFound': '未找到',
|
||||
'ai.claude.path': '路径:',
|
||||
'ai.claude.notFoundHint': '在 PATH 中未找到 claude。请安装或在下方指定可执行文件路径。',
|
||||
'ai.claude.customPathPlaceholder': '例如 /usr/local/bin/claude',
|
||||
'ai.claude.configSection': '认证与配置(可选)',
|
||||
'ai.claude.configDir': '配置目录',
|
||||
'ai.claude.configDir.placeholder': '~/.claude(留空用默认)',
|
||||
'ai.claude.configDir.hint': '设置 CLAUDE_CONFIG_DIR —— 指向你已运行 `claude` 登录的目录(含 settings.json 和凭据)。',
|
||||
'ai.claude.settings': 'Settings 文件',
|
||||
'ai.claude.settings.placeholder': '~/team-settings.json(路径,或内联 {"model":"..."})',
|
||||
'ai.claude.settings.hint': '可选。settings.json 路径或内联 JSON,作为 SDK 的 `settings` 传入。与上面的「配置目录」互补且独立(叠加合并,不是替换)。',
|
||||
'ai.claude.envVars': '环境变量',
|
||||
'ai.claude.envVars.placeholder': 'ANTHROPIC_BASE_URL=https://...\nANTHROPIC_MODEL=...',
|
||||
'ai.claude.envVars.hint': '每行一个 KEY=VALUE,传给 Claude agent。明文存在本地——API key/凭据建议用上面的「配置目录」(claude 登录),不要放这里。',
|
||||
'ai.claude.check': '检查',
|
||||
'ai.claude.resetPath': '重置',
|
||||
|
||||
// AI GitHub Copilot CLI
|
||||
'ai.copilot.title': 'GitHub Copilot CLI',
|
||||
'ai.copilot.description': '接入 GitHub Copilot CLI。检测到后即可作为外部编程 Agent 使用。',
|
||||
'ai.copilot.detecting': '检测中...',
|
||||
'ai.copilot.detected': '已检测到',
|
||||
'ai.copilot.notFound': '未找到',
|
||||
'ai.copilot.path': '路径:',
|
||||
'ai.copilot.notFoundHint': '在 PATH 中未找到 copilot。请安装或在下方指定可执行文件路径。',
|
||||
'ai.copilot.customPathPlaceholder': '例如 /usr/local/bin/copilot',
|
||||
'ai.copilot.check': '检查',
|
||||
'ai.copilot.resetPath': '重置',
|
||||
|
||||
// AI Cursor SDK
|
||||
'ai.cursor.title': 'Cursor',
|
||||
'ai.cursor.description': '使用 Cursor SDK。',
|
||||
'ai.cursor.detecting': '检测中...',
|
||||
'ai.cursor.detected': '可用',
|
||||
'ai.cursor.notFound': '不可用',
|
||||
'ai.cursor.path': '运行环境:',
|
||||
'ai.cursor.notFoundHint': '填写 API Key 后即可使用。',
|
||||
'ai.cursor.notInstalledHint': '未检测到 Cursor SDK。',
|
||||
'ai.cursor.installStatus': 'Cursor SDK',
|
||||
'ai.cursor.installed': '已检测到',
|
||||
'ai.cursor.notInstalled': '未检测到',
|
||||
'ai.cursor.apiKeyStatus': 'API Key',
|
||||
'ai.cursor.apiKeyConfigured': '已填写',
|
||||
'ai.cursor.apiKeyMissing': '未填写',
|
||||
'ai.cursor.apiKeyFromEnv': '来自环境变量',
|
||||
'ai.cursor.apiKey': 'API Key',
|
||||
'ai.cursor.apiKeyPlaceholder': '输入 Cursor API Key',
|
||||
'ai.cursor.apiKeyPlaceholder.env': '已使用 CURSOR_API_KEY;填写后会覆盖',
|
||||
'ai.cursor.apiKeyEnvHint': '已检测到本机 CURSOR_API_KEY。留空即可继续使用,填写保存后会覆盖它。',
|
||||
'ai.cursor.apiKeyOverrideHint': '当前优先使用这里保存的 Key;清空保存后会回到 CURSOR_API_KEY。',
|
||||
'ai.cursor.saveApiKey': '保存',
|
||||
'ai.cursor.saved': '已保存',
|
||||
'ai.cursor.showApiKey': '显示 API Key',
|
||||
'ai.cursor.hideApiKey': '隐藏 API Key',
|
||||
'ai.cursor.customPathPlaceholder': '例如 /usr/local/bin/cursor',
|
||||
'ai.cursor.check': '检查',
|
||||
|
||||
// AI CodeBuddy Code
|
||||
'ai.codebuddy.title': 'CodeBuddy Code',
|
||||
'ai.codebuddy.description': '通过官方 Agent SDK(`@tencent-ai/agent-sdk`)接入 CodeBuddy Code。检测到后即可作为外部编程 Agent 使用。',
|
||||
'ai.codebuddy.detecting': '检测中...',
|
||||
'ai.codebuddy.detected': '已检测到',
|
||||
'ai.codebuddy.notFound': '未找到',
|
||||
'ai.codebuddy.path': '路径:',
|
||||
'ai.codebuddy.notFoundHint': '在 PATH 中未找到 codebuddy。请安装或在下方指定可执行文件路径。',
|
||||
'ai.codebuddy.customPathPlaceholder': '例如 /usr/local/bin/codebuddy',
|
||||
'ai.codebuddy.check': '检查',
|
||||
'ai.codebuddy.resetPath': '重置',
|
||||
'ai.codebuddy.configSection': '认证与配置(可选)',
|
||||
'ai.codebuddy.internetEnv': '网络环境',
|
||||
'ai.codebuddy.internetEnv.default': '默认(海外)',
|
||||
'ai.codebuddy.internetEnv.internal': 'Internal',
|
||||
'ai.codebuddy.internetEnv.ioa': 'IOA',
|
||||
'ai.codebuddy.internetEnv.hint': '设置 CODEBUDDY_INTERNET_ENVIRONMENT —— 受限网络环境请选择 Internal 或 IOA。',
|
||||
'ai.codebuddy.envVars': '环境变量',
|
||||
'ai.codebuddy.envVars.placeholder': 'CODEBUDDY_API_KEY=...\nCODEBUDDY_AUTH_TOKEN=...\nOTHER_VAR=...',
|
||||
'ai.codebuddy.envVars.hint': '每行一个 KEY=VALUE,传给 CodeBuddy agent。可在此设置 CODEBUDDY_API_KEY 或 CODEBUDDY_AUTH_TOKEN 完成认证。明文存在本地。',
|
||||
|
||||
// AI OpenCode
|
||||
'ai.opencode.title': 'OpenCode',
|
||||
'ai.opencode.description': '通过官方 SDK 接入 OpenCode。先在 OpenCode 里配置 provider 和密钥,检测到后即可作为外部编程 Agent 使用。',
|
||||
'ai.opencode.detecting': '检测中...',
|
||||
'ai.opencode.detected': '已检测到',
|
||||
'ai.opencode.notFound': '未找到',
|
||||
'ai.opencode.path': '路径:',
|
||||
'ai.opencode.notFoundHint': '在 PATH 中未找到 opencode。请安装或在下方指定可执行文件路径。',
|
||||
'ai.opencode.customPathPlaceholder': '例如 /usr/local/bin/opencode',
|
||||
'ai.opencode.check': '检查',
|
||||
'ai.opencode.resetPath': '重置',
|
||||
|
||||
// AI Default Agent
|
||||
'ai.defaultAgent': '默认 Agent',
|
||||
'ai.defaultAgent.description': '创建新 AI 会话时使用的 Agent',
|
||||
'ai.defaultAgent.catty': 'Catty(内置)',
|
||||
'ai.toolAccess.title': '工具接入',
|
||||
'ai.toolAccess.mode': 'Netcatty 接入模式',
|
||||
'ai.toolAccess.description': '选择外部 Agent 访问 Netcatty 会话的方式。MCP 会暴露内置服务器,Skills + CLI 会引导 Agent 读取本地 Skill 并调用 Netcatty CLI。',
|
||||
'ai.toolAccess.mode.mcp': 'MCP',
|
||||
'ai.toolAccess.mode.skills': 'Skills + CLI',
|
||||
|
||||
// External MCP
|
||||
'ai.externalMcp.title': '对外 MCP',
|
||||
'ai.externalMcp.description': '把 Netcatty 作为 MCP 服务器暴露给 Codex、Claude Code、Cursor、Grok 等外部客户端。工具面与应用内 Agent 相同(终端、SFTP、Vault、端口转发)。客户端连接期间请保持 Netcatty 运行。',
|
||||
'ai.externalMcp.sessionsExposed': '作用域内会话:{count}',
|
||||
'ai.externalMcp.mode': '可用模式',
|
||||
'ai.externalMcp.mode.temporary': '临时',
|
||||
'ai.externalMcp.mode.persistent': '常开',
|
||||
'ai.externalMcp.mode.description': '临时模式会在空闲超时后自动关闭;常开模式会在 Netcatty 启动时恢复对外 MCP。',
|
||||
'ai.externalMcp.idleTimeout': '空闲超时',
|
||||
'ai.externalMcp.idleTimeout.description': '临时模式下,若超过该分钟数没有 MCP 操作,将自动关闭对外 MCP。',
|
||||
'ai.externalMcp.idleTimeout.minutes': '分钟',
|
||||
'ai.externalMcp.usage.title': '使用说明',
|
||||
'ai.externalMcp.usage.keepRunning': '1. 打开对外 MCP 开关,并保持 Netcatty 运行。',
|
||||
'ai.externalMcp.usage.localhost': '2. 客户端通过本地 launcher 连接(仅 127.0.0.1)。关闭开关后会删除 discovery。',
|
||||
'ai.externalMcp.usage.permissions': '3. 写操作遵循「设置 → AI → 安全」(观察 / 确认 / 自动)以及命令黑名单。',
|
||||
'ai.externalMcp.usage.capabilities': '4. 完整 catalog 工具可用:终端、SFTP、Vault、端口转发。密码 / 私钥不会返回。',
|
||||
'ai.externalMcp.help.ariaLabel': '对外 MCP 使用说明',
|
||||
'ai.externalMcp.security': '安全',
|
||||
'ai.externalMcp.security.description': '仅监听 127.0.0.1,使用轮换 token,写操作复用 AI 权限模式,关闭时删除 discovery。不做 OAuth——这是本机桌面桥接。',
|
||||
'ai.externalMcp.permissionMode': '当前权限模式:{mode}',
|
||||
'ai.externalMcp.permissionMode.label': '写操作权限模式',
|
||||
'ai.externalMcp.permissionMode.hint': '与「设置 → AI → 安全」为同一项。自动:Netcatty 写操作不再弹本应用审批;确认:每次询问。外部客户端(Codex / Claude / Grok)仍可能有自己的工具审批界面。',
|
||||
'ai.externalMcp.permissionMode.unknown': '未知',
|
||||
'ai.externalMcp.discovery': 'Discovery',
|
||||
'ai.externalMcp.launcher': 'Launcher',
|
||||
'ai.externalMcp.unavailable': '不可用',
|
||||
'ai.externalMcp.bridgeUnavailable': '对外 MCP 桥接不可用',
|
||||
'ai.externalMcp.copy': '复制',
|
||||
'ai.externalMcp.copied': '已复制',
|
||||
'ai.externalMcp.copyFailed': '复制失败,请手动复制。',
|
||||
'ai.externalMcp.refresh': '刷新',
|
||||
'ai.externalMcp.clientConfiguration': '客户端配置',
|
||||
'ai.externalMcp.clientConfiguration.description': '选择客户端一键安装,或复制 CLI / 配置片段。',
|
||||
'ai.externalMcp.client.codex': 'Codex',
|
||||
'ai.externalMcp.client.claude': 'Claude Code',
|
||||
'ai.externalMcp.client.grok': 'Grok',
|
||||
'ai.externalMcp.client.cursor': 'Cursor',
|
||||
'ai.externalMcp.cliCommand': 'CLI 命令',
|
||||
'ai.externalMcp.configSnippet': '配置片段',
|
||||
'ai.externalMcp.addToCodex': '添加到 Codex',
|
||||
'ai.externalMcp.addToClaude': '添加到 Claude Code',
|
||||
'ai.externalMcp.addToGrok': '添加到 Grok',
|
||||
'ai.externalMcp.codexAdded': '已添加 Codex MCP 条目。请重启 Codex 或打开新会话。',
|
||||
'ai.externalMcp.claudeAdded': '已添加 Claude Code MCP 条目。请重启 Claude Code 或打开新会话。',
|
||||
'ai.externalMcp.grokAdded': '已添加 Grok MCP 条目。请重启 Grok 或打开新会话。',
|
||||
'ai.externalMcp.installCodex': '请先单独安装 Codex,然后点击刷新。',
|
||||
'ai.externalMcp.installClaude': '请先单独安装 Claude Code,然后点击刷新。',
|
||||
'ai.externalMcp.installGrok': '请先单独安装 Grok CLI,然后点击刷新。',
|
||||
'ai.externalMcp.conflict.description': '已存在指向其他位置的 netcatty-external 条目,请手动删除或修改。',
|
||||
'ai.externalMcp.enableForLauncher': '请先启用对外 MCP,以获取可用的 launcher 路径。',
|
||||
'ai.externalMcp.cursor.title': 'Cursor / 其他客户端',
|
||||
'ai.externalMcp.cursor.description': '合并到 MCP 配置文件(例如 ~/.cursor/mcp.json)。若已有其他服务器,请勿整文件覆盖。',
|
||||
'ai.externalMcp.status.unavailable': '不可用',
|
||||
'ai.externalMcp.status.disabled': '已关闭',
|
||||
'ai.externalMcp.status.running': '运行中',
|
||||
'ai.externalMcp.status.starting': '启动中',
|
||||
'ai.externalMcp.status.error': '错误',
|
||||
'ai.externalMcp.status.configured': '已配置',
|
||||
'ai.externalMcp.status.notConfigured': '未配置',
|
||||
'ai.externalMcp.status.checking': '检查中',
|
||||
'ai.externalMcp.status.codexNotFound': '未找到 Codex',
|
||||
'ai.externalMcp.status.claudeNotFound': '未找到 Claude Code',
|
||||
'ai.externalMcp.status.grokNotFound': '未找到 Grok',
|
||||
'ai.externalMcp.status.conflict': '冲突',
|
||||
'ai.userSkills.title': '用户 Skills',
|
||||
'ai.userSkills.description': '打开 Netcatty 的 Skills 文件夹以添加你自己的技能目录。Netcatty 会自动扫描这些 skills,默认只注入轻量索引,只有在请求明显命中某个 skill 时才展开正文。',
|
||||
'ai.userSkills.openFolder': '打开 Skills 文件夹',
|
||||
'ai.userSkills.reload': '重新加载 Skills',
|
||||
'ai.userSkills.location': '位置',
|
||||
'ai.userSkills.loading': '正在扫描用户 skills...',
|
||||
'ai.userSkills.summary': '已就绪 {ready} 个,警告 {warnings} 个',
|
||||
'ai.userSkills.empty': '暂未发现用户 skills。打开文件夹后可添加包含 SKILL.md 的技能目录。',
|
||||
'ai.userSkills.unavailable': '当前环境不支持用户 skills。',
|
||||
'ai.userSkills.status.ready': '正常',
|
||||
'ai.userSkills.status.warning': '警告',
|
||||
|
||||
// AI Quick Messages
|
||||
'ai.quickMessages.title': '快捷消息',
|
||||
'ai.quickMessages.description': '创建常用提示词,在 AI 聊天框输入 / 或点击快捷按钮即可插入到输入框。与用户 Skills 不同,快捷消息会直接填入消息内容。',
|
||||
'ai.quickMessages.add': '添加快捷消息',
|
||||
'ai.quickMessages.createTitle': '新建快捷消息',
|
||||
'ai.quickMessages.editTitle': '编辑快捷消息',
|
||||
'ai.quickMessages.name': '名称',
|
||||
'ai.quickMessages.name.placeholder': '例如:检查磁盘空间',
|
||||
'ai.quickMessages.slug': '命令',
|
||||
'ai.quickMessages.slug.placeholder': 'disk-check',
|
||||
'ai.quickMessages.descriptionField': '说明(可选)',
|
||||
'ai.quickMessages.descriptionField.placeholder': '简短描述这条快捷消息的用途',
|
||||
'ai.quickMessages.content': '消息内容',
|
||||
'ai.quickMessages.content.placeholder': '输入选择后要插入的完整提示词...',
|
||||
'ai.quickMessages.empty': '还没有快捷消息。添加几条常用提示,聊天时就能一键插入。',
|
||||
'ai.quickMessages.confirmDelete': '确定删除快捷消息「{name}」吗?',
|
||||
'ai.quickMessages.error.nameRequired': '请填写名称。',
|
||||
'ai.quickMessages.error.invalidSlug': '命令只能包含小写字母、数字和连字符。',
|
||||
'ai.quickMessages.error.contentRequired': '请填写消息内容。',
|
||||
'ai.quickMessages.error.slugTaken': '该命令已被其他快捷消息使用。',
|
||||
'ai.quickMessages.error.slugConflictsWithSkill': '该命令与用户 Skill「/{slug}」冲突,请换一个命令。',
|
||||
'ai.quickMessages.error.maxItems': '最多只能保存 {max} 条快捷消息。',
|
||||
|
||||
// AI Chat
|
||||
'ai.chat.noProvider': '尚未配置 AI 提供商。请前往 **设置 → AI → 提供商** 添加并启用一个提供商。',
|
||||
'ai.chat.toolDenied': '操作已被用户拒绝。',
|
||||
'ai.chat.toolApproved': '已批准',
|
||||
'ai.chat.toolApprovalHint': 'Enter 允许 · Esc 拒绝',
|
||||
'ai.chat.approve': '批准',
|
||||
'ai.chat.approveOnce': '允许',
|
||||
'ai.chat.alwaysAllow': '始终',
|
||||
'ai.chat.slashStopDesc': '停止当前 AI 回合并取消进行中的工具',
|
||||
'ai.chat.reject': '拒绝',
|
||||
'ai.chat.toolLabel': '工具',
|
||||
'ai.chat.targetLabel': '目标',
|
||||
'ai.chat.permissionRequired': '需要权限',
|
||||
'ai.chat.permissionDescription': 'AI Agent 希望执行一个需要你批准的工具调用。',
|
||||
'ai.chat.commandBlocked': '此命令已被安全策略拦截,无法执行。',
|
||||
'ai.chat.recommendAllow': '允许',
|
||||
'ai.chat.recommendConfirm': '确认',
|
||||
'ai.chat.recommendDeny': '拒绝',
|
||||
'ai.chat.exportConversation': '导出对话',
|
||||
'ai.chat.exportAs': '导出为',
|
||||
'ai.chat.exportMarkdown': 'Markdown',
|
||||
'ai.chat.exportJSON': 'JSON',
|
||||
'ai.chat.exportPlainText': '纯文本',
|
||||
'ai.chat.thinking': '思考中',
|
||||
'ai.chat.thoughtFor': '思考了 {duration}',
|
||||
'ai.chat.thought': '思考',
|
||||
'ai.chat.agents': 'Agents',
|
||||
'ai.chat.detectedOnMachine': '在本机检测到',
|
||||
'ai.chat.rescan': '重新扫描',
|
||||
'ai.chat.permObserver': '观察',
|
||||
'ai.chat.permConfirm': '确认',
|
||||
'ai.chat.permAuto': '自动',
|
||||
'ai.chat.permObserverDesc': '只读模式',
|
||||
'ai.chat.permConfirmDesc': '操作前询问',
|
||||
'ai.chat.permAutoDesc': '自由执行',
|
||||
'ai.chat.emptyHint': '询问服务器相关问题、执行命令或获取配置帮助。',
|
||||
'ai.chat.placeholder': '向 {agent} 发送消息 — @ 引用上下文,/ 使用命令',
|
||||
'ai.chat.placeholderDefault': '向 Catty Agent 发送消息...',
|
||||
'ai.chat.noModel': '未选择模型',
|
||||
'ai.chat.noProviderModel': '未配置默认模型——前往 设置 → AI → 提供商 设置。',
|
||||
'ai.chat.selectProvider': '选择提供商',
|
||||
'ai.chat.recent': '最近',
|
||||
'ai.chat.viewAll': '查看全部',
|
||||
'ai.chat.untitled': '无标题',
|
||||
'ai.chat.justNow': '刚刚',
|
||||
'ai.chat.minutesAgo': '{n}分钟前',
|
||||
'ai.chat.hoursAgo': '{n}小时前',
|
||||
'ai.chat.daysAgo': '{n}天前',
|
||||
'ai.chat.newChat': '新对话',
|
||||
'ai.chat.allSessions': '所有会话',
|
||||
'ai.chat.loadEarlierMessages': '加载更早的消息(还有 {n} 条)',
|
||||
'ai.chat.usedTools': '已使用 {n} 个工具',
|
||||
'ai.chat.loadMoreSessions': '加载更多会话(还有 {n} 条)',
|
||||
'ai.chat.noSessions': '没有历史会话',
|
||||
'ai.chat.retryHint': '你可以重新发送消息来重试。',
|
||||
'ai.chat.approvalTimeout': '工具审批已超时(5 分钟)。你可以重新发送消息来重试。',
|
||||
'ai.chat.menuHosts': '主机',
|
||||
'ai.chat.menuContext': '上下文',
|
||||
'ai.chat.menuFiles': '文件',
|
||||
'ai.chat.menuImage': '图片',
|
||||
'ai.chat.menuMentionHost': '提及主机',
|
||||
'ai.chat.menuUserSkills': '用户 Skills',
|
||||
'ai.chat.menuSlashCommands': '快捷命令',
|
||||
'ai.chat.slashCommands': '快捷命令',
|
||||
'ai.chat.slashQuickMessages': '快捷消息',
|
||||
'ai.chat.slashUserSkills': '用户 Skills',
|
||||
'ai.chat.quickMessages': '快捷命令',
|
||||
'ai.chat.slashNoResults': '没有匹配的命令',
|
||||
'ai.chat.slashEmptyHint': '可在 设置 → AI → 快捷消息 中添加常用提示词。',
|
||||
|
||||
// AI 聊天快捷入口
|
||||
'ai.chatShortcuts.title': '聊天快捷入口',
|
||||
'ai.chatShortcuts.selectionAction': '选中终端内容时显示“添加到对话”',
|
||||
'ai.chatShortcuts.selectionAction.description': '在终端里选中文本后显示 AI 快捷按钮。',
|
||||
|
||||
// AI Error
|
||||
'ai.codex.bridgeError': 'Codex 主进程处理器尚未加载。请完全重启 Netcatty 或重启 Electron 开发进程,然后重试。',
|
||||
|
||||
// AI Web Search
|
||||
'ai.webSearch.title': '网络搜索',
|
||||
'ai.webSearch.enable': '启用网络搜索',
|
||||
'ai.webSearch.enable.description': '允许 AI 代理搜索互联网获取最新信息。',
|
||||
'ai.webSearch.provider': '搜索供应商',
|
||||
'ai.webSearch.provider.description': '选择一个网络搜索 API 供应商。',
|
||||
'ai.webSearch.apiKey': 'API 密钥',
|
||||
'ai.webSearch.apiKey.description': '所选搜索供应商的 API 密钥。',
|
||||
'ai.webSearch.apiKey.placeholder': '输入 API 密钥...',
|
||||
'ai.webSearch.apiHost': 'API 地址',
|
||||
'ai.webSearch.apiHost.description': '自定义 API 端点。除非使用代理,否则保持默认值。',
|
||||
'ai.webSearch.apiHost.searxngDescription': 'SearXNG 实例的 URL(必填)。',
|
||||
'ai.webSearch.maxResults': '最大结果数',
|
||||
'ai.webSearch.maxResults.description': '搜索返回的最大结果数(1-20)。',
|
||||
|
||||
// AI Safety Settings
|
||||
'ai.safety.title': '安全',
|
||||
'ai.safety.permissionMode': '权限模式',
|
||||
'ai.safety.permissionMode.description': '控制 AI 通过 Netcatty 访问终端会话的方式。观察者模式会阻止经由 Netcatty 的写操作;外部 Agent CLI 可能仍有自己的本机工具和审批流程。',
|
||||
'ai.safety.permissionMode.observer': '观察者 - 只读,禁止操作',
|
||||
'ai.safety.permissionMode.confirm': '确认 - 操作前询问',
|
||||
'ai.safety.permissionMode.auto': '自动 - 自由执行',
|
||||
'ai.safety.commandTimeout': '命令超时',
|
||||
'ai.safety.commandTimeout.description': '通过 Netcatty 执行命令时允许运行的最长秒数,超时将被终止。',
|
||||
'ai.safety.commandTimeout.unit': '秒',
|
||||
'ai.safety.maxIterations': '最大迭代次数',
|
||||
'ai.safety.maxIterations.description': '防止 AI 失控执行的最大工具调用循环次数。外部 Agent 可能有自己的内部迭代限制,以其为准。',
|
||||
'ai.safety.blocklist': '命令黑名单',
|
||||
'ai.safety.blocklist.description': '用于拦截通过 Netcatty 执行的危险命令的正则表达式。',
|
||||
'ai.safety.blocklist.placeholder': '正则表达式...',
|
||||
'ai.safety.blocklist.reset': '恢复默认',
|
||||
'ai.safety.blocklist.add': '添加规则',
|
||||
'ai.safety.grants.title': '许可记忆表',
|
||||
'ai.safety.grants.heading': 'Confirm 模式放行规则',
|
||||
'ai.safety.grants.description': 'Confirm 模式会先询问再执行;保存为规则后,匹配的同类操作会自动放行。规则对所有终端节点/会话生效,也可手动编辑。',
|
||||
'ai.safety.grants.empty': '尚无规则。可在审批时选择「一律许可」,或手动添加。',
|
||||
'ai.safety.grants.capability': '能力',
|
||||
'ai.safety.grants.sessionPattern': '节点/会话模式',
|
||||
'ai.safety.grants.commandPattern': '命令模式(可选)',
|
||||
'ai.safety.grants.note': '备注(可选)',
|
||||
'ai.safety.grants.add': '添加规则',
|
||||
'ai.safety.grants.remove': '删除',
|
||||
'ai.safety.grants.export': '导出 JSON',
|
||||
'ai.safety.grants.import': '导入 JSON',
|
||||
'ai.safety.note': '这些安全设置会约束经由 Netcatty 执行的操作。外部 Agent CLI 也可能提供本机工具,那部分由 Agent 自己的控制规则约束。',
|
||||
|
||||
// 统一终端工作区和顶部标签的 tooltip 文案 (issue #954)
|
||||
'terminal.layer.addTerminal': '添加终端',
|
||||
'terminal.layer.switchToSplitView': '切换到分屏视图',
|
||||
'terminal.layer.sftp': '文件传输',
|
||||
'terminal.layer.scripts': '脚本',
|
||||
'terminal.layer.history': '命令历史',
|
||||
'terminal.layer.theme': '主题',
|
||||
'terminal.layer.notes': '笔记',
|
||||
'terminal.layer.aiChat': 'AI 助手',
|
||||
'terminal.layer.movePanelLeft': '面板移至左侧',
|
||||
'terminal.layer.movePanelRight': '面板移至右侧',
|
||||
'terminal.layer.closePanel': '关闭面板',
|
||||
'terminal.layer.hostTree.search': '搜索主机...',
|
||||
'terminal.layer.hostTree.searchButton': '搜索',
|
||||
'terminal.layer.hostTree.tagsButton': '按标签筛选',
|
||||
'terminal.layer.hostTree.newGroup': '新建分组',
|
||||
'terminal.layer.hostTree.localShell': '本地 Shell',
|
||||
'terminal.layer.hostTree.tagsEmpty': '暂无标签',
|
||||
'terminal.layer.hostTree.clearTags': '清除筛选',
|
||||
'terminal.layer.hostTree.collapse': '收起主机列表',
|
||||
'terminal.layer.hostTree.expand': '展开主机列表',
|
||||
'terminal.layer.hostTree.empty': '没有匹配的主机',
|
||||
'terminal.layer.hostTree.details.host': '主机',
|
||||
'terminal.layer.hostTree.details.user': '用户',
|
||||
'terminal.layer.hostTree.details.port': '端口',
|
||||
'terminal.layer.hostTree.details.protocol': '协议',
|
||||
'terminal.layer.hostTree.details.group': '分组',
|
||||
'terminal.layer.hostTree.details.tags': '标签',
|
||||
'terminal.layer.hostTree.details.lastConnected': '最近连接',
|
||||
'topTabs.openQuickSwitcher': '打开快速切换',
|
||||
'topTabs.moreTabs': '更多标签页',
|
||||
'topTabs.aiAssistant': 'AI 助手',
|
||||
'topTabs.windowOpacity': '窗口透明度',
|
||||
'topTabs.toggleTheme': '切换主题',
|
||||
'topTabs.openSettings': '打开设置',
|
||||
'ai.chat.sessionHistory': '会话历史',
|
||||
'ai.chat.attach': '附件',
|
||||
'ai.chat.terminalSelectionAttachment': '终端选区',
|
||||
'ai.chat.terminalSelectionLines': '{count} 行',
|
||||
'ai.chat.collapse': '收起',
|
||||
'ai.chat.expand': '展开',
|
||||
'ai.chat.enableAgent': '启用 {name}',
|
||||
'ai.chat.artifact.noteFallback': 'Vault 笔记',
|
||||
'ai.chat.artifact.openNotes': '打开笔记',
|
||||
'ai.chat.artifact.openHosts': '打开主机',
|
||||
'ai.chat.artifact.notesSummary': 'Vault 中有 {count} 条笔记',
|
||||
'ai.chat.artifact.hostsSummary': 'Vault 中有 {count} 台主机',
|
||||
'ai.chat.artifact.hostsAdded': '已添加 {count} 台主机',
|
||||
'ai.chat.artifact.hostsPreview': '预览 {count} 台主机',
|
||||
'ai.chat.artifact.failed': 'Vault 操作失败',
|
||||
'ai.chat.artifact.unavailableTitle': '不可用',
|
||||
'ai.chat.artifact.noteMissing': '该笔记已不在 Vault 中。',
|
||||
'ai.chat.artifact.hostMissing': '该主机已不在 Vault 中。',
|
||||
'ai.chat.artifact.snippetMissing': '该 Snippet 或脚本已不在 Vault 中。',
|
||||
'ai.chat.artifact.openSnippets': '打开 Snippets',
|
||||
'ai.chat.artifact.snippetsSummary': 'Vault 中有 {count} 个 Snippet',
|
||||
'ai.chat.artifact.scriptsSummary': 'Vault 中有 {count} 个脚本',
|
||||
'ai.chat.artifact.snippetFallback': 'Vault Snippet',
|
||||
'ai.chat.artifact.scriptFallback': '自动化脚本',
|
||||
'ai.chat.artifact.scriptLanguage': '{language} 脚本',
|
||||
'ai.chat.artifact.snippetDeleted': 'Snippet 已删除',
|
||||
'ai.chat.artifact.scriptDeleted': '脚本已删除',
|
||||
'ai.chat.artifact.snippetRan': 'Snippet 已执行',
|
||||
'ai.chat.artifact.scriptStarted': '脚本运行已启动',
|
||||
'ai.chat.artifact.scriptRunStatus': '脚本运行 {status}',
|
||||
'ai.chat.artifact.scriptRunsSummary': '{count} 个脚本运行',
|
||||
'ai.chat.artifact.scriptRunStopped': '脚本运行已停止',
|
||||
'ai.chat.artifact.scriptRunPaused': '脚本运行已暂停',
|
||||
'ai.chat.artifact.scriptRunResumed': '脚本运行已恢复',
|
||||
'ai.chat.artifact.scriptReference': 'nct API 参考',
|
||||
'zmodem.waitingForRemote': '等待远端...',
|
||||
'zmodem.uploading': '上传中',
|
||||
'zmodem.downloading': '下载中',
|
||||
'zmodem.cancelTransfer': '取消传输 (Ctrl+C)',
|
||||
'zmodem.overwrite.title': '远端已存在同名文件',
|
||||
'zmodem.overwrite.applyToRest': '应用到其余冲突文件',
|
||||
'zmodem.overwrite.overwrite': '覆盖',
|
||||
'zmodem.overwrite.skip': '跳过',
|
||||
'zmodem.overwrite.cancel': '取消',
|
||||
'settings.shortcuts.resetToDefault': '重置为默认',
|
||||
};
|
||||
@@ -0,0 +1,758 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhCNCoreMessages: Messages = {
|
||||
// Common
|
||||
'common.save': '保存',
|
||||
'common.cancel': '取消',
|
||||
'common.close': '关闭',
|
||||
'common.reset': '重置',
|
||||
'common.zoomIn': '放大',
|
||||
'common.zoomOut': '缩小',
|
||||
'common.settings': '设置',
|
||||
'common.search': '搜索',
|
||||
'common.connect': '连接',
|
||||
'common.terminal': '终端',
|
||||
'common.create': '创建',
|
||||
'common.add': '添加',
|
||||
'common.rename': '重命名',
|
||||
'common.refresh': '刷新',
|
||||
'common.continue': '继续',
|
||||
'common.enabled': '已启用',
|
||||
'common.disabled': '已禁用',
|
||||
'common.unknownError': '未知错误',
|
||||
'common.noResultsFound': '没有匹配结果',
|
||||
'common.back': '返回',
|
||||
'common.apply': '应用',
|
||||
'common.use': '使用',
|
||||
'common.useGlobal': '跟随全局',
|
||||
'common.left': '左侧',
|
||||
'common.right': '右侧',
|
||||
'common.more': '更多',
|
||||
'common.selectAHost': '选择主机',
|
||||
'sort.manual': '手动顺序',
|
||||
'sort.az': 'A-z',
|
||||
'sort.za': 'Z-a',
|
||||
'sort.newest': '从新到旧',
|
||||
'sort.oldest': '从旧到新',
|
||||
'sort.group': '按分组',
|
||||
'field.label': 'Label',
|
||||
'field.type': '类型',
|
||||
'auth.keyType': '类型 {type}',
|
||||
'auth.showAllKeys': '显示全部 keys',
|
||||
|
||||
// Dialogs / prompts
|
||||
'confirm.deleteHost': '删除主机 "{name}"?',
|
||||
'confirm.deleteIdentity': '删除身份 "{name}"?',
|
||||
'confirm.removeProvider': '移除提供商 "{name}"?',
|
||||
'confirm.closeBusyTerminal.title': '确认关闭',
|
||||
'confirm.closeBusyTerminal.message': '进程 "{command}" 仍在运行,关闭后会被终止。',
|
||||
'confirm.closeBusyTerminal.messageWithMore': '进程 "{command}" 及其他 {count} 个正在运行的进程将被终止。',
|
||||
'confirm.closeBusyTerminal.cancel': '取消',
|
||||
'confirm.closeBusyTerminal.close': '关闭',
|
||||
'dialog.renameWorkspace.title': '重命名工作区',
|
||||
'dialog.renameSession.title': '重命名会话',
|
||||
'field.name': '名称',
|
||||
'placeholder.workspaceName': '工作区名称',
|
||||
'placeholder.sessionName': '会话名称',
|
||||
'toast.settingsUnavailable': '当前平台无法打开设置窗口。',
|
||||
'credentials.protectionUnavailable.title': '凭据保护不可用',
|
||||
'credentials.protectionUnavailable.message': '当前设备无法自动解密已保存的密码和密钥。连接前请重新输入凭据。',
|
||||
'credentials.protectionUnavailable.action': '打开设置',
|
||||
|
||||
// Settings shell
|
||||
'settings.title': '设置',
|
||||
'settings.tab.application': '应用',
|
||||
'settings.tab.appearance': '外观',
|
||||
'settings.tab.terminal': '终端',
|
||||
'settings.tab.shortcuts': '快捷键',
|
||||
'settings.tab.syncCloud': '同步与云',
|
||||
'settings.tab.system': '系统',
|
||||
'settings.terminal.connection.verifyHostKeys': '校验 SSH 主机密钥',
|
||||
'settings.terminal.connection.verifyHostKeys.desc': '连接到新的或已变化的 SSH 主机密钥前先询问。只建议在可信内网中关闭。',
|
||||
'settings.terminal.section.sidePanel': '侧边栏',
|
||||
'settings.terminal.sidePanel.autoOpen': '打开终端时展开侧边栏',
|
||||
'settings.terminal.sidePanel.autoOpen.desc': '终端连接成功后自动打开一个侧边栏。',
|
||||
'settings.terminal.sidePanel.autoOpenPane': '默认侧边栏',
|
||||
'settings.terminal.sidePanel.autoOpenPane.desc': '选择优先打开哪个面板。',
|
||||
'settings.terminal.sidePanel.pane.sftp': 'SFTP',
|
||||
'settings.terminal.sidePanel.pane.scripts': '脚本',
|
||||
'settings.terminal.sidePanel.pane.history': '历史',
|
||||
'settings.terminal.sidePanel.pane.theme': '主题',
|
||||
'settings.terminal.sidePanel.pane.system': '系统',
|
||||
'settings.terminal.sidePanel.pane.notes': '笔记',
|
||||
'settings.terminal.sidePanel.pane.ai': 'AI',
|
||||
|
||||
// Settings > System
|
||||
'settings.system.title': '系统',
|
||||
'settings.system.description': '系统信息与临时文件管理。',
|
||||
'settings.system.tempDirectory': '临时文件',
|
||||
'settings.system.location': '位置',
|
||||
'settings.system.fileCount': '文件数量',
|
||||
'settings.system.totalSize': '占用空间',
|
||||
'settings.system.openFolder': '打开文件夹',
|
||||
'settings.system.refresh': '刷新',
|
||||
'settings.system.clearTempFiles': '清理临时文件',
|
||||
'settings.system.clearing': '清理中...',
|
||||
'settings.system.clearResult': '已删除 {deleted} 个文件,{failed} 个失败。',
|
||||
'settings.system.tempDirectoryHint': '临时文件在使用外部应用打开远程文件时创建。SFTP 会话关闭时会自动清理。',
|
||||
'settings.system.credentials.title': '凭据保护',
|
||||
'settings.system.credentials.status': '状态',
|
||||
'settings.system.credentials.checking': '检查中...',
|
||||
'settings.system.credentials.available': '可用(系统钥匙串正常)',
|
||||
'settings.system.credentials.unavailable': '不可用(无法解密已保存凭据)',
|
||||
'settings.system.credentials.unknown': '未知(当前环境不支持)',
|
||||
'settings.system.credentials.unavailableHint': '在其他用户或机器上加密的凭据无法在此处解密。请在当前设备重新输入并保存凭据。',
|
||||
'settings.system.credentials.portabilityHint': '云同步可跨设备,因为使用主密钥加密;本地 safeStorage 加密仅绑定当前系统用户/设备。',
|
||||
|
||||
// Settings > System > Network Proxy
|
||||
'settings.system.networkProxy.title': '网络代理',
|
||||
'settings.system.networkProxy.description': '用于云同步与 AI 服务商的 HTTP(S) 代理。与 SSH 的 ProxyJump / ProxyCommand 配置无关。',
|
||||
'settings.system.networkProxy.mode': '代理模式',
|
||||
'settings.system.networkProxy.mode.system': '系统代理',
|
||||
'settings.system.networkProxy.mode.direct': '直连(不使用代理)',
|
||||
'settings.system.networkProxy.mode.custom': '自定义代理',
|
||||
'settings.system.networkProxy.url': '代理地址',
|
||||
'settings.system.networkProxy.url.placeholder': 'http://127.0.0.1:7890',
|
||||
'settings.system.networkProxy.url.desc': '支持 http://、https://、socks5:// 的 host:port 地址。代理 URL 中不支持账号密码。',
|
||||
'settings.system.networkProxy.bypass': '绕过列表',
|
||||
'settings.system.networkProxy.bypass.placeholder': '<local>,localhost,127.0.0.1',
|
||||
'settings.system.networkProxy.bypass.desc': '逗号分隔、不走代理的主机。可用 <local> 表示本机与局域网名称。',
|
||||
'settings.system.networkProxy.hint': '影响 Google Drive / OneDrive / GitHub 同步登录与 AI 服务商请求,不会改变 SSH 连接。',
|
||||
|
||||
// Settings > System > Crash Logs
|
||||
'settings.system.crashLogs.title': '崩溃日志',
|
||||
'settings.system.crashLogs.description': '查看主进程错误日志,帮助诊断异常行为。',
|
||||
'settings.system.crashLogs.noLogs': '未找到崩溃日志。',
|
||||
'settings.system.crashLogs.entries': '{count} 条记录',
|
||||
'settings.system.crashLogs.clear': '清除所有日志',
|
||||
'settings.system.crashLogs.cleared': '已清除 {count} 个日志文件。',
|
||||
'settings.system.crashLogs.source': '来源',
|
||||
'settings.system.crashLogs.time': '时间',
|
||||
'settings.system.crashLogs.message': '消息',
|
||||
'settings.system.crashLogs.stack': '堆栈跟踪',
|
||||
'settings.system.crashLogs.hint': '崩溃日志保留 30 天,超期自动清理。',
|
||||
'settings.system.crashLogs.collapse': '收起',
|
||||
'settings.system.crashLogs.expand': '查看详情',
|
||||
|
||||
// Settings > System > Software Update
|
||||
'settings.update.title': '软件更新',
|
||||
'settings.update.currentVersion': '当前版本',
|
||||
'settings.update.checkForUpdates': '检查更新',
|
||||
'settings.update.checking': '检查中...',
|
||||
'settings.update.upToDate': '当前已是最新版本。',
|
||||
'settings.update.available': '新版本 {version} 已发布。',
|
||||
'settings.update.download': '下载更新',
|
||||
'settings.update.downloading': '正在下载... {percent}%',
|
||||
'settings.update.readyToInstall': '更新已下载,准备安装。',
|
||||
'settings.update.restartNow': '重启并更新',
|
||||
'settings.update.error': '检查更新失败。',
|
||||
'settings.update.downloadError': '下载失败。',
|
||||
'settings.update.manualDownload': '前往 GitHub 下载',
|
||||
'settings.update.manualDownloadHint': '当前平台不支持自动更新,请前往 GitHub 下载最新版本。',
|
||||
'settings.update.hint': 'Netcatty 从 GitHub Releases 检查更新。',
|
||||
'settings.update.lastCheckedJustNow': '刚刚',
|
||||
'settings.update.lastCheckedMinutesAgo': '{n} 分钟前',
|
||||
'settings.update.lastCheckedHoursAgo': '{n} 小时前',
|
||||
'settings.update.lastCheckedPrefix': '上次检查:',
|
||||
'settings.update.autoUpdateEnabled': '自动更新',
|
||||
'settings.update.autoUpdateEnabledDesc': '有新版本时自动检查并下载更新。',
|
||||
|
||||
// Settings > Session Restore
|
||||
'settings.sessionRestore.title': '会话恢复',
|
||||
'settings.sessionRestore.restorePreviousSession': '恢复上次的标签页和工作区',
|
||||
'settings.sessionRestore.restorePreviousSessionDesc': '启动时重新打开上次的终端标签页和工作区布局。',
|
||||
'settings.sessionRestore.restoreTerminalCwd': '恢复终端工作目录',
|
||||
'settings.sessionRestore.restoreTerminalCwdDesc': '重新连接恢复的终端时,尝试回到上次记录的目录;失败时不会阻塞连接。',
|
||||
'terminal.restore.placeholder.title': '已恢复的会话',
|
||||
'terminal.restore.placeholder.desc': '此终端当前已断开。准备好后可以重新连接,为这个标签页启动新的 shell。',
|
||||
'terminal.restore.placeholder.reconnect': '重新连接',
|
||||
'terminal.restore.cwdLog': '正在恢复工作目录:{cwd}',
|
||||
|
||||
// Settings > Session Logs
|
||||
'settings.sessionLogs.title': '会话日志',
|
||||
'settings.sessionLogs.description': '配置会话日志导出和自动保存设置。',
|
||||
'settings.sessionLogs.autoSave': '自动保存',
|
||||
'settings.sessionLogs.enableAutoSave': '启用自动保存',
|
||||
'settings.sessionLogs.enableAutoSaveDesc': '在终端会话结束时自动保存会话日志。',
|
||||
'settings.sessionLogs.directory': '保存目录',
|
||||
'settings.sessionLogs.noDirectory': '未选择目录',
|
||||
'settings.sessionLogs.browse': '浏览',
|
||||
'settings.sessionLogs.openFolder': '打开文件夹',
|
||||
'settings.sessionLogs.directoryHint': '日志将按主机名组织在子目录中。',
|
||||
'settings.sessionLogs.format': '日志格式',
|
||||
'settings.sessionLogs.formatDesc': '选择保存日志文件的格式。',
|
||||
'settings.sessionLogs.formatTxt': '纯文本 (.txt)',
|
||||
'settings.sessionLogs.formatRaw': '原始格式 (.log)',
|
||||
'settings.sessionLogs.formatHtml': 'HTML (.html)',
|
||||
'settings.sessionLogs.timestamps': '添加时间戳',
|
||||
'settings.sessionLogs.timestampsDesc': '为纯文本和 HTML 日志的每一行添加本地时间。',
|
||||
'settings.sessionLogs.hint': '会话日志用于记录终端输出,便于故障排查和审计。',
|
||||
|
||||
// Settings > SSH Debug Logs
|
||||
'settings.sshDebugLogs.title': 'SSH 调试日志',
|
||||
'settings.sshDebugLogs.enable': '启用 SSH 调试日志',
|
||||
'settings.sshDebugLogs.enableDesc': '记录连接、认证、握手、断开和错误原因,不记录终端输出。',
|
||||
'settings.sshDebugLogs.location': '日志位置',
|
||||
'settings.sshDebugLogs.status': '状态',
|
||||
'settings.sshDebugLogs.statusOn': '已开启',
|
||||
'settings.sshDebugLogs.statusOff': '未开启',
|
||||
'settings.sshDebugLogs.size': '大小',
|
||||
'settings.sshDebugLogs.hint': '开启后,新发起的 SSH 连接会写入诊断信息,方便排查堡垒机、认证和异常断开问题。',
|
||||
|
||||
// Settings > Global Hotkey (Quake Mode)
|
||||
'settings.globalHotkey.title': '全局快捷键',
|
||||
'settings.globalHotkey.toggleWindow': '切换窗口',
|
||||
'settings.globalHotkey.toggleWindowDesc': '按下组合键以设置显示/隐藏窗口的全局快捷键。',
|
||||
'settings.globalHotkey.notSet': '未设置',
|
||||
'settings.globalHotkey.reset': '恢复默认',
|
||||
'settings.globalHotkey.closeToTray': '关闭时最小化到托盘',
|
||||
'settings.globalHotkey.closeToTrayDesc': '启用后,关闭窗口将最小化到系统托盘而不是退出程序。',
|
||||
'settings.globalHotkey.enabled': '启用全局快捷键',
|
||||
'settings.globalHotkey.enabledDesc': '注册系统级键盘快捷键。禁用后将取消所有全局快捷键注册。',
|
||||
'settings.globalHotkey.hint': '全局快捷键在系统范围内工作,可快速显示或隐藏窗口(下拉式终端风格)。',
|
||||
|
||||
// Tray Panel
|
||||
'tray.openMainWindow': '打开主窗口',
|
||||
'tray.sessions': '会话',
|
||||
'tray.portForwarding': '端口转发',
|
||||
'tray.status.connected': '已连接',
|
||||
'tray.status.connecting': '连接中',
|
||||
'tray.status.disconnected': '已断开',
|
||||
'tray.status.active': '已启用',
|
||||
'tray.status.inactive': '未启用',
|
||||
'tray.status.error': '错误',
|
||||
'tray.recentHosts': '最近连接的主机',
|
||||
'tray.empty.title': '一切都很安静',
|
||||
'tray.empty.subtitle': '去连接个服务器吧,它们想念你了 🚀',
|
||||
'tray.quit': '退出 Netcatty',
|
||||
|
||||
// Vault Sidebar
|
||||
'vault.sidebar.collapse': '收起侧边栏',
|
||||
'vault.sidebar.expand': '展开侧边栏',
|
||||
'vault.sidebar.resize': '调整侧边栏宽度',
|
||||
|
||||
// Settings > Application
|
||||
'settings.application.checkUpdates': '检查更新',
|
||||
'settings.application.reportProblem': '反馈问题',
|
||||
'settings.application.reportProblem.subtitle': '生成预填的 GitHub issue',
|
||||
'settings.application.community': '社区',
|
||||
'settings.application.community.subtitle': 'GitHub Discussions',
|
||||
'settings.application.github': 'GitHub',
|
||||
'settings.application.github.subtitle': '源代码',
|
||||
'settings.application.whatsNew': '更新内容',
|
||||
'settings.application.whatsNew.subtitle': '查看发布说明',
|
||||
'settings.application.openExternal.failedTitle': '无法打开链接',
|
||||
'settings.application.openExternal.failedBody': '系统浏览器和内置浏览器窗口都无法打开该链接。',
|
||||
'settings.vault.title': '主机库',
|
||||
'settings.vault.showRecentHosts': '显示最近连接的主机',
|
||||
'settings.vault.showRecentHostsDesc': '在主机列表顶部显示最近连接过的主机',
|
||||
'settings.vault.showOnlyUngroupedHostsInRoot': '根目录只显示未分组主机',
|
||||
'settings.vault.showOnlyUngroupedHostsInRootDesc': '开启后,主机库根目录的主机列表只显示没有分组的主机,已分组主机请从左侧分组进入查看。',
|
||||
'settings.vault.showSftpTab': '显示 SFTP 标签页',
|
||||
'settings.vault.showSftpTabDesc': '在顶部标签栏显示独立的 SFTP 视图。关闭后可改用会话内左侧的 SFTP 侧栏。',
|
||||
'settings.vault.showHostTreeSidebar': '显示主机列表侧栏',
|
||||
'settings.vault.showHostTreeSidebarDesc': '在终端和编辑器标签页显示主机列表侧栏及顶部开关。',
|
||||
|
||||
// Update notifications
|
||||
'update.available.title': '发现新版本',
|
||||
'update.available.message': '新版本 {version} 已发布,点击前往下载。',
|
||||
'update.checking': '正在检查更新...',
|
||||
'update.upToDate.title': '已是最新版本',
|
||||
'update.upToDate.message': '当前版本 ({version}) 已是最新。',
|
||||
'update.error': '检查更新失败',
|
||||
'update.downloadNow': '立即下载',
|
||||
'update.viewInSettings': '在设置中查看',
|
||||
'update.readyToInstall.title': '更新已就绪',
|
||||
'update.readyToInstall.message': '版本 {version} 已下载完成,准备安装。',
|
||||
'update.restartNow': '立即重启',
|
||||
'update.downloadFailed.title': '更新失败',
|
||||
'update.downloadFailed.message': '下载更新失败,可前往 GitHub 手动下载。',
|
||||
'update.needsSave.title': '有未保存内容',
|
||||
'update.needsSave.message': '请先保存已打开的编辑器,然后再次点击「立即重启」以安装更新。',
|
||||
'update.openReleases': '打开 Releases',
|
||||
'update.remindLater': '稍后提醒',
|
||||
'update.skipVersion': '跳过此版本',
|
||||
|
||||
// Settings > Appearance
|
||||
'settings.appearance.uiTheme': '界面主题',
|
||||
'settings.appearance.theme': '主题',
|
||||
'settings.appearance.theme.desc': '选择浅色、深色或跟随系统设置',
|
||||
'settings.appearance.theme.light': '浅色',
|
||||
'settings.appearance.theme.dark': '深色',
|
||||
'settings.appearance.theme.system': '系统',
|
||||
'settings.appearance.accentColor': '强调色',
|
||||
'settings.appearance.customColor': '自定义颜色',
|
||||
'settings.appearance.accentColor.mode': '使用自定义强调色',
|
||||
'settings.appearance.accentColor.mode.desc': '覆盖主题自带的强调色',
|
||||
'settings.appearance.accentColor.custom': '自定义强调色',
|
||||
'settings.appearance.themeColor': '主题色',
|
||||
'settings.appearance.themeColor.desc': '为浅色与深色主题选择预设配色',
|
||||
'settings.appearance.themeColor.light': '浅色主题',
|
||||
'settings.appearance.themeColor.dark': '深色主题',
|
||||
'settings.appearance.customCss': '自定义 CSS',
|
||||
'settings.appearance.customCss.desc':
|
||||
'使用自定义 CSS 个性化界面,修改会立即生效。主要 UI 区块都暴露了 [data-section="..."] 属性供你定位,比如:snippets-panel、host-details-panel、group-details-panel、serial-host-details-panel、ai-chat-panel、vault-sidebar、vault-main、vault-hosts-header、vault-host-list、vault-view、terminal-workspace、terminal-workspace-sidebar(Focus 模式终端列表)、terminal-host-tree-sidebar、terminal-host-tree-sidebar-content、terminal-host-tree-sidebar-row、terminal-side-panel(SFTP/脚本/主题/AI 侧栏,打开时生效)、terminal-side-panel-tabs、terminal-side-panel-content、terminal-sftp-panel、terminal-sftp-host-header、terminal-sftp-pane、terminal-sftp-toolbar、terminal-sftp-path、terminal-sftp-filter-bar、terminal-sftp-list、terminal-sftp-list-header、terminal-sftp-list-row、terminal-sftp-tree、terminal-sftp-tree-row、terminal-sftp-transfer-queue、terminal-sftp-transfer-row、terminal-split-pane、terminal-split-resizer、top-tabs、top-tabs-host-tree-toggle、top-tabs-quick-switcher-toggle。',
|
||||
'settings.appearance.customCss.placeholder':
|
||||
'/* 示例 — 由于 Tailwind 优先级较高,需要使用 !important */\n\n/* 隐藏顶部标签栏里的主机列表开关 */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* 隐藏打开快速切换器的加号按钮 */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* SFTP / 操作侧栏边框(关闭侧栏后不会残留) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* 修改整个操作侧栏背景,而不只是顶部标签 */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* 修改选中的 SFTP 文件行 */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* 加粗分屏分割线 */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* 高亮当前聚焦的分屏 */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* 也可在 设置 → 终端 → 工作区聚焦指示 → 聚焦窗格显示边框 */',
|
||||
'settings.appearance.language': '语言',
|
||||
'settings.appearance.language.desc': '选择界面语言',
|
||||
'settings.appearance.uiFont': '界面字体',
|
||||
'settings.appearance.uiFont.desc': '选择软件界面使用的字体',
|
||||
'settings.appearance.windowOpacity': '窗口透明度',
|
||||
'settings.appearance.windowOpacity.desc': '调节整个应用窗口的透明度,方便叠在其他内容上方。较低时终端文字也会变淡;部分 Linux 桌面环境可能不支持。',
|
||||
'settings.appearance.appIcon': '应用图标',
|
||||
'settings.appearance.appIcon.desc': '选择 Dock/任务栏图标风格,切换应用时更容易找到 Netcatty。运行时生效,安装包内的图标不会改变。',
|
||||
'settings.appearance.appIcon.original': '原始',
|
||||
'settings.appearance.appIcon.bright': '明亮',
|
||||
'settings.appearance.appIcon.dark': '深色',
|
||||
'settings.appearance.appIcon.colorful': '彩色',
|
||||
'settings.appearance.appIcon.highContrast': '高对比',
|
||||
'settings.appearance.appIcon.group.classic': '彩色底',
|
||||
'settings.appearance.appIcon.group.white': '白底',
|
||||
'settings.appearance.appIcon.group.special': '隐藏款',
|
||||
'settings.appearance.appIcon.whiteNavy': '白底 · 海军蓝猫',
|
||||
'settings.appearance.appIcon.whiteSky': '白底 · 天蓝猫',
|
||||
'settings.appearance.appIcon.whiteRose': '白底 · 玫红猫',
|
||||
'settings.appearance.appIcon.whiteEmerald': '白底 · 翡翠猫',
|
||||
'settings.appearance.appIcon.whiteAmber': '白底 · 琥珀猫',
|
||||
'settings.appearance.appIcon.whiteViolet': '白底 · 紫罗兰猫',
|
||||
'settings.appearance.appIcon.rainbow': '彩虹猫',
|
||||
// Context menus / common actions
|
||||
'action.newHost': '新建主机',
|
||||
'action.newSubfolder': '新建文件夹',
|
||||
'action.copyPublicKey': '复制公钥',
|
||||
'action.keyExport': '导出密钥',
|
||||
'action.edit': '编辑',
|
||||
'action.delete': '删除',
|
||||
'action.remove': '移除',
|
||||
'action.convertToHost': '转换为主机',
|
||||
|
||||
// Sync
|
||||
'sync.cloudSync': '云同步',
|
||||
'sync.settings': '同步设置',
|
||||
'sync.active': '云同步已启用',
|
||||
'sync.pending': '有待同步的更改',
|
||||
'sync.syncing': '正在同步…',
|
||||
'sync.error': '同步错误',
|
||||
'sync.notConfigured': '未配置',
|
||||
'sync.failed': '同步失败',
|
||||
'sync.connected': '已连接',
|
||||
'sync.syncNow': '立即同步',
|
||||
'sync.recentActivity': '最近活动',
|
||||
'sync.history.uploaded': '已 Upload',
|
||||
'sync.history.downloaded': '已 Download',
|
||||
'sync.history.resolved': '已处理',
|
||||
'sync.toast.completedMessage': '同步完成',
|
||||
'sync.toast.errorTitle': '同步错误',
|
||||
'sync.autoSync.failedTitle': '同步失败',
|
||||
'sync.autoSync.inspectFailedTitle': '同步已暂停',
|
||||
'sync.autoSync.inspectFailedMessage': '无法访问云端以检查变更。数据改动或下次启动时会自动重试。',
|
||||
'sync.autoSync.syncedTitle': '已从云端同步',
|
||||
'sync.autoSync.syncedMessage': '你的数据已从云端更新。',
|
||||
'sync.autoSync.noProvider': '未连接云同步 provider。请打开 设置 → Sync & Cloud 进行连接。',
|
||||
'sync.autoSync.alreadySyncing': '同步正在进行中。',
|
||||
'sync.autoSync.restoreInProgress': '另一个窗口中的本地备份恢复正在进行中,请等待其完成。',
|
||||
'sync.autoSync.interruptedApplyTitle': '同步已暂停 — 上次恢复未完成',
|
||||
'sync.autoSync.interruptedApplyMessage': '上次本地恢复过程未正常结束,本地数据可能处于半应用状态。请打开「设置 → Sync & Cloud → 恢复」,从保护性备份中恢复后再让自动同步继续。',
|
||||
'sync.autoSync.vaultLocked': 'Vault 处于锁定状态。请打开 设置 → Sync & Cloud 解锁。',
|
||||
'sync.autoSync.conflictDetected': '检测到同步冲突。请打开 设置 → Sync & Cloud 处理。',
|
||||
'sync.autoSync.syncFailed': '同步失败',
|
||||
'sync.autoSync.restoredTitle': '已恢复',
|
||||
'sync.autoSync.restoredMessage': '已从云端恢复主机库数据。',
|
||||
'sync.autoSync.keptLocalTitle': '已保留本地数据',
|
||||
'sync.autoSync.keptLocalMessage': '保留了空的本地主机库,未应用云端数据。',
|
||||
'sync.autoSync.emptyVaultConflict.title': '检测到空主机库',
|
||||
'sync.autoSync.emptyVaultConflict.description': '本地主机库为空,但云端有数据。这通常发生在应用更新或存储重置之后。请选择如何处理:',
|
||||
'sync.autoSync.emptyVaultConflict.cloudLabel': '云端',
|
||||
'sync.autoSync.emptyVaultConflict.restore': '从云端恢复',
|
||||
'sync.autoSync.emptyVaultConflict.restoreDesc': '推荐 — 从云端备份恢复主机、密钥和代码片段',
|
||||
'sync.autoSync.emptyVaultConflict.keepEmpty': '保持为空',
|
||||
'sync.autoSync.emptyVaultConflict.keepEmptyDesc': '从头开始,使用空的主机库',
|
||||
'sync.autoSync.emptyVaultConflict.cloudSummary': '{hosts} 台主机,{keys} 个密钥,{snippets} 个代码片段,{notes} 条笔记,{proxyProfiles} 个代理',
|
||||
'sync.autoSync.emptyVaultManual': '无法同步:本地 vault 为空。请先从本地备份恢复,或在同步面板里使用"强制推送"。',
|
||||
|
||||
'sync.blocked.title': '同步已暂停',
|
||||
'sync.blocked.reason.bulkShrink': '即将从云端删除 {baseCount} 条 {entityType} 中的 {lost} 条(缩减 {percent}%)。',
|
||||
'sync.blocked.reason.largeShrink': '即将从云端删除 {lost} 条 {entityType}。',
|
||||
'sync.blocked.detail': '通常是本地状态异常(钥匙串故障、数据加载不全)导致。请从本地备份恢复,如果确实要删这些条目请使用强制推送。',
|
||||
'sync.blocked.restoreButton': '从本地备份恢复',
|
||||
'sync.blocked.forcePushButton': '强制推送',
|
||||
|
||||
'sync.forcePush.title': '确认强制推送',
|
||||
'sync.forcePush.body': '你将从云端移除 {lost} 条 {entityType},此操作不可撤销。继续?',
|
||||
'sync.forcePush.confirm': '确认推送',
|
||||
'sync.forcePush.cancel': '取消',
|
||||
|
||||
'sync.entityType.hosts': '主机',
|
||||
'sync.entityType.keys': '密钥',
|
||||
'sync.entityType.identities': '身份',
|
||||
'sync.entityType.proxyProfiles': '代理配置',
|
||||
'sync.entityType.snippets': '代码片段',
|
||||
'sync.entityType.customGroups': '分组',
|
||||
'sync.entityType.snippetPackages': '片段包',
|
||||
'sync.entityType.knownHosts': '主机密钥记录',
|
||||
'sync.entityType.portForwardingRules': '端口转发规则',
|
||||
'sync.entityType.groupConfigs': '分组配置',
|
||||
|
||||
'sync.credentialsUnavailable': '当前设备无法解密部分已保存凭据。请先在本地重新输入凭据后再同步。',
|
||||
'time.never': '从未',
|
||||
'time.justNow': '刚刚',
|
||||
'time.minutesAgo': '{minutes} 分钟前',
|
||||
|
||||
// Vault navigation
|
||||
'vault.nav.hosts': '主机',
|
||||
'vault.nav.keychain': '钥匙串',
|
||||
'vault.nav.proxies': '代理',
|
||||
'vault.nav.portForwarding': '端口转发',
|
||||
'vault.nav.snippets': '脚本',
|
||||
'vault.nav.notes': '笔记',
|
||||
'vault.nav.knownHosts': '已知主机',
|
||||
'vault.nav.logs': '日志',
|
||||
|
||||
'proxyProfiles.action.add': '添加代理',
|
||||
'proxyProfiles.search.placeholder': '搜索代理…',
|
||||
'proxyProfiles.section.proxies': '代理',
|
||||
'proxyProfiles.count.items': '{count} 项',
|
||||
'proxyProfiles.empty.title': '暂无代理',
|
||||
'proxyProfiles.empty.desc': '创建可复用的 HTTP、SOCKS5 或命令代理,然后在主机详情里选择。',
|
||||
'proxyProfiles.usage': '已关联 {count} 处',
|
||||
'proxyProfiles.copyName': '{name} 副本',
|
||||
'proxyProfiles.panel.newTitle': '新建代理',
|
||||
'proxyProfiles.field.name': '代理名称',
|
||||
'proxyProfiles.error.required': '名称和代理详情不能为空。',
|
||||
'proxyProfiles.error.port': '端口必须在 1 到 65535 之间。',
|
||||
'proxyProfiles.viewMode': '代理显示方式',
|
||||
'proxyProfiles.delete.title': '删除代理?',
|
||||
'proxyProfiles.delete.desc': '删除 "{name}" 会同时从 {count} 个主机或分组设置中解除关联。',
|
||||
|
||||
'vault.groups.title': '分组',
|
||||
'vault.groups.total': '共 {count} 个',
|
||||
'vault.groups.hostsCount': '{count} 台主机',
|
||||
'vault.groups.newSubgroup': '新建子分组',
|
||||
'vault.groups.rename': '重命名分组',
|
||||
'vault.groups.unnamed': '未命名分组',
|
||||
'vault.groups.delete': '删除分组',
|
||||
'vault.groups.createSubfolder': '创建子分组',
|
||||
'vault.groups.createRoot': '创建根分组',
|
||||
'vault.groups.createDialog.desc': '创建新的分组用于组织主机。',
|
||||
'vault.groups.renameDialogTitle': '重命名分组',
|
||||
'vault.groups.renameDialog.desc': '重命名已有分组。',
|
||||
'vault.groups.deleteDialogTitle': '删除分组',
|
||||
'vault.groups.deleteDialog.desc': '这将永久删除该分组并将所有主机移动到根级别。',
|
||||
'vault.groups.deleteDialog.managedDesc': '这是一个托管的 SSH config 分组。删除后将同时删除所有主机并断开与源文件的连接。',
|
||||
'vault.groups.deleteDialog.deleteHosts': '同时删除该分组下的所有主机',
|
||||
'vault.groups.ungrouped': '未分组',
|
||||
'vault.groups.field.name': '分组名称',
|
||||
'vault.groups.placeholder.example': '例如:Production',
|
||||
'vault.groups.parentLabel': '父级',
|
||||
'vault.groups.pathLabel': '路径',
|
||||
'vault.groups.settings': '分组设置',
|
||||
'vault.groups.details': '分组详情',
|
||||
'vault.groups.details.general': '常规',
|
||||
'vault.groups.details.ssh': 'SSH',
|
||||
'vault.groups.details.telnet': 'Telnet',
|
||||
'vault.groups.details.advanced': '高级',
|
||||
'vault.groups.details.appearance': '外观',
|
||||
'vault.groups.details.mosh': 'Mosh',
|
||||
'vault.groups.details.parentGroup': '父分组',
|
||||
'vault.groups.details.none': '无',
|
||||
'vault.groups.details.inherited': '继承自分组',
|
||||
'vault.groups.details.addProtocol': '添加协议',
|
||||
'vault.groups.details.removeProtocol': '移除协议',
|
||||
'vault.groups.details.fontFamily': '字体',
|
||||
'vault.groups.details.fontSize': '字号',
|
||||
'vault.groups.errors.required': '分组名称不能为空。',
|
||||
'vault.groups.errors.invalidChars': "分组名称不能包含 '/' 或 '\\\\'.",
|
||||
'vault.groups.errors.duplicatePath': '该位置已存在同名分组。',
|
||||
|
||||
'vault.deleteConfirm.title': '删除“{name}”?',
|
||||
'vault.deleteConfirm.desc': '此操作不可撤销。',
|
||||
'vault.deleteConfirm.packageDesc': '这会删除脚本包,包内脚本会保留,并移出该脚本包。',
|
||||
'vault.deleteConfirm.noteGroupDesc': '这会删除文件夹,文件夹内笔记会保留,并移出该文件夹。',
|
||||
'vault.deleteConfirm.portForwardingDesc': '这会删除该端口转发规则。',
|
||||
|
||||
'vault.managedSource.unmanage': '取消托管',
|
||||
'vault.managedSource.unmanageSuccess': '已取消托管分组',
|
||||
|
||||
'vault.hosts.header.entries': '{count} 条',
|
||||
'vault.hosts.header.live': '{count} 个在线',
|
||||
|
||||
// Vault hosts header/actions
|
||||
'vault.hosts.search.placeholder': '查找主机或 ssh user@hostname / ssh -p 2222 user@hostname…',
|
||||
'vault.hosts.connect': '连接',
|
||||
'vault.view.grid': '网格',
|
||||
'vault.view.list': '列表',
|
||||
'vault.view.tree': '树形',
|
||||
'vault.tree.expandAll': '展开全部',
|
||||
'vault.tree.collapseAll': '折叠全部',
|
||||
'vault.hosts.newHost': '新建主机',
|
||||
'vault.hosts.newGroup': '新建分组',
|
||||
'vault.hosts.import': '导入',
|
||||
'vault.hosts.export': '导出',
|
||||
'vault.hosts.export.toast.success': '已导出 {count} 个主机到 CSV',
|
||||
'vault.hosts.export.toast.successWithSkipped': '已导出 {count} 个主机到 CSV(跳过 {skipped} 个不支持的主机)',
|
||||
'vault.hosts.export.toast.noHosts': '没有主机可导出',
|
||||
'vault.hosts.allHosts': '全部主机',
|
||||
'vault.hosts.pinned': '已置顶',
|
||||
'vault.hosts.recentlyConnected': '最近连接',
|
||||
'vault.hosts.pinToTop': '置顶',
|
||||
'vault.hosts.unpin': '取消置顶',
|
||||
'vault.hosts.copyCredentials': '复制账密信息',
|
||||
'vault.hosts.copyCredentials.toast.success': '账密信息已复制到剪贴板',
|
||||
'vault.hosts.copyCredentials.toast.noPassword': '该主机未保存密码',
|
||||
'vault.hosts.multiSelect': '多选',
|
||||
'vault.hosts.selected': '已选择 {count} 项',
|
||||
'vault.hosts.selectAll': '全选',
|
||||
'vault.hosts.deselectAll': '取消全选',
|
||||
'vault.hosts.deleteSelected': '删除 ({count})',
|
||||
'vault.hosts.deleteMultiple.success': '已删除 {count} 个主机',
|
||||
'vault.hosts.connectSelected': '连接 ({count})',
|
||||
'vault.hosts.connectMultiple.success': '正在连接 {count} 个主机',
|
||||
'vault.hosts.moveToGroup.success': '已将 {host} 移动到 {group}',
|
||||
'vault.hosts.errors.nameRequired': '主机名称不能为空。',
|
||||
'vault.hosts.empty.title': '设置你的主机',
|
||||
'vault.hosts.empty.desc': '保存主机以快速连接到你的服务器、虚拟机和容器。',
|
||||
'deepLink.ssh.invalid': '无效的 ssh:// 链接',
|
||||
'deepLink.telnet.invalid': '无效的 telnet:// 链接',
|
||||
'deepLink.jms.invalid': '无效的 JumpServer 链接',
|
||||
'deepLink.jms.unsupported': '不支持的 JumpServer 协议:{{protocol}}',
|
||||
|
||||
// Vault import
|
||||
'vault.import.title': '添加数据到你的 Vault',
|
||||
'vault.import.desc': '从常见工具迁移连接信息。选择一种格式开始导入。',
|
||||
'vault.import.chooseFormat': '选择文件格式',
|
||||
'vault.import.csv.tip': '批量导入:可使用 CSV 模板填写后导入。',
|
||||
'vault.import.csv.downloadTemplate': '下载 CSV 模板',
|
||||
'vault.import.toast.start': '正在从 {format} 导入...',
|
||||
'vault.import.toast.completedTitle': '导入完成',
|
||||
'vault.import.toast.failedTitle': '导入失败',
|
||||
'vault.import.toast.noEntries': '{format} 文件中没有可导入的条目。',
|
||||
'vault.import.toast.noNewHosts': '从 {format} 没有导入到新的主机。',
|
||||
'vault.import.toast.summary': '已导入 {count} 个主机(跳过 {skipped},重复 {duplicates})。',
|
||||
'vault.import.toast.firstIssue': '首个问题:{issue}',
|
||||
'vault.import.sshConfig.chooseMode': '选择如何导入你的 SSH config 文件。',
|
||||
'vault.import.sshConfig.modeQuestion': '你希望如何导入?',
|
||||
'vault.import.sshConfig.importOnly': '仅导入',
|
||||
'vault.import.sshConfig.importOnlyDesc': '一次性导入,修改不会同步回文件。',
|
||||
'vault.import.sshConfig.managed': '托管同步',
|
||||
'vault.import.sshConfig.managedDesc': '保持同步,修改会自动保存回文件。',
|
||||
'vault.import.sshConfig.managedGroup': 'ssh config',
|
||||
'vault.import.sshConfig.managedSuccess': '已导入 {count} 个主机,文件已托管。',
|
||||
'vault.import.sshConfig.alreadyManaged': '该文件已被托管。',
|
||||
'vault.import.sshConfig.alreadyManagedDesc': '该文件已在分组 "{group}" 下托管。如需重新导入,请先移除现有的托管源。',
|
||||
'vault.import.sshConfig.noFilePath': '无法托管此文件。',
|
||||
'vault.import.sshConfig.noFilePathDesc': '无法确定文件路径。托管同步需要访问文件系统。',
|
||||
|
||||
// Known Hosts
|
||||
'knownHosts.search.placeholder': '搜索已知主机...',
|
||||
'knownHosts.action.scanSystem': '扫描系统',
|
||||
'knownHosts.action.importFile': '导入文件',
|
||||
'knownHosts.action.browseFile': '浏览文件',
|
||||
'knownHosts.empty.title': '暂无已知主机',
|
||||
'knownHosts.empty.desc':
|
||||
'Known Hosts 是你之前连接过的 SSH server。导入系统的 known_hosts 文件以开始。',
|
||||
'knownHosts.results.showingLimited': '显示 {shown}/{total} 个主机。使用搜索查找特定主机。',
|
||||
'knownHosts.toast.scanUnavailable': '当前平台无法扫描系统 known_hosts。',
|
||||
'knownHosts.toast.scanNoFile': '未找到系统 known_hosts 文件。',
|
||||
'knownHosts.toast.scanNoEntries': 'known_hosts 中没有可用条目。',
|
||||
'knownHosts.toast.scanImported': '已导入 {count} 个新主机。',
|
||||
'knownHosts.toast.scanNoNew': '没有发现新的主机。',
|
||||
'knownHosts.toast.scanFailed': '扫描系统 known_hosts 失败。',
|
||||
|
||||
// Port Forwarding
|
||||
'pf.empty.title': '配置端口转发规则',
|
||||
'pf.empty.desc': '保存端口转发规则,用于访问数据库、Web 应用等服务。',
|
||||
'pf.title': '端口转发规则',
|
||||
'pf.rulesCount': '{count} 条规则',
|
||||
'pf.wizard.editTitle': '编辑端口转发规则',
|
||||
'pf.wizard.newTitle': '新建端口转发规则',
|
||||
'pf.wizard.saveChanges': '保存修改',
|
||||
'pf.wizard.done': '完成',
|
||||
'pf.wizard.continue': '继续',
|
||||
'pf.wizard.cancel': '取消',
|
||||
'pf.wizard.skipWizard': '跳过向导',
|
||||
'pf.error.hostNotFound': '未找到主机',
|
||||
'pf.toast.titleWithLabel': '端口转发规则: {label}',
|
||||
'pf.type.local': '本地转发',
|
||||
'pf.type.remote': '远程转发',
|
||||
'pf.type.dynamic': '动态转发',
|
||||
'pf.type.menu.local': '本地转发',
|
||||
'pf.type.menu.remote': '远程转发',
|
||||
'pf.type.menu.dynamic': '动态转发',
|
||||
'pf.type.local.desc': '本地转发让你像访问本地一样访问远程服务端口。',
|
||||
'pf.type.remote.desc': '远程转发在远端开启端口,并将连接转发到本地(当前)主机。',
|
||||
'pf.type.dynamic.desc': '动态转发将 Netcatty 作为 SOCKS 代理使用。',
|
||||
'pf.wizard.type.title': '选择端口转发类型:',
|
||||
'pf.wizard.localConfig.title': '设置本地端口与绑定地址:',
|
||||
'pf.wizard.localConfig.desc': '该端口会在本地(当前设备)打开,并接收流量。',
|
||||
'pf.wizard.localConfig.localPort': '本地端口 *',
|
||||
'pf.wizard.bindAddress': '绑定地址',
|
||||
'pf.wizard.remoteHost.title': '选择远端主机:',
|
||||
'pf.wizard.remoteHost.desc': '选择要打开端口的远端主机。该端口的流量将转发到目标地址。',
|
||||
'pf.wizard.remoteConfig.title': '设置端口与绑定地址:',
|
||||
'pf.wizard.remoteConfig.desc': '将从所选主机的指定端口与网卡地址转发流量。',
|
||||
'pf.wizard.remoteConfig.remotePort': '远端端口 *',
|
||||
'pf.wizard.destination.title': '设置目标地址:',
|
||||
'pf.wizard.destination.desc.local': '输入你希望通过 tunnel 访问的远端目标地址。',
|
||||
'pf.wizard.destination.desc.remote': '要转发流量到的目标地址与端口。',
|
||||
'pf.wizard.destination.address': '目标地址 *',
|
||||
'pf.wizard.destination.addressPlaceholder': '例如:127.0.0.1 或 192.168.1.100',
|
||||
'pf.wizard.destination.port': '目标端口 *',
|
||||
'pf.wizard.sshServer.title': '选择 SSH server:',
|
||||
'pf.wizard.sshServer.desc.dynamic': '选择作为 SOCKS proxy 的 SSH server。',
|
||||
'pf.wizard.sshServer.desc.default': '选择用于将流量 tunnel 到目标地址的 SSH server。',
|
||||
'pf.wizard.label.title': '设置 Label:',
|
||||
'pf.wizard.label.placeholder.dynamic': '例如:SOCKS Proxy',
|
||||
'pf.wizard.label.placeholder.default': '例如:MySQL Production',
|
||||
'pf.wizard.label.placeholder.remoteRule': '例如:Remote Rule',
|
||||
'pf.wizard.placeholders.portExample': '例如:{port}',
|
||||
|
||||
// SFTP
|
||||
'sftp.newFolder': '新建文件夹',
|
||||
'sftp.newFile': '新建文件',
|
||||
'sftp.filter': '筛选',
|
||||
'sftp.filter.placeholder': '按文件名筛选...',
|
||||
'sftp.bookmark.add': '收藏此路径',
|
||||
'sftp.bookmark.remove': '取消收藏',
|
||||
'sftp.bookmark.list': '收藏路径',
|
||||
'sftp.bookmark.addGlobal': '+全局',
|
||||
'sftp.bookmark.addGlobalTooltip': '保存为全局收藏(所有主机共享)',
|
||||
'sftp.bookmark.empty': '暂无收藏路径',
|
||||
'sftp.columns.name': '名称',
|
||||
'sftp.columns.modified': '修改时间',
|
||||
'sftp.columns.size': '大小',
|
||||
'sftp.columns.kind': '类型',
|
||||
'sftp.columns.actions': '操作',
|
||||
'sftp.emptyDirectory': '空目录',
|
||||
'sftp.nav.up': '返回上层',
|
||||
'sftp.nav.home': '返回主目录',
|
||||
'sftp.nav.refresh': '刷新',
|
||||
'sftp.upload': '上传',
|
||||
'sftp.uploadFiles': '上传文件',
|
||||
'sftp.uploadFolder': '上传文件夹',
|
||||
'sftp.dragDropToUpload': '拖拽文件到这里上传',
|
||||
'sftp.retry': '重试',
|
||||
'sftp.context.open': '打开',
|
||||
'sftp.context.navigateTo': '跳转到这里',
|
||||
'sftp.context.moveTo': '移动到...',
|
||||
'sftp.context.moveToParent': '移动到上级目录',
|
||||
'sftp.moveTo.title': '移动到目录',
|
||||
'sftp.moveTo.placeholder': '输入目标目录路径',
|
||||
'sftp.moveTo.confirm': '移动',
|
||||
'sftp.moveTo.pathNotFound': '目录不存在或无法访问',
|
||||
'sftp.context.download': '下载',
|
||||
'sftp.context.copyToOtherPane': '复制到另一侧',
|
||||
'sftp.copyCurrentPath': '复制当前路径',
|
||||
'sftp.copyCurrentPath.success': '已复制当前路径',
|
||||
'sftp.copyCurrentPath.error': '无法复制当前路径',
|
||||
'sftp.viewMode.label': '视图模式',
|
||||
'sftp.viewMode.list': '列表视图',
|
||||
'sftp.viewMode.tree': '树形视图',
|
||||
'sftp.viewMode.switchToList': '切换到列表视图',
|
||||
'sftp.viewMode.switchToTree': '切换到树形视图',
|
||||
'sftp.tree.loadError': '加载目录失败',
|
||||
'sftp.tree.loading': '加载中...',
|
||||
'sftp.kind.folder': '文件夹',
|
||||
'sftp.context.rename': '重命名',
|
||||
'sftp.context.permissions': '权限',
|
||||
'sftp.context.delete': '删除',
|
||||
'sftp.context.refresh': '刷新',
|
||||
'sftp.context.uploadFiles': '上传文件...',
|
||||
'sftp.context.uploadFilesHere': '上传文件到这里...',
|
||||
'sftp.context.uploadFolder': '上传文件夹...',
|
||||
'sftp.context.uploadFolderHere': '上传文件夹到这里...',
|
||||
'sftp.context.downloadSelected': '下载选中项({count})',
|
||||
'sftp.context.deleteSelected': '删除选中项({count})',
|
||||
'sftp.dropFilesHere': '拖拽文件到这里',
|
||||
'sftp.itemsCount': '{count} 个项目',
|
||||
'sftp.selectedCount': '已选 {count} 个',
|
||||
'sftp.path.doubleClickToEdit': '双击编辑路径',
|
||||
'sftp.showHiddenPaths': '隐藏的路径',
|
||||
'sftp.task.waiting': '等待中...',
|
||||
'sftp.transfer.preparing': '准备中...',
|
||||
'sftp.status.loading': '加载中...',
|
||||
'sftp.status.uploading': '上传中...',
|
||||
'sftp.status.ready': '就绪',
|
||||
'sftp.transfers': '传输',
|
||||
'sftp.transfers.active': '{count} 个进行中',
|
||||
'sftp.transfers.clearCompleted': '清除已完成',
|
||||
'sftp.transfers.calculatingTotal': '正在统计总大小...',
|
||||
'sftp.transfers.filesCount': '{count} 个文件',
|
||||
'sftp.transfers.filesProgress': '{current}/{total} 个文件',
|
||||
'sftp.transfers.expandChildren': '展开文件',
|
||||
'sftp.transfers.collapseChildren': '收起文件',
|
||||
'sftp.transfers.expandChildList': '展开详情',
|
||||
'sftp.transfers.collapseChildList': '收起',
|
||||
'sftp.transfers.retryAction': '重试',
|
||||
'sftp.transfers.dismissAction': '移除',
|
||||
'sftp.transfers.openTargetFolder': '打开目标目录',
|
||||
'sftp.transfers.openTargetFolderError': '无法打开目标目录',
|
||||
'sftp.transfers.copyTargetPath': '复制目标路径',
|
||||
'sftp.transfers.copyTargetPathSuccess': '已复制目标路径',
|
||||
'sftp.transfers.copyTargetPathError': '无法复制目标路径',
|
||||
'sftp.transfers.resizeNameColumn': '调整文件名列宽',
|
||||
'sftp.transfers.dragToResize': '拖拽调整高度',
|
||||
'sftp.goUp': '上一级',
|
||||
'sftp.goToTerminalCwd': '定位到终端当前目录',
|
||||
'sftp.followTerminalCwd': '追随终端目录',
|
||||
'sftp.followTerminalCwd.enable': '开启追随终端目录',
|
||||
'sftp.followTerminalCwd.disable': '关闭追随终端目录',
|
||||
'sftp.encoding.label': '文件名编码',
|
||||
'sftp.encoding.auto': '自动',
|
||||
'sftp.encoding.utf8': 'UTF-8',
|
||||
'sftp.encoding.gb18030': 'GB18030',
|
||||
'sftp.goHome': '返回主目录',
|
||||
'sftp.folderName': '文件夹名称',
|
||||
'sftp.folderName.placeholder': '输入文件夹名称',
|
||||
'sftp.fileName': '文件名称',
|
||||
'sftp.fileName.placeholder': '输入文件名称',
|
||||
'sftp.prompt.newFolderName': '新建文件夹名称?',
|
||||
'sftp.rename.title': '重命名',
|
||||
'sftp.rename.newName': '新名称',
|
||||
'sftp.rename.placeholder': '输入新名称',
|
||||
'sftp.confirm.deleteOne': '删除 "{name}"?',
|
||||
'sftp.deleteConfirm.single': '删除 "{name}"?',
|
||||
'sftp.deleteConfirm.title': '删除 {count} 个项目?',
|
||||
'sftp.deleteConfirm.desc': '此操作不可撤销,将删除以下内容:',
|
||||
'sftp.deleteConfirm.descSingle': '此操作不可撤销。',
|
||||
'sftp.deleteConfirm.host': '主机',
|
||||
'sftp.deleteConfirm.path': '路径',
|
||||
'sftp.error.loadFailed': '加载目录失败',
|
||||
'sftp.error.downloadFailed': '下载失败',
|
||||
'sftp.error.uploadFailed': '上传失败',
|
||||
'sftp.error.deleteFailed': '删除失败',
|
||||
'sftp.error.createFolderFailed': '创建文件夹失败',
|
||||
'sftp.error.createFileFailed': '创建文件失败',
|
||||
'sftp.error.invalidFileName': '文件名包含非法字符:{chars}',
|
||||
'sftp.error.reservedName': '此文件名是系统保留名称',
|
||||
'sftp.overwrite.title': '文件已存在',
|
||||
'sftp.overwrite.desc': '名为"{name}"的文件已存在。是否要替换它?',
|
||||
'sftp.overwrite.confirm': '替换',
|
||||
'sftp.error.renameFailed': '重命名失败',
|
||||
'sftp.picker.title': '选择主机',
|
||||
'sftp.picker.desc': '为{side}窗格选择主机',
|
||||
'sftp.picker.searchPlaceholder': '搜索主机...',
|
||||
'sftp.picker.local.title': '本地文件系统',
|
||||
'sftp.picker.local.desc': '浏览本地文件',
|
||||
'sftp.picker.local.badge': '本地',
|
||||
'sftp.picker.connected.section': '已连接',
|
||||
'sftp.picker.connected.badge': '已连接',
|
||||
'sftp.picker.connected.connecting': '连接中',
|
||||
'sftp.picker.noMatch': '没有匹配的主机',
|
||||
'sftp.permissions.title': '编辑权限',
|
||||
'sftp.permissions.owner': '所有者',
|
||||
'sftp.permissions.group': '群组',
|
||||
'sftp.permissions.others': '其他',
|
||||
'sftp.permissions.octal': '八进制',
|
||||
'sftp.permissions.symbolic': '符号',
|
||||
'sftp.permissions.success': '权限已更新',
|
||||
'sftp.permissions.failed': '权限更新失败',
|
||||
|
||||
// Quick Switcher
|
||||
'qs.search.placeholder': '搜索主机或标签页',
|
||||
'qs.jumpTo': '跳转到',
|
||||
'qs.localTerminal': '本地终端',
|
||||
'qs.localShells': '本地 Shell',
|
||||
'qs.default': '默认',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
export const zhCNScriptsMessages = {
|
||||
'scripts.meta.name': '名称',
|
||||
'scripts.meta.language': '语言',
|
||||
'scripts.meta.description': '描述',
|
||||
'scripts.meta.descriptionPlaceholder': '可选,脚本的简要说明',
|
||||
'scripts.meta.trigger': '触发方式',
|
||||
'scripts.meta.triggerPattern': '输出匹配(正则)',
|
||||
'scripts.meta.code': '脚本代码',
|
||||
'scripts.trigger.manual': '手动运行',
|
||||
'scripts.trigger.onConnect': '连接时运行',
|
||||
'scripts.trigger.onOutput': '输出匹配时运行',
|
||||
'scripts.trigger.onOutputHint': '仅在服务端输出匹配正则时触发(会忽略本地按键回显)。未配置目标主机时,对当前已连接会话生效;配置目标后仅监听对应主机。进入备用屏幕的应用(如 vim、htop)期间不会触发。本 session 有其他脚本运行期间会抑制;脚本结束后再检查 buffer。流程内等待普通文本用 nct.screen.waitForText,正则用 nct.screen.waitForRegex。',
|
||||
'scripts.actions.save': '保存',
|
||||
'scripts.actions.runNow': '立即运行',
|
||||
'scripts.actions.openEditor': '弹窗编辑',
|
||||
'scripts.actions.openEditorHint': '在更大的窗口中编辑脚本',
|
||||
'scripts.editor.modalTitle': '脚本编辑器',
|
||||
'scripts.editor.modalSubtitle': '在更大的工作区中编辑脚本元数据和代码。',
|
||||
'scripts.editor.lineCount': '{count} 行',
|
||||
'scripts.editor.resize': '调整编辑器高度',
|
||||
'scripts.targets.hint': '自动触发作用于所选主机;勾选「对所有主机生效」则作用于全部可连接主机。',
|
||||
'scripts.targets.connectOrderHint': '连接脚本的执行顺序请在主机详情 → 自动化 中调整。',
|
||||
'scripts.actions.runNowHint': '在所选目标主机上运行;勾选「对所有主机生效」时在所有可连接主机上运行。',
|
||||
'scripts.actions.runParallel': '在所有标签页并行运行',
|
||||
'scripts.actions.runSequential': '在所有标签页顺序运行',
|
||||
'scripts.actions.skippedConnectingSessions': '有 {count} 个标签仍在连接中,已跳过',
|
||||
'scripts.actions.noRunnableHosts': '没有与脚本目标匹配的可连接主机',
|
||||
'scripts.sidePanel.library': '脚本库',
|
||||
'scripts.sidePanel.running': '运行中',
|
||||
'scripts.sidePanel.newScript': '新建脚本',
|
||||
'scripts.running.empty': '当前会话没有正在运行的脚本。',
|
||||
'scripts.running.unnamed': '未命名脚本',
|
||||
'scripts.running.status.running': '运行中',
|
||||
'scripts.running.status.paused': '已暂停',
|
||||
'scripts.running.status.completed': '已完成',
|
||||
'scripts.running.status.failed': '失败',
|
||||
'scripts.running.waitingFor': '等待 {pattern}',
|
||||
'scripts.running.waitingForLabel': '等待',
|
||||
'scripts.running.waitingForShellPrompt': 'Shell 提示符(# 或 $)',
|
||||
'scripts.running.lastSent': '已发送:{command}',
|
||||
'scripts.recording.start': '开始录制',
|
||||
'scripts.recording.active': '停止录制',
|
||||
'scripts.recording.startHint': '在当前终端会话中录制操作,自动生成 nct 脚本代码',
|
||||
'scripts.recording.unavailableHint': '此入口不支持录制,请用终端右侧脚本侧栏',
|
||||
'scripts.recording.activeHint': '正在录制当前终端。在终端中正常输入命令;点「停止录制」或终端工具栏的 REC 结束并保存。',
|
||||
'scripts.recording.started': '已开始录制,请在终端中操作',
|
||||
'scripts.recording.noSession': '请先连接终端(单机标签或工作区均可)',
|
||||
'scripts.recording.alreadyActive': '已有其他终端正在录制',
|
||||
'scripts.recording.stop': '停止录制',
|
||||
'scripts.recording.pause': '暂停录制',
|
||||
'scripts.recording.resume': '继续录制',
|
||||
'scripts.recording.saveTitle': '保存录制的脚本',
|
||||
'scripts.recording.namePlaceholder': '脚本名称',
|
||||
'scripts.recording.packagePlaceholder': '保存到文件夹',
|
||||
'scripts.recording.rootPackage': '根目录',
|
||||
'scripts.recording.save': '保存',
|
||||
'scripts.recording.saveAndEdit': '保存并编辑',
|
||||
'scripts.recording.helpTitle': '如何录制脚本',
|
||||
'scripts.recording.helpIntro': '录制会把你在终端里的操作自动转成可重复运行的自动化脚本,适合部署、巡检等重复任务。',
|
||||
'scripts.recording.helpStep1': '连接主机。单机终端标签或工作区里的终端都可以,打开右侧脚本侧栏即可。',
|
||||
'scripts.recording.helpStep2': '点击「开始录制」。终端工具栏出现红色 REC 表示已开始。',
|
||||
'scripts.recording.helpStep3': '在终端里像平时一样输入命令,按 Enter 执行。每次回车都会记为一步。',
|
||||
'scripts.recording.helpStep4': '全部操作完成后,点击「停止录制」,或点终端工具栏 REC 旁的停止按钮。',
|
||||
'scripts.recording.helpStep5': '填写脚本名称并保存。需要微调时可选「保存并编辑」,在脚本编辑器里修改生成的代码。',
|
||||
'scripts.recording.helpTipsTitle': '小提示',
|
||||
'scripts.recording.helpTip1': '两次操作之间停顿超过 1 秒,会自动加入等待时间,避免回放太快。',
|
||||
'scripts.recording.helpTip2': '每条命令后会等待 shell 提示符(如 $ 或 #)出现,再记录下一步,减少乱序。',
|
||||
'scripts.recording.helpTip3': '密码输入会被标记为敏感内容,不会明文写入脚本。',
|
||||
'scripts.recording.helpTip4': 'vim、交互式菜单等复杂操作,建议先录主干命令,再在编辑器里手动补充。',
|
||||
'scripts.dialog.title': '脚本',
|
||||
'scripts.dialog.ok': '确定',
|
||||
'scripts.dialog.required': '必填',
|
||||
'scripts.dialog.numberInvalid': '请输入有效数字',
|
||||
'scripts.dialog.numberMin': '不能小于 {min}',
|
||||
'scripts.dialog.numberMax': '不能大于 {max}',
|
||||
'scripts.dialog.numberStep': '步进必须为 {step} 的倍数',
|
||||
'scripts.dialog.waitForTimeoutTitle': '等待超时',
|
||||
'scripts.dialog.retry': '重试',
|
||||
'scripts.dialog.skip': '跳过',
|
||||
'scripts.dialog.abort': '终止',
|
||||
'scripts.running.stepProgress': '步骤 {current} / {total}',
|
||||
'scripts.running.determinateProgress': '{label} {current}/{total}',
|
||||
'scripts.running.progressFallback': '进度',
|
||||
'scripts.running.operationsCount': '已执行 {count} 个操作',
|
||||
'scripts.running.opsPrefix': '已执行 ',
|
||||
'scripts.running.opsSuffix': ' 个操作',
|
||||
'scripts.running.elapsedLabel': '耗时',
|
||||
'scripts.running.lastSentLabel': '已发送:',
|
||||
'scripts.running.elapsed': '耗时 {elapsed}',
|
||||
'scripts.running.completedSummary': '已完成 · 共 {count} 个操作 · {elapsed}',
|
||||
'scripts.running.dismissHint': '点击关闭',
|
||||
'scripts.running.dismiss': '关闭',
|
||||
'scripts.running.viewLogs': '查看日志',
|
||||
'scripts.running.logTitle': '{name} · 运行日志',
|
||||
'scripts.running.logEmpty': '暂无日志输出',
|
||||
'scripts.running.pause': '暂停',
|
||||
'scripts.running.resume': '继续',
|
||||
'scripts.running.stop': '停止',
|
||||
'scripts.recording.saved': '脚本已保存',
|
||||
'scripts.recording.savedNamed': '已保存「{name}」',
|
||||
'scripts.observer.blocked': 'Observer 模式禁止执行会写入终端的脚本。',
|
||||
'vault.section.scripts': '脚本',
|
||||
'vault.nav.scripts': '脚本',
|
||||
'snippets.action.newScript': '新建自动化脚本',
|
||||
'hostDetails.section.automation': '自动化',
|
||||
'hostDetails.automation.loginScript': '登录脚本',
|
||||
'hostDetails.automation.loginScriptPlaceholder': '选择脚本',
|
||||
'hostDetails.automation.none': '无',
|
||||
'hostDetails.automation.outputTriggers': '输出触发器',
|
||||
'hostDetails.automation.addTrigger': '添加触发器',
|
||||
'hostDetails.automation.triggerPatternPlaceholder': '正则表达式',
|
||||
'hostDetails.automation.linkedScripts': '已关联脚本',
|
||||
'hostDetails.automation.linkedScriptsEmpty': '尚未关联任何脚本。',
|
||||
'hostDetails.automation.linkScriptPlaceholder': '关联已有脚本…',
|
||||
'hostDetails.automation.unlink': '取消关联',
|
||||
'hostDetails.automation.equivalenceHint': '此处关联会将本主机加入 Scripts 中该脚本的目标列表,两侧视图同步。',
|
||||
'hostDetails.automation.queueHint': '连接本主机时按顺序运行脚本。全局脚本最先执行,再执行下方队列。',
|
||||
'hostDetails.automation.globalScripts': '全局连接脚本',
|
||||
'hostDetails.automation.globalScriptsHint': '每次连接时最先运行。顺序在脚本库排序中调整。',
|
||||
'hostDetails.automation.connectQueue': '本主机运行队列',
|
||||
'hostDetails.automation.connectQueueEmpty': '尚未为本主机配置连接脚本队列。',
|
||||
'hostDetails.automation.addToQueuePlaceholder': '添加脚本到队列…',
|
||||
'hostDetails.automation.moveUp': '上移',
|
||||
'hostDetails.automation.moveDown': '下移',
|
||||
'hostDetails.automation.removeFromQueue': '从队列移除',
|
||||
'hostDetails.automation.dragHandle': '拖拽排序',
|
||||
'hostDetails.automation.queueDragHint': '拖动条目即可调整连接时的运行顺序。',
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhCnSystemManagerMessages: Messages = {
|
||||
'terminal.layer.system': '系统',
|
||||
|
||||
'systemManager.noSession': '没有活动的终端会话。',
|
||||
'systemManager.notConnected': '请先连接到主机以管理进程与服务。',
|
||||
'systemManager.empty': '暂无数据。',
|
||||
'systemManager.tabs.overview': '概览',
|
||||
'systemManager.tabs.processes': '进程',
|
||||
'systemManager.tabs.tmux': 'tmux',
|
||||
'systemManager.tabs.docker': 'Docker',
|
||||
'systemManager.popup.loading': '正在打开终端…',
|
||||
'systemManager.popup.startupFailed': '启动命令未成功。请确认目标仍然可用后重试。',
|
||||
|
||||
'systemManager.errors.loadProcesses': '加载进程列表失败',
|
||||
'systemManager.errors.loadTmux': '加载 tmux 会话失败',
|
||||
'systemManager.errors.loadTmuxWindows': '加载 tmux 窗口失败',
|
||||
'systemManager.errors.loadTmuxPanes': '加载 tmux 面板失败',
|
||||
'systemManager.errors.loadTmuxClients': '加载 tmux 客户端失败',
|
||||
'systemManager.errors.actionFailed': '操作失败',
|
||||
'systemManager.errors.loadDocker': '加载容器列表失败',
|
||||
'systemManager.errors.loadDockerStats': '加载容器性能数据失败',
|
||||
'systemManager.errors.loadDockerImages': '加载镜像列表失败',
|
||||
'systemManager.errors.loadOverview': '加载系统概览失败',
|
||||
'systemManager.errors.sshChannelUnavailable': '服务器拒绝打开新的执行通道。请稍后重试,或重新连接当前主机。',
|
||||
|
||||
'systemManager.overview.empty': '暂无系统概览数据。',
|
||||
'systemManager.overview.loading': '正在加载系统概览…',
|
||||
'systemManager.overview.memory': '内存',
|
||||
'systemManager.overview.disk': '磁盘',
|
||||
'systemManager.overview.network': '网络',
|
||||
'systemManager.overview.rx': '下行',
|
||||
'systemManager.overview.tx': '上行',
|
||||
'systemManager.overview.cores': '{{count}} 核',
|
||||
'systemManager.overview.load': '负载',
|
||||
'systemManager.overview.uptime': '运行时间',
|
||||
'systemManager.overview.duration.daysHours': '{{days}}天 {{hours}}小时',
|
||||
'systemManager.overview.duration.hoursMinutes': '{{hours}}小时 {{minutes}}分钟',
|
||||
'systemManager.overview.duration.minutes': '{{minutes}}分钟',
|
||||
'systemManager.overview.system': '系统',
|
||||
'systemManager.overview.kernel': '内核',
|
||||
'systemManager.overview.swap': '交换区',
|
||||
'systemManager.overview.latency': '延迟',
|
||||
'systemManager.overview.cpuCores': 'CPU 核心',
|
||||
'systemManager.overview.disks': '磁盘',
|
||||
'systemManager.overview.interfaces': '网络接口',
|
||||
'systemManager.overview.topProcesses': '内存占用最高进程',
|
||||
'systemManager.overview.noData': '暂无数据',
|
||||
'systemManager.overview.noDisks': '暂无磁盘数据',
|
||||
'systemManager.overview.noInterfaces': '暂无网卡数据',
|
||||
'systemManager.overview.noTopProcesses': '暂无进程数据',
|
||||
|
||||
'systemManager.processes.search': '搜索进程…',
|
||||
'systemManager.processes.command': '命令',
|
||||
'systemManager.processes.user': '用户',
|
||||
'systemManager.processes.term': '终止',
|
||||
'systemManager.processes.kill': '强杀',
|
||||
'systemManager.processes.stop': '暂停 (SIGSTOP)',
|
||||
'systemManager.processes.cont': '继续 (SIGCONT)',
|
||||
'systemManager.processes.hup': '挂断 (SIGHUP)',
|
||||
'systemManager.processes.renice': '调整优先级',
|
||||
'systemManager.processes.renicePrompt': 'Nice 值 (-20 到 19)',
|
||||
'systemManager.processes.reniceInvalid': 'Nice 值必须在 -20 到 19 之间',
|
||||
'systemManager.processes.confirmKill': '向进程 {{pid}} 发送 SIGKILL?',
|
||||
'systemManager.processes.confirmSignal': '向进程 {{pid}} 发送 SIG{{signal}}?',
|
||||
'systemManager.processes.filter.all': '全部',
|
||||
'systemManager.processes.filter.running': '运行中',
|
||||
'systemManager.processes.ppid': '父进程 PID',
|
||||
'systemManager.processes.rss': '物理内存',
|
||||
'systemManager.processes.vsz': '虚拟内存',
|
||||
'systemManager.processes.elapsed': '运行时长',
|
||||
'systemManager.processes.stat': '状态',
|
||||
'systemManager.processes.meta': '{{count}} 个进程',
|
||||
'systemManager.processes.loading': '正在加载进程…',
|
||||
'systemManager.processes.loadingMore': '正在显示更多进程…',
|
||||
'systemManager.processes.state.running': '运行中',
|
||||
'systemManager.processes.state.sleeping': '睡眠',
|
||||
'systemManager.processes.state.stopped': '已暂停',
|
||||
'systemManager.processes.state.zombie': '僵尸',
|
||||
'systemManager.processes.sort.cpu': 'CPU',
|
||||
'systemManager.processes.sort.mem': '内存',
|
||||
'systemManager.processes.sort.pid': 'PID',
|
||||
'systemManager.processes.sort.command': '命令',
|
||||
'systemManager.processes.sort.user': '用户',
|
||||
|
||||
'systemManager.common.dismiss': '关闭',
|
||||
'systemManager.common.checkingAvailability': '正在检查可用状态…',
|
||||
'systemManager.common.loading': '正在加载…',
|
||||
'systemManager.common.loadingDetails': '正在加载详情…',
|
||||
'systemManager.common.loadingStats': '正在加载性能数据…',
|
||||
|
||||
'systemManager.tmux.new': '新建',
|
||||
'systemManager.tmux.search': '搜索会话…',
|
||||
'systemManager.tmux.newSessionTitle': '新建 tmux 会话',
|
||||
'systemManager.tmux.newSessionDesc': '为会话命名,并可选在启动时执行脚本。',
|
||||
'systemManager.tmux.newSessionTabCustom': '自定义命令',
|
||||
'systemManager.tmux.newSessionTabSnippet': '从代码片段',
|
||||
'systemManager.tmux.pickSnippet': '从代码片段选择',
|
||||
'systemManager.tmux.pickSnippetEmpty': '暂无代码片段,可在脚本侧栏或仓库中添加。',
|
||||
'systemManager.tmux.selectedSnippet': '已选片段:{{label}}',
|
||||
'systemManager.tmux.newSessionName': '会话名称',
|
||||
'systemManager.tmux.newSessionCommand': '启动命令',
|
||||
'systemManager.tmux.newSessionCommandPlaceholder': '例如 htop 或 npm run dev(可选)',
|
||||
'systemManager.tmux.newSessionCommandHint': '留空则创建默认 shell 会话。',
|
||||
'systemManager.tmux.creating': '创建中…',
|
||||
'systemManager.tmux.newSessionPlaceholder': 'my-session',
|
||||
'systemManager.tmux.newSessionRequired': '请先输入会话名称',
|
||||
'systemManager.tmux.empty': '没有 tmux 会话',
|
||||
'systemManager.tmux.attach': '附加',
|
||||
'systemManager.tmux.attached': '已附加',
|
||||
'systemManager.tmux.detached': '未附加',
|
||||
'systemManager.tmux.windows': '{{count}} 个窗口',
|
||||
'systemManager.tmux.created': '创建时间',
|
||||
'systemManager.tmux.activity': '活动时间',
|
||||
'systemManager.tmux.rename': '重命名',
|
||||
'systemManager.tmux.detach': '全部分离',
|
||||
'systemManager.tmux.killSession': '结束会话',
|
||||
'systemManager.tmux.killServer': '结束 tmux 服务',
|
||||
'systemManager.tmux.loadingDetails': '正在加载详情…',
|
||||
'systemManager.tmux.clients': '已附加客户端',
|
||||
'systemManager.tmux.windowList': '窗口',
|
||||
'systemManager.tmux.newWindow': '新建窗口',
|
||||
'systemManager.tmux.newWindowPlaceholder': '窗口名称(可选)',
|
||||
'systemManager.tmux.noWindows': '没有窗口',
|
||||
'systemManager.tmux.unavailable': '此主机未检测到 tmux',
|
||||
'systemManager.docker.unavailable': '此主机未检测到 Docker',
|
||||
'systemManager.tmux.windowsMismatch': '会话显示有 {{count}} 个窗口,但 list-windows 未返回任何窗口',
|
||||
'systemManager.tmux.lastCommand': '最后执行的命令:{{command}}',
|
||||
'systemManager.tmux.noPanes': '没有面板',
|
||||
'systemManager.tmux.panes': '{{count}} 个面板',
|
||||
'systemManager.tmux.active': '当前',
|
||||
'systemManager.tmux.unnamedWindow': '未命名窗口',
|
||||
'systemManager.tmux.unnamedPane': '未命名面板',
|
||||
'systemManager.tmux.attachWindow': '附加到窗口',
|
||||
'systemManager.tmux.selectWindow': '选中窗口',
|
||||
'systemManager.tmux.killWindow': '关闭窗口',
|
||||
'systemManager.tmux.killPane': '关闭面板',
|
||||
'systemManager.tmux.splitHorizontal': '水平分屏',
|
||||
'systemManager.tmux.splitVertical': '垂直分屏',
|
||||
'systemManager.tmux.sendKeys': '发送按键',
|
||||
'systemManager.tmux.sendKeysTo': '向窗口 {{window}} 面板 {{pane}} 发送按键',
|
||||
'systemManager.tmux.sendKeysPlaceholder': '命令或文本…',
|
||||
'systemManager.tmux.renameSessionPrompt': '重命名会话',
|
||||
'systemManager.tmux.renameWindowPrompt': '重命名窗口',
|
||||
'systemManager.tmux.windowName': '窗口名称',
|
||||
'systemManager.tmux.confirmKillSession': '确定结束 tmux 会话「{{name}}」?',
|
||||
'systemManager.tmux.confirmDetachSession': '确定将所有客户端从「{{name}}」分离?',
|
||||
'systemManager.tmux.confirmKillWindow': '确定关闭窗口「{{name}}」?',
|
||||
'systemManager.tmux.confirmKillPane': '确定关闭面板 #{{index}}?',
|
||||
'systemManager.tmux.confirmKillServer': '确定结束 tmux 服务?所有会话将被终止。',
|
||||
'systemManager.tmux.meta': '{{count}} 个会话',
|
||||
|
||||
'systemManager.docker.title': '容器',
|
||||
'systemManager.docker.subTabs.containers': '容器',
|
||||
'systemManager.docker.subTabs.images': '镜像',
|
||||
'systemManager.docker.empty': '未找到容器',
|
||||
'systemManager.docker.imagesEmpty': '未找到镜像',
|
||||
'systemManager.docker.search': '搜索容器…',
|
||||
'systemManager.docker.searchImages': '搜索镜像…',
|
||||
'systemManager.docker.filter.all': '全部',
|
||||
'systemManager.docker.filter.running': '运行中',
|
||||
'systemManager.docker.filter.stopped': '已停止',
|
||||
'systemManager.docker.filter.paused': '已暂停',
|
||||
'systemManager.docker.shell': 'Shell',
|
||||
'systemManager.docker.logs': '日志',
|
||||
'systemManager.docker.details': '详情',
|
||||
'systemManager.docker.inspect': 'Inspect',
|
||||
'systemManager.docker.imageInspect': '镜像 Inspect',
|
||||
'systemManager.docker.confirmRemove': '确定删除此容器?',
|
||||
'systemManager.docker.confirmKill': '确定强制终止此容器?',
|
||||
'systemManager.docker.confirmRemoveImage': '确定删除镜像「{{name}}」?',
|
||||
'systemManager.docker.confirmPrune': '确定清理悬空镜像?',
|
||||
'systemManager.docker.confirmPruneAll': '确定清理所有未使用镜像?',
|
||||
'systemManager.docker.pause': '暂停',
|
||||
'systemManager.docker.unpause': '恢复',
|
||||
'systemManager.docker.restart': '重启',
|
||||
'systemManager.docker.kill': '强杀',
|
||||
'systemManager.docker.renamePrompt': '容器名称',
|
||||
'systemManager.docker.prune': '清理悬空',
|
||||
'systemManager.docker.pruneAll': '清理全部',
|
||||
'systemManager.docker.tag': '打标签',
|
||||
'systemManager.docker.tagRepoPrompt': '仓库名',
|
||||
'systemManager.docker.tagNamePrompt': '标签名',
|
||||
'systemManager.docker.meta': '{{count}} 个容器',
|
||||
'systemManager.docker.imagesMeta': '{{count}} 个镜像',
|
||||
'systemManager.docker.start': '启动',
|
||||
'systemManager.docker.stop': '停止',
|
||||
|
||||
'systemManager.inspect.status': '状态',
|
||||
'systemManager.inspect.image': '镜像',
|
||||
'systemManager.inspect.created': '创建时间',
|
||||
'systemManager.inspect.started': '启动时间',
|
||||
'systemManager.inspect.restartPolicy': '重启策略',
|
||||
'systemManager.inspect.command': '启动命令',
|
||||
'systemManager.inspect.ports': '端口映射',
|
||||
'systemManager.inspect.networks': '网络',
|
||||
'systemManager.inspect.mounts': '挂载',
|
||||
'systemManager.inspect.env': '环境变量',
|
||||
'systemManager.inspect.labels': '标签',
|
||||
'systemManager.inspect.tags': '镜像标签',
|
||||
'systemManager.inspect.digests': '摘要',
|
||||
'systemManager.inspect.size': '大小',
|
||||
'systemManager.inspect.platform': '平台',
|
||||
'systemManager.inspect.workdir': '工作目录',
|
||||
'systemManager.inspect.exposedPorts': '暴露端口',
|
||||
'systemManager.inspect.showRaw': 'JSON',
|
||||
'systemManager.inspect.hideRaw': '收起 JSON',
|
||||
};
|
||||
@@ -0,0 +1,818 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhCNTerminalMessages: Messages = {
|
||||
'terminal.sudoHint.pressEnter': '按 Enter 粘贴 sudo 密码',
|
||||
'terminal.menu.rename': '重命名',
|
||||
'terminal.menu.uploadClipboardImage': '上传剪贴板图片',
|
||||
'terminal.clipboardImageUpload.noImage': '剪贴板中没有图片',
|
||||
'terminal.clipboardImageUpload.failed': '无法上传剪贴板图片',
|
||||
'terminal.toolbar.configureOsc7': '配置目录追踪',
|
||||
'terminal.toolbar.detach': '移出到独立标签',
|
||||
'terminal.menu.detach': '从工作区移出',
|
||||
'terminal.osc7Setup.title': '配置目录追踪',
|
||||
'terminal.osc7Setup.desc': 'Netcatty 会为当前远端用户添加 OSC 7 提示符配置。这样在 sudo 或 su 之后,SFTP 也能继续跟随终端目录。',
|
||||
'terminal.osc7Setup.targets': '可能写入的文件',
|
||||
'terminal.osc7Setup.command': '将执行的命令',
|
||||
'terminal.osc7Setup.run': '执行配置',
|
||||
'terminal.osc7Setup.running': '正在配置...',
|
||||
'terminal.osc7Setup.configured': '目录追踪已配置',
|
||||
'terminal.osc7Setup.failed': '目录追踪配置失败',
|
||||
'terminal.osc7Setup.sent': '目录追踪配置已发送到终端',
|
||||
'terminal.toolbar.timestampsEnable': '显示时间戳',
|
||||
'terminal.toolbar.timestampsDisable': '隐藏时间戳',
|
||||
'terminal.progress.enterReconnectHint': '按 Enter 重新连接',
|
||||
'terminal.progress.autoReconnectScheduled': '连接已断开,将在 {seconds} 秒后自动重连(第 {attempt} 次)。',
|
||||
'terminal.progress.autoReconnectAttempt': '正在自动重连(第 {attempt} 次)...',
|
||||
'terminal.connection.protocol.et': 'EternalTerminal',
|
||||
'terminal.et.proxyUnsupported': 'EternalTerminal 目前不支持 Netcatty 的代理设置。请改用 SSH,或移除该主机的代理。',
|
||||
'terminal.et.multiJumpUnsupported': 'EternalTerminal 目前在 Netcatty 中最多支持一个跳板机。',
|
||||
// Command history side panel
|
||||
'history.scope.label': '历史范围',
|
||||
'history.tab.host': '主机',
|
||||
'history.tab.global': '全局',
|
||||
'history.searchPlaceholder': '搜索历史命令...',
|
||||
'history.loading': '正在读取远程历史...',
|
||||
'history.meta.count': '{count} 条',
|
||||
'history.empty.noSession': '请先打开一个远程会话以查看其命令历史。',
|
||||
'history.empty.unsupportedProtocol': '仅 SSH/Mosh/ET 会话支持命令历史。',
|
||||
'history.empty.noHistory': '该主机上未找到命令历史。',
|
||||
'history.empty.noGlobalHistory': '暂无全局命令历史。你执行的命令会记录在这里。',
|
||||
'history.action.refresh': '刷新',
|
||||
'history.action.retry': '重试',
|
||||
'history.action.paste': '粘贴到终端',
|
||||
'history.action.run': '在终端执行',
|
||||
'history.action.saveAsSnippet': '保存为代码片段',
|
||||
// SFTP File Opener
|
||||
'sftp.context.copyPath': '复制文件路径',
|
||||
'sftp.context.openWith': '打开方式...',
|
||||
'sftp.context.edit': '编辑',
|
||||
'sftp.context.preview': '预览',
|
||||
'sftp.opener.title': '打开方式',
|
||||
'sftp.opener.desc': '选择一个应用程序来打开此文件',
|
||||
'sftp.opener.builtInEditor': '内置编辑器',
|
||||
'sftp.opener.editDescription': '编辑文本文件',
|
||||
'sftp.opener.builtInImageViewer': '内置图片预览',
|
||||
'sftp.opener.previewDescription': '预览图片',
|
||||
'sftp.opener.systemApp': '选择应用程序...',
|
||||
'sftp.opener.systemAppDescription': '从本地选择一个应用程序',
|
||||
'sftp.opener.onlySystemApp': '此文件只能用外部应用程序打开',
|
||||
'sftp.opener.noAppsAvailable': '无可用应用程序',
|
||||
'sftp.opener.noExtension': '无扩展名文件',
|
||||
'sftp.opener.setDefault': '始终使用此方式打开 {ext} 文件',
|
||||
'sftp.opener.confirmTitle': '设为默认?',
|
||||
'sftp.opener.confirmDescription': '是否始终使用 {app} 打开 {ext} 文件?',
|
||||
'sftp.opener.yesRemember': '是,记住此选择',
|
||||
'sftp.opener.justOnce': '仅此一次',
|
||||
'sftp.opener.confirm.title': '设置默认应用程序',
|
||||
'sftp.opener.confirm.desc': '是否始终使用此应用程序打开 .{ext} 文件?',
|
||||
'sftp.editor.title': '文本编辑器',
|
||||
'sftp.editor.save': '保存到远程',
|
||||
'sftp.editor.saving': '保存中...',
|
||||
'sftp.editor.saved': '保存成功',
|
||||
'sftp.editor.saveFailed': '保存文件失败',
|
||||
'sftp.editor.unsavedChanges': '您有未保存的更改。确定要关闭吗?',
|
||||
'sftp.editor.syntaxHighlight': '语法高亮',
|
||||
'sftp.preview.title': '图片预览',
|
||||
'sftp.preview.zoomIn': '放大',
|
||||
'sftp.preview.zoomOut': '缩小',
|
||||
'sftp.preview.resetZoom': '重置缩放',
|
||||
'sftp.preview.fitToWindow': '适应窗口',
|
||||
|
||||
// Settings > SFTP File Associations
|
||||
'settings.tab.sftpFileAssociations': 'SFTP',
|
||||
'settings.sftp.transferConcurrency': '传输并发数',
|
||||
'settings.sftp.transferConcurrency.desc': '上传或下载文件夹时并行传输的文件数量。较高的值可能提高速度,但可能导致某些服务器过载。',
|
||||
'settings.sftp.defaultOpener': '默认文件打开方式',
|
||||
'settings.sftp.defaultOpener.desc': '选择没有特定文件关联时的默认打开方式',
|
||||
'settings.sftp.defaultOpener.ask': '每次询问',
|
||||
'settings.sftp.defaultOpener.askDesc': '每次打开文件时弹出选择对话框',
|
||||
'settings.sftp.defaultOpener.builtInDesc': '默认使用内置编辑器打开文本文件',
|
||||
'settings.sftp.defaultOpener.systemApp': '选择应用程序...',
|
||||
'settings.sftp.defaultOpener.systemAppDesc': '默认使用指定的外部应用程序打开文件',
|
||||
'settings.sftpFileAssociations.title': 'SFTP 文件关联',
|
||||
'settings.sftpFileAssociations.desc': '配置按扩展名打开文件的默认应用程序',
|
||||
'settings.sftpFileAssociations.extension': '扩展名',
|
||||
'settings.sftpFileAssociations.application': '应用程序',
|
||||
'settings.sftpFileAssociations.noAssociations': '未配置文件关联',
|
||||
'settings.sftpFileAssociations.remove': '移除',
|
||||
'settings.sftpFileAssociations.removeConfirm': '确定移除 .{ext} 的关联吗?',
|
||||
|
||||
// Settings > SFTP Behavior
|
||||
'settings.sftp.doubleClickBehavior': '双击行为',
|
||||
'settings.sftp.doubleClickBehavior.desc': '选择在 SFTP 视图中双击文件时的操作',
|
||||
'settings.sftp.doubleClickBehavior.open': '打开文件',
|
||||
'settings.sftp.doubleClickBehavior.transfer': '传输到另一侧',
|
||||
'settings.sftp.doubleClickBehavior.openDesc': '使用默认应用程序打开文件',
|
||||
'settings.sftp.doubleClickBehavior.transferDesc': '将文件传输到另一窗格的活动主机',
|
||||
'settings.sshDeepLink.title': 'SSH 和 Telnet 链接',
|
||||
'settings.sshDeepLink.enable': '用 Netcatty 打开 ssh:// 和 telnet:// 链接',
|
||||
'settings.sshDeepLink.enableDesc': '允许 Netcatty 接管来自浏览器和其他应用的 ssh://、telnet:// 链接。',
|
||||
'settings.jmsDeepLink.title': 'JumpServer 链接',
|
||||
'settings.jmsDeepLink.enable': '处理 jms:// 链接',
|
||||
'settings.jmsDeepLink.enableDesc': '将 Netcatty 注册为 JumpServer jms:// 客户端链接的处理程序,可能与官方 JumpServer 客户端冲突。',
|
||||
|
||||
// Settings > SFTP Auto Sync
|
||||
'settings.sftp.autoSync': '自动同步到远程',
|
||||
'settings.sftp.autoSync.desc': '使用外部应用程序打开文件时,自动将文件更改同步回远程服务器',
|
||||
'settings.sftp.autoSync.enable': '启用自动同步',
|
||||
'settings.sftp.autoSync.enableDesc': '在外部应用程序中保存文件时,更改将自动上传到远程服务器',
|
||||
|
||||
// Settings > SFTP 自动打开侧栏
|
||||
'settings.sftp.autoOpenSidebar': '连接时自动打开侧栏',
|
||||
'settings.sftp.autoOpenSidebar.desc': '连接到主机时自动打开 SFTP 文件浏览器侧栏',
|
||||
'settings.sftp.autoOpenSidebar.enable': '启用自动打开侧栏',
|
||||
'settings.sftp.autoOpenSidebar.enableDesc': '当终端会话连接到远程主机时,SFTP 侧栏将自动打开',
|
||||
|
||||
'settings.sftp.followTerminalCwd': '追随终端目录',
|
||||
'settings.sftp.followTerminalCwd.desc': '在侧栏 SFTP 中自动跟随终端当前工作目录变化(可在工具栏切换)',
|
||||
'settings.sftp.followTerminalCwd.enable': '默认开启追随终端目录',
|
||||
'settings.sftp.followTerminalCwd.enableDesc': '打开侧栏 SFTP 时默认启用追随模式,终端执行 cd 后文件浏览器会自动跳转',
|
||||
|
||||
'settings.sftp.defaultViewMode': '默认视图模式',
|
||||
'settings.sftp.defaultViewMode.desc': '选择打开新 SFTP 标签页时的默认视图模式。每个主机的偏好设置会覆盖此全局设置。',
|
||||
'settings.sftp.defaultViewMode.list': '列表视图',
|
||||
'settings.sftp.defaultViewMode.listDesc': '以平面列表显示当前目录的文件',
|
||||
'settings.sftp.defaultViewMode.tree': '树形视图',
|
||||
'settings.sftp.defaultViewMode.treeDesc': '以层级树形结构显示文件',
|
||||
|
||||
'sftp.autoSync.success': '文件已同步到远程:{fileName}',
|
||||
'sftp.autoSync.error': '同步文件失败:{error}',
|
||||
|
||||
// SFTP Folder Upload Progress
|
||||
'sftp.upload.progress': '正在上传 {current}/{total} 个文件...',
|
||||
'sftp.upload.uploading': '正在上传...',
|
||||
'sftp.upload.compressing': '正在压缩...',
|
||||
'sftp.upload.extracting': '正在解压...',
|
||||
'sftp.upload.scanning': '正在扫描文件...',
|
||||
'sftp.upload.completed': '已完成',
|
||||
'sftp.upload.compressed': '压缩传输',
|
||||
'sftp.upload.currentFile': '当前: {fileName}',
|
||||
'sftp.upload.cancelled': '上传已取消',
|
||||
'sftp.upload.cancel': '取消',
|
||||
'sftp.upload.completedToPath': '已上传至 {path}',
|
||||
|
||||
// SFTP Download
|
||||
'sftp.download.completed': '已下载',
|
||||
'sftp.download.cancelled': '下载已取消',
|
||||
|
||||
// SFTP Reconnecting
|
||||
'sftp.reconnecting.title': '正在重连...',
|
||||
'sftp.reconnecting.desc': '连接已断开,正在尝试重新连接',
|
||||
'sftp.reconnected': '连接已恢复',
|
||||
'sftp.error.reconnectFailed': '重连失败,请重试。',
|
||||
'sftp.error.connectionLostManual': '连接已断开,请手动重新连接。',
|
||||
'sftp.error.connectionLostReconnecting': '连接已断开,正在重连...',
|
||||
'sftp.error.sessionLost': 'SFTP 会话已断开,请重新连接。',
|
||||
|
||||
// Settings > SFTP Show Hidden Files
|
||||
'settings.sftp.showHiddenFiles': '显示隐藏文件',
|
||||
'settings.sftp.showHiddenFiles.desc': '在 SFTP 文件浏览器中显示隐藏文件(Unix/macOS 点文件和 Windows 隐藏属性文件)。',
|
||||
'settings.sftp.showHiddenFiles.enable': '显示隐藏文件',
|
||||
'settings.sftp.showHiddenFiles.enableDesc': '浏览本地和远程文件系统时显示隐藏文件',
|
||||
|
||||
// Settings > SFTP Compressed Upload
|
||||
'settings.sftp.compressedUpload': '文件夹压缩传输',
|
||||
'settings.sftp.compressedUpload.desc': '上传前压缩文件夹,可大幅减少传输时间。',
|
||||
'settings.sftp.compressedUpload.enable': '启用文件夹压缩',
|
||||
'settings.sftp.compressedUpload.enableDesc': '自动使用 tar 压缩文件夹后再传输。需要服务器支持 tar 命令,不支持时自动回退到普通传输。',
|
||||
|
||||
// Settings > Terminal
|
||||
'settings.terminal.section.theme': '终端主题',
|
||||
'settings.terminal.themeModal.title': '选择主题',
|
||||
'settings.terminal.themeModal.darkThemes': '深色主题',
|
||||
'settings.terminal.themeModal.lightThemes': '浅色主题',
|
||||
'settings.terminal.theme.selectButton': '选择主题',
|
||||
'settings.terminal.theme.followApp': '跟随应用主题',
|
||||
'settings.terminal.theme.followApp.desc': '终端背景色自动匹配当前应用主题,保持视觉一致性。',
|
||||
'settings.terminal.theme.darkTheme': '深色模式终端主题',
|
||||
'settings.terminal.theme.lightTheme': '浅色模式终端主题',
|
||||
'settings.terminal.theme.auto': '自动(跟随界面主题)',
|
||||
'settings.terminal.theme.autoDesc': '跟随当前界面主题预设',
|
||||
'settings.terminal.section.font': '字体',
|
||||
'settings.terminal.section.cursor': '光标',
|
||||
'settings.terminal.section.keyboard': '键盘',
|
||||
'settings.terminal.section.accessibility': '无障碍',
|
||||
'settings.terminal.section.behavior': '行为',
|
||||
'settings.terminal.section.scrollback': '回滚',
|
||||
'settings.terminal.section.keywordHighlight': '关键字高亮',
|
||||
'settings.terminal.font.family': '字体',
|
||||
'settings.terminal.font.family.desc': '终端字体',
|
||||
'settings.terminal.font.cjk': '中文 / CJK 字体',
|
||||
'settings.terminal.font.cjk.desc': '用于渲染中 / 日 / 韩字符的字体;"Auto" 会按主字体智能搭配',
|
||||
'settings.terminal.font.cjk.option.auto': 'Auto · 按主字体智能搭配',
|
||||
'settings.terminal.font.cjk.option.sarasaSC': 'Sarasa Mono SC (更纱黑体 简)',
|
||||
'settings.terminal.font.cjk.option.sarasaTC': 'Sarasa Mono TC (更纱黑体 繁)',
|
||||
'settings.terminal.font.cjk.option.mapleCN': 'Maple Mono CN',
|
||||
'settings.terminal.font.cjk.option.sourceHan': 'Source Han Mono SC (思源等宽)',
|
||||
'settings.terminal.font.cjk.option.notoCJK': 'Noto Sans Mono CJK SC',
|
||||
'settings.terminal.font.cjk.option.lxgwWenkai': 'LXGW WenKai Mono (霞鹜文楷等宽)',
|
||||
'settings.terminal.font.cjk.option.simSun': 'SimSun (宋体)',
|
||||
'settings.terminal.font.cjk.option.legacy': '{font} · 不推荐(非等宽字体)',
|
||||
'settings.terminal.font.size': '字体大小',
|
||||
'settings.terminal.font.size.desc': '终端文字大小',
|
||||
'settings.terminal.font.weight': '字重',
|
||||
'settings.terminal.font.weight.desc': '常规文本字重 (100-900)',
|
||||
'settings.terminal.font.weight.thin': '极细',
|
||||
'settings.terminal.font.weight.extraLight': '特细',
|
||||
'settings.terminal.font.weight.light': '细',
|
||||
'settings.terminal.font.weight.normal': '常规',
|
||||
'settings.terminal.font.weight.medium': '中等',
|
||||
'settings.terminal.font.weight.semiBold': '半粗',
|
||||
'settings.terminal.font.weight.bold': '粗',
|
||||
'settings.terminal.font.weight.extraBold': '特粗',
|
||||
'settings.terminal.font.weight.black': '黑体',
|
||||
'settings.terminal.font.weightBold': '粗体字重',
|
||||
'settings.terminal.font.weightBold.desc': '粗体文本字重 (100-900)',
|
||||
'settings.terminal.font.smoothing': '字体平滑',
|
||||
'settings.terminal.font.smoothing.desc': '使用 macOS 原生字体抗锯齿',
|
||||
'settings.terminal.font.linePadding': '行间距',
|
||||
'settings.terminal.font.linePadding.desc': '行之间的额外间距 (0-10)',
|
||||
'settings.terminal.font.emulationType': '终端仿真类型',
|
||||
'settings.terminal.cursor.style': '光标样式',
|
||||
'settings.terminal.cursor.style.block': '块',
|
||||
'settings.terminal.cursor.style.bar': '竖线',
|
||||
'settings.terminal.cursor.style.underline': '下划线',
|
||||
'settings.terminal.cursor.blink': '光标闪烁',
|
||||
'settings.terminal.keyboard.altAsMeta': '将 Option 作为 Meta 键',
|
||||
'settings.terminal.keyboard.altAsMeta.desc': '使用 Option (Alt) 作为 Meta 键,而不是用于输入特殊字符',
|
||||
'settings.terminal.keyboard.optionArrowWordJump': 'Option+←/→ 按单词跳转',
|
||||
'settings.terminal.keyboard.optionArrowWordJump.desc': '按 Option+左/右 时发送 Meta-b / Meta-f,让 Shell 按单词移动光标(而非默认的 ^[[1;3D / ^[[1;3C)',
|
||||
'settings.terminal.accessibility.minimumContrastRatio': '最小对比度',
|
||||
'settings.terminal.accessibility.minimumContrastRatio.desc': '调整颜色以满足对比度要求 (1 = 禁用, 21 = 最大)',
|
||||
'settings.terminal.behavior.rightClick': '右键行为',
|
||||
'settings.terminal.behavior.rightClick.desc': '在终端中右键时执行的操作',
|
||||
'settings.terminal.behavior.rightClick.menu': '显示菜单',
|
||||
'settings.terminal.behavior.rightClick.paste': '粘贴',
|
||||
'settings.terminal.behavior.rightClick.selectWord': '选择单词',
|
||||
'settings.terminal.behavior.copyOnSelect': '选择即复制',
|
||||
'settings.terminal.behavior.copyOnSelect.desc': '自动复制选中的文本。在 tmux/vim 鼠标模式下,macOS 按住 Option,Windows/Linux 按住 Shift 拖选即可选中文本',
|
||||
'settings.terminal.behavior.middleClickPaste': '中键粘贴',
|
||||
'settings.terminal.behavior.middleClickPaste.desc': '中键点击时粘贴剪贴板内容',
|
||||
'settings.terminal.behavior.middleClick': '中键行为',
|
||||
'settings.terminal.behavior.middleClick.desc': '在终端中点击鼠标中键时执行的操作',
|
||||
'settings.terminal.behavior.middleClick.menu': '显示菜单',
|
||||
'settings.terminal.behavior.middleClick.paste': '粘贴',
|
||||
'settings.terminal.behavior.middleClick.disabled': '无动作',
|
||||
'settings.terminal.behavior.wordSeparators': '单词分隔符',
|
||||
'settings.terminal.behavior.wordSeparators.desc':
|
||||
'双击选择文本时作为边界的字符。空格也算;可添加 =、逗号或 :,方便选择 uid=name 里的部分内容。',
|
||||
'settings.terminal.behavior.bracketedPaste': '括号粘贴模式',
|
||||
'settings.terminal.behavior.bracketedPaste.desc':
|
||||
'粘贴文本时使用转义序列包裹,以便终端区分粘贴和键入。如果出现 ^[[200~ 字样请关闭此选项。',
|
||||
'settings.terminal.behavior.shiftEnterNewline': 'Shift+Enter 发送文本',
|
||||
'settings.terminal.behavior.shiftEnterNewline.desc':
|
||||
'在终端中按 Shift+Enter 时发送配置的文本,而不是普通回车。',
|
||||
'settings.terminal.behavior.shiftEnterNewlineText': '发送内容',
|
||||
'settings.terminal.behavior.shiftEnterNewlineText.desc':
|
||||
'使用 \\n 表示换行,\\t 表示 Tab,\\\\ 表示反斜杠。',
|
||||
'settings.terminal.behavior.clearWipesScrollback': '`clear` 同时清空回滚历史',
|
||||
'settings.terminal.behavior.clearWipesScrollback.desc':
|
||||
'`clear` 命令同时清空回滚历史(POSIX 默认行为)。关闭则保留历史。',
|
||||
'settings.terminal.behavior.preserveSelectionOnInput': '输入时保留选区',
|
||||
'settings.terminal.behavior.preserveSelectionOnInput.desc':
|
||||
'键盘输入时不清除鼠标选中的文本,方便选中路径后输入 `sz ` 之类命令再粘贴。',
|
||||
'settings.terminal.behavior.forcePromptNewLine': '提示符另起一行',
|
||||
'settings.terminal.behavior.forcePromptNewLine.desc':
|
||||
'当命令输出的最后一行未以换行符结束时,将识别到的 shell 提示符移动到下一行显示。',
|
||||
'settings.terminal.behavior.dynamicTabTitle': '动态标签页标题',
|
||||
'settings.terminal.behavior.dynamicTabTitle.desc':
|
||||
'选择标签页什么时候跟随 Shell 报告的窗口标题。',
|
||||
'settings.terminal.behavior.dynamicTabTitle.off': '关闭',
|
||||
'settings.terminal.behavior.dynamicTabTitle.agent': '仅 Agent',
|
||||
'settings.terminal.behavior.dynamicTabTitle.all': '全部会话',
|
||||
'settings.terminal.behavior.osc52Clipboard': 'OSC-52 剪贴板',
|
||||
'settings.terminal.behavior.osc52Clipboard.desc':
|
||||
'允许远程程序(tmux、vim 等)通过 OSC-52 转义序列访问本地剪贴板。',
|
||||
'settings.terminal.behavior.osc52Clipboard.off': '关闭',
|
||||
'settings.terminal.behavior.osc52Clipboard.writeOnly': '仅写入',
|
||||
'settings.terminal.behavior.osc52Clipboard.readWrite': '读写',
|
||||
'settings.terminal.behavior.osc52Clipboard.prompt': '写入 + 读取时询问',
|
||||
'terminal.osc52.readPrompt.title': '剪贴板读取请求',
|
||||
'terminal.osc52.readPrompt.desc': '远程程序正在请求读取您的剪贴板,是否允许?',
|
||||
'terminal.osc52.readPrompt.allow': '允许',
|
||||
'terminal.osc52.readPrompt.deny': '拒绝',
|
||||
'settings.terminal.behavior.scrollOnInput': '输入时自动滚动',
|
||||
'settings.terminal.behavior.scrollOnInput.desc': '输入时将终端滚动到底部',
|
||||
'settings.terminal.behavior.scrollOnOutput': '输出时自动滚动',
|
||||
'settings.terminal.behavior.scrollOnOutput.desc': '有新输出时将终端滚动到底部',
|
||||
'settings.terminal.behavior.scrollOnKeyPress': '按键时自动滚动',
|
||||
'settings.terminal.behavior.scrollOnKeyPress.desc': '按键(例如 Enter)时将终端滚动到底部',
|
||||
'settings.terminal.behavior.scrollOnPaste': '粘贴时自动滚动',
|
||||
'settings.terminal.behavior.scrollOnPaste.desc': '粘贴文本时将终端滚动到底部',
|
||||
'settings.terminal.behavior.smoothScrolling': '平滑滚动',
|
||||
'settings.terminal.behavior.smoothScrolling.desc': '滚动终端视口时使用平滑动画',
|
||||
'settings.terminal.behavior.linkModifier': '链接修饰键',
|
||||
'settings.terminal.behavior.linkModifier.desc': '按住此键再点击终端中的链接',
|
||||
'settings.terminal.behavior.linkModifier.none': '无(直接点击)',
|
||||
'settings.terminal.behavior.linkModifier.ctrl': 'Ctrl',
|
||||
'settings.terminal.behavior.linkModifier.alt': 'Alt / Option',
|
||||
'settings.terminal.behavior.linkModifier.meta': 'Cmd / Win',
|
||||
'settings.terminal.scrollback.desc': '限制终端行数。设为 0 表示不限制。',
|
||||
'settings.terminal.scrollback.rows': '行数 *',
|
||||
'settings.terminal.section.startupCommand': '启动命令',
|
||||
'settings.terminal.startupCommandDelay.label': '启动命令延迟(毫秒)',
|
||||
'settings.terminal.startupCommandDelay.desc': '连接建立后等待多久再发送启动命令;启动命令为多行时,行与行之间也使用该间隔。慢连接可调大。',
|
||||
'settings.terminal.keywordHighlight.title': '关键字高亮',
|
||||
'settings.terminal.keywordHighlight.resetColors': '重置为默认颜色',
|
||||
'settings.terminal.keywordHighlight.resetDefaults': '把内置规则恢复为默认',
|
||||
'settings.terminal.keywordHighlight.resetBuiltIn': '恢复内置标签与正则',
|
||||
'settings.terminal.keywordHighlight.addCustom': '添加自定义规则',
|
||||
'settings.terminal.keywordHighlight.editCustom': '编辑规则',
|
||||
'settings.terminal.keywordHighlight.editBuiltIn': '编辑内置规则',
|
||||
'settings.terminal.keywordHighlight.labelField': '标签与颜色',
|
||||
'settings.terminal.keywordHighlight.labelPlaceholder': '标签(如 Down)',
|
||||
'settings.terminal.keywordHighlight.patternField': '正则表达式',
|
||||
'settings.terminal.keywordHighlight.patternPlaceholder': '每行一个正则(如 \\bdown\\b)',
|
||||
'settings.terminal.keywordHighlight.patternHint': '每行一个正则。匹配忽略大小写,全局匹配。',
|
||||
'settings.terminal.keywordHighlight.invalidPattern': '无效的正则表达式',
|
||||
'settings.terminal.keywordHighlight.preview': '预览',
|
||||
'settings.terminal.section.localShell': '本地 Shell',
|
||||
'settings.terminal.localShell.shell': 'Shell 可执行文件',
|
||||
'settings.terminal.localShell.shell.desc': 'Shell 可执行文件的路径(例如 /bin/zsh、pwsh.exe)。留空使用系统默认。',
|
||||
'settings.terminal.localShell.shell.placeholder': '系统默认',
|
||||
'settings.terminal.localShell.shell.detected': '检测到',
|
||||
'settings.terminal.localShell.shell.notFound': '未找到 Shell 可执行文件',
|
||||
'settings.terminal.localShell.shell.isDirectory': '路径是目录,不是可执行文件',
|
||||
'settings.terminal.localShell.shell.default': '系统默认',
|
||||
'settings.terminal.localShell.shell.custom': '自定义...',
|
||||
'settings.terminal.localShell.shell.customPath': 'Shell 可执行文件路径',
|
||||
'settings.terminal.localShell.shell.customArgs': '启动参数',
|
||||
'settings.terminal.localShell.shell.customArgs.placeholder': '例如 --login -i',
|
||||
'settings.terminal.localShell.shell.customArgs.desc': '传给 Shell 的启动参数。部分 Shell 必须指定才能正常工作,例如 msys2 bash 需要 --login -i 才能加载环境变量。',
|
||||
'settings.terminal.localShell.shell.commonPaths': '常用路径',
|
||||
'settings.terminal.localShell.shell.pathValid': '路径有效',
|
||||
'settings.terminal.localShell.startDir': '起始目录',
|
||||
'settings.terminal.localShell.startDir.desc': '打开本地终端时的起始目录。留空使用用户主目录。',
|
||||
'settings.terminal.localShell.startDir.placeholder': '用户主目录',
|
||||
'settings.terminal.localShell.startDir.notFound': '目录不存在',
|
||||
'settings.terminal.localShell.startDir.isFile': '路径是文件,不是目录',
|
||||
'settings.terminal.section.connection': '连接',
|
||||
'settings.terminal.connection.sshAutoReconnectEnabled': 'SSH 断线后自动重连',
|
||||
'settings.terminal.connection.sshAutoReconnectEnabled.desc': '打开后,已连上的 SSH 会话意外断开时会每 5 秒尝试重新连接,直到关闭标签页或连接恢复。',
|
||||
'settings.terminal.connection.keepaliveInterval': '会话保持间隔',
|
||||
'settings.terminal.connection.keepaliveInterval.desc': '向服务器发送 SSH 保活数据包的频率(秒)。设为 0 表示全局禁用——单个主机可在自己的设置里覆盖此值。',
|
||||
'settings.terminal.connection.keepaliveCountMax': '最大无响应保活次数',
|
||||
'settings.terminal.connection.keepaliveCountMax.desc': '判定连接死亡前允许的无响应保活次数。值越大对短暂网络抖动和响应慢的 SSH 服务越宽容。',
|
||||
'settings.terminal.connection.x11Display': 'X11 显示地址',
|
||||
'settings.terminal.connection.x11Display.desc': '可选的本机 X11 显示地址。留空则使用系统默认值。',
|
||||
'settings.terminal.connection.x11Display.placeholder': '自动(:0 或 DISPLAY)',
|
||||
'settings.terminal.section.serverStats': '服务器状态(Linux)',
|
||||
'settings.terminal.serverStats.show': '显示服务器状态',
|
||||
'settings.terminal.serverStats.show.desc': '在终端状态栏显示 CPU、内存和磁盘使用情况(仅限 Linux 服务器)。',
|
||||
'settings.terminal.serverStats.refreshInterval': '刷新间隔',
|
||||
'settings.terminal.serverStats.refreshInterval.desc': '服务器状态刷新的频率。',
|
||||
'settings.terminal.serverStats.seconds': '秒',
|
||||
'settings.terminal.section.systemManager': '系统管理',
|
||||
'settings.terminal.systemManager.processRefreshInterval': '进程列表刷新间隔',
|
||||
'settings.terminal.systemManager.processRefreshInterval.desc': '系统管理侧栏中进程列表的刷新频率。',
|
||||
'settings.terminal.systemManager.tmuxRefreshInterval': 'tmux 会话刷新间隔',
|
||||
'settings.terminal.systemManager.tmuxRefreshInterval.desc': 'tmux 会话列表的刷新频率。',
|
||||
'settings.terminal.systemManager.dockerListRefreshInterval': 'Docker 容器列表刷新间隔',
|
||||
'settings.terminal.systemManager.dockerListRefreshInterval.desc': 'Docker 容器列表的刷新频率。',
|
||||
'settings.terminal.systemManager.dockerStatsRefreshInterval': 'Docker 性能数据刷新间隔',
|
||||
'settings.terminal.systemManager.dockerStatsRefreshInterval.desc': 'Docker 容器 CPU/内存/网络指标的刷新频率。',
|
||||
|
||||
// Settings > Terminal > Rendering
|
||||
'settings.terminal.section.rendering': '渲染',
|
||||
'settings.terminal.rendering.renderer': '渲染器',
|
||||
'settings.terminal.rendering.renderer.desc': '选择终端渲染技术。自动模式会在低内存设备上使用 DOM 渲染。更改将在新终端会话中生效。',
|
||||
'settings.terminal.rendering.auto': '自动',
|
||||
'settings.terminal.rendering.hibernateHiddenTabs': '隐藏标签页休眠',
|
||||
'settings.terminal.rendering.hibernateHiddenTabs.desc': '标签页离开视野后释放终端渲染器以节省内存,SSH 连接保持不断。文件传输期间不会休眠。',
|
||||
'settings.terminal.rendering.hibernateHiddenTabsDelay': '休眠延迟',
|
||||
'settings.terminal.rendering.hibernateHiddenTabsDelay.desc': '标签页离开视野多久后释放渲染器(5–600 秒)。',
|
||||
'settings.terminal.rendering.hibernateSkipAltScreen': '全屏 TUI 跳过休眠',
|
||||
'settings.terminal.rendering.hibernateSkipAltScreen.desc': 'vim、htop 或 Agent TUI 占用 alternate screen 时保持终端渲染器,避免慢恢复。',
|
||||
'settings.terminal.rendering.hibernateKeepRendererCount': '保留隐藏渲染器数量',
|
||||
'settings.terminal.rendering.hibernateKeepRendererCount.desc': '多少个不可见标签页仅暂停 WebGL、不销毁 xterm,超出后才完整休眠。',
|
||||
'settings.terminal.rendering.hibernateReplayChunkBytes': '唤醒分片大小',
|
||||
'settings.terminal.rendering.hibernateReplayChunkBytes.desc': '恢复休眠终端时每帧回放的 ANSI 字节数(4–64 KB)。',
|
||||
'settings.terminal.rendering.hibernatePreferWasmSerialize': '优先 WASM 序列化',
|
||||
'settings.terminal.rendering.hibernatePreferWasmSerialize.desc': '在可用时使用 WASM 终端序列化,否则回退到 JS SerializeAddon。',
|
||||
|
||||
// Settings > Terminal > Workspace Focus Indicator
|
||||
'settings.terminal.section.workspaceFocus': '工作区焦点提示',
|
||||
'settings.terminal.workspaceFocus.style': '焦点提示样式',
|
||||
'settings.terminal.workspaceFocus.style.desc': '在分屏视图中如何标识当前聚焦的窗格。',
|
||||
'settings.terminal.workspaceFocus.dim': '淡化未聚焦窗格',
|
||||
'settings.terminal.workspaceFocus.border': '为聚焦窗格显示边框',
|
||||
|
||||
// Settings > Terminal > Autocomplete
|
||||
'settings.terminal.section.autocomplete': '自动补全',
|
||||
'settings.terminal.autocomplete.enabled': '启用自动补全',
|
||||
'settings.terminal.autocomplete.enabled.desc': '输入时根据历史命令和命令规范显示补全建议。',
|
||||
'settings.terminal.autocomplete.ghostText': '行内建议',
|
||||
'settings.terminal.autocomplete.ghostText.desc': '在光标后显示灰色的建议文本(类似 fish shell)。',
|
||||
'settings.terminal.autocomplete.popupMenu': '弹出菜单',
|
||||
'settings.terminal.autocomplete.popupMenu.desc': '显示包含多个建议的浮动列表。',
|
||||
|
||||
// Settings > Shortcuts
|
||||
'settings.shortcuts.section.scheme': '快捷键方案',
|
||||
'settings.shortcuts.scheme.label': '键盘快捷键',
|
||||
'settings.shortcuts.scheme.desc': '选择快捷键使用的键盘布局',
|
||||
'settings.shortcuts.scheme.disabled': '禁用',
|
||||
'settings.shortcuts.scheme.mac': 'Mac (Cmd)',
|
||||
'settings.shortcuts.scheme.pc': 'PC (Ctrl)',
|
||||
'settings.shortcuts.disableTerminalFontZoom.label': '禁用终端缩放',
|
||||
'settings.shortcuts.disableTerminalFontZoom.desc': '关闭终端文字缩放快捷操作,包括 Cmd/Ctrl 加滚轮。',
|
||||
'settings.shortcuts.shellOnlyTabNumberShortcuts.label': '标签快捷键跳过固定标签',
|
||||
'settings.shortcuts.shellOnlyTabNumberShortcuts.desc': '开启后,Cmd/Ctrl+[1...9] 和 Ctrl+Tab 仅在终端、工作区、编辑器等可关闭标签页之间切换,不包括固定的 Vault 和 SFTP 标签页。',
|
||||
'settings.shortcuts.section.custom': '自定义快捷键',
|
||||
'settings.shortcuts.resetAll': '全部重置',
|
||||
'settings.shortcuts.recording': '请按键...',
|
||||
'settings.shortcuts.none': '无',
|
||||
'settings.shortcuts.setDisabled': '设为禁用',
|
||||
'settings.shortcuts.category.tabs': '标签页',
|
||||
'settings.shortcuts.category.terminal': '终端',
|
||||
'settings.shortcuts.category.navigation': '导航',
|
||||
'settings.shortcuts.category.app': '应用',
|
||||
'settings.shortcuts.category.sftp': 'SFTP',
|
||||
'settings.shortcuts.binding.switch-tab-1-9': '切换到标签页 [1...9]',
|
||||
'settings.shortcuts.binding.next-tab': '下一个标签页',
|
||||
'settings.shortcuts.binding.prev-tab': '上一个标签页',
|
||||
'settings.shortcuts.binding.close-tab': '关闭标签页',
|
||||
'settings.shortcuts.binding.close-session': '关闭会话窗格',
|
||||
'settings.shortcuts.binding.new-tab': '新建本地标签页',
|
||||
'settings.shortcuts.binding.copy': '从终端复制',
|
||||
'settings.shortcuts.binding.paste': '粘贴到终端',
|
||||
'settings.shortcuts.binding.paste-selection': '将选区粘贴到终端',
|
||||
'settings.shortcuts.binding.select-all': '全选终端内容',
|
||||
'settings.shortcuts.binding.clear-buffer': '清空终端缓冲区',
|
||||
'settings.shortcuts.binding.search-terminal': '打开终端搜索',
|
||||
'settings.shortcuts.binding.increase-terminal-font-size': '增大终端字号',
|
||||
'settings.shortcuts.binding.decrease-terminal-font-size': '减小终端字号',
|
||||
'settings.shortcuts.binding.reset-terminal-font-size': '重置终端字号',
|
||||
'settings.shortcuts.binding.move-focus': '在分屏间移动焦点',
|
||||
'settings.shortcuts.binding.split-horizontal': '水平分屏',
|
||||
'settings.shortcuts.binding.split-vertical': '垂直分屏',
|
||||
'settings.shortcuts.binding.toggle-pane-zoom': '切换窗格缩放',
|
||||
'settings.shortcuts.binding.open-hosts': '打开主机列表',
|
||||
'settings.shortcuts.binding.open-local': '打开本地终端',
|
||||
'settings.shortcuts.binding.open-sftp': '打开 SFTP',
|
||||
'settings.shortcuts.binding.open-settings': '打开设置',
|
||||
'settings.shortcuts.binding.port-forwarding': '打开端口转发',
|
||||
'settings.shortcuts.binding.command-palette': '打开命令面板',
|
||||
'settings.shortcuts.binding.quick-switch': '快速切换',
|
||||
'settings.shortcuts.binding.new-workspace': '新建工作区',
|
||||
'settings.shortcuts.binding.snippets': '打开代码片段',
|
||||
'settings.shortcuts.binding.broadcast': '切换广播模式',
|
||||
'settings.shortcuts.binding.toggle-side-panel': '切换侧边栏',
|
||||
'settings.shortcuts.binding.sftp-copy': '复制文件',
|
||||
'settings.shortcuts.binding.sftp-cut': '剪切文件',
|
||||
'settings.shortcuts.binding.sftp-paste': '粘贴文件',
|
||||
'settings.shortcuts.binding.sftp-select-all': '全选文件',
|
||||
'settings.shortcuts.binding.sftp-rename': '重命名文件',
|
||||
'settings.shortcuts.binding.sftp-delete': '删除文件',
|
||||
'settings.shortcuts.binding.sftp-refresh': '刷新',
|
||||
'settings.shortcuts.binding.sftp-new-folder': '新建文件夹',
|
||||
'settings.shortcuts.binding.sftp-open': '打开文件 / 进入目录',
|
||||
'settings.shortcuts.binding.sftp-go-parent': '转到上级目录',
|
||||
'settings.shortcuts.binding.sftp-navigate-to': '转到选中的目录',
|
||||
|
||||
// Host Details (sub-panels)
|
||||
'hostDetails.proxyPanel.title': '通过 HTTP/SOCKS5/命令代理',
|
||||
'hostDetails.proxyPanel.hostPlaceholder': '代理主机',
|
||||
'hostDetails.proxyPanel.command': 'ProxyCommand',
|
||||
'hostDetails.proxyPanel.commandPlaceholder': 'cloudflared access ssh --hostname %h',
|
||||
'hostDetails.proxyPanel.commandHelp': '使用 %h 表示目标主机,%p 表示目标端口,%% 表示字面百分号。',
|
||||
'hostDetails.proxyPanel.credentials': '凭据',
|
||||
'hostDetails.proxyPanel.usernamePlaceholder': '用户名',
|
||||
'hostDetails.proxyPanel.passwordPlaceholder': '密码',
|
||||
'hostDetails.proxyPanel.identities': '身份',
|
||||
'hostDetails.proxyPanel.keychainIdentity': '钥匙串身份',
|
||||
'hostDetails.proxyPanel.manualCredentials': '手动凭据',
|
||||
'hostDetails.proxyPanel.remove': '移除代理',
|
||||
'hostDetails.proxyPanel.savedProxy': '已保存代理',
|
||||
'hostDetails.proxyPanel.selectSaved': '选择已保存代理',
|
||||
'hostDetails.proxyPanel.customProxy': '自定义代理',
|
||||
'hostDetails.proxyPanel.missing': '缺失',
|
||||
'hostDetails.proxyPanel.missingSaved': '保存的代理不存在',
|
||||
'hostDetails.proxyPanel.missingIdentity': '钥匙串身份不存在',
|
||||
'hostDetails.proxyPanel.incompleteIdentity': '代理身份需要用户名和密码',
|
||||
'hostDetails.proxyPanel.unreadableIdentity': '此设备无法读取代理身份密码',
|
||||
'hostDetails.proxyPanel.error.required': '代理主机和端口,或 ProxyCommand 不能为空。',
|
||||
'hostDetails.envVars.title': '环境变量',
|
||||
'hostDetails.envVars.desc': '为 {host} 设置环境变量。',
|
||||
'hostDetails.envVars.note': '部分 SSH 服务器默认只允许以 LC_ 和 LANG_ 为前缀的变量。',
|
||||
'hostDetails.envVars.variable': '变量',
|
||||
'hostDetails.envVars.value': '值',
|
||||
'hostDetails.envVars.newVariable': '新变量',
|
||||
'hostDetails.envVars.variableName': '变量名',
|
||||
'hostDetails.chain.title': '编辑链路',
|
||||
'hostDetails.chain.desc': '添加另一台主机将创建到 {host} 的连接。',
|
||||
'hostDetails.chain.addHost': '添加主机',
|
||||
'hostDetails.chain.target': '目标',
|
||||
'hostDetails.chain.availableHosts': '可用主机',
|
||||
'hostDetails.chain.clear': '清空',
|
||||
'hostDetails.group.title': '新建分组',
|
||||
'hostDetails.group.general': '常规',
|
||||
'hostDetails.group.namePlaceholder': '分组名称',
|
||||
'hostDetails.group.parentPlaceholder': '父分组',
|
||||
'hostDetails.group.cloudSync': '云同步',
|
||||
'hostDetails.group.addProtocol': '添加协议',
|
||||
|
||||
// Keychain
|
||||
'keychain.filter.key': '密钥',
|
||||
'keychain.filter.certificate': '证书',
|
||||
'keychain.action.generateKey': '生成密钥',
|
||||
'keychain.action.importKey': '导入密钥',
|
||||
'keychain.action.newIdentity': '新建身份',
|
||||
'keychain.action.importCertificate': '导入证书',
|
||||
'keychain.view.grid': '网格',
|
||||
'keychain.view.list': '列表',
|
||||
'keychain.section.keys': '密钥',
|
||||
'keychain.section.identities': '身份',
|
||||
'keychain.count.items': '{count} 项',
|
||||
'keychain.empty.title': '设置密钥',
|
||||
'keychain.empty.desc': '导入或生成 SSH 密钥用于安全认证。',
|
||||
'keychain.panel.generateKey': '生成密钥',
|
||||
'keychain.panel.newKey': '新建密钥',
|
||||
'keychain.panel.keyDetails': '密钥详情',
|
||||
'keychain.panel.editKey': '编辑密钥',
|
||||
'keychain.panel.editIdentity': '编辑身份',
|
||||
'keychain.panel.newIdentity': '新建身份',
|
||||
'keychain.panel.keyExport': '密钥导出',
|
||||
'keychain.validation.labelRequired': '请填写密钥的 Label',
|
||||
'keychain.validation.labelAndPrivateKeyRequired': 'Label 和私钥为必填项',
|
||||
'keychain.validation.labelAndUsernameRequired': 'Label 和用户名为必填项',
|
||||
'keychain.error.generationUnavailable': '无法生成密钥:请确保应用运行在 Electron 环境',
|
||||
'keychain.error.generateKeyPairFailed': '生成密钥对失败',
|
||||
'keychain.error.generateKeyFailed': '生成密钥失败',
|
||||
'keychain.error.keyGenerationTitle': '密钥生成',
|
||||
'keychain.export.exportTo': '导出到 *',
|
||||
'keychain.export.selectHost': '选择主机',
|
||||
'keychain.export.location': '位置 ~ $1 *',
|
||||
'keychain.export.filename': '文件名 ~ $2 *',
|
||||
'keychain.export.note': '密钥导出目前仅支持 {unix} 系统。请在 {advanced} 部分自定义导出脚本。',
|
||||
'keychain.export.script': '脚本 *',
|
||||
'keychain.export.scriptPlaceholder': '导出脚本...',
|
||||
'keychain.export.missingCredentials': '主机未保存密码或密钥。请先为该主机添加密码凭据。',
|
||||
'keychain.export.successTitle': '导出成功',
|
||||
'keychain.export.successMessage': '已导出公钥并绑定到 {host}',
|
||||
'keychain.export.failedTitle': '导出失败',
|
||||
'keychain.export.failedMessage': '导出密钥失败:{error}',
|
||||
'keychain.export.failedPrefix': '导出失败:{error}',
|
||||
'keychain.export.exitCode': '命令退出码 {code}',
|
||||
'keychain.export.exporting': '导出中...',
|
||||
'keychain.export.exportAndAttach': '导出并绑定',
|
||||
'keychain.export.title': '密钥导出',
|
||||
'keychain.export.exportToRequired': '导出到 *',
|
||||
'keychain.export.selectHostPlaceholder': '选择主机...',
|
||||
'keychain.export.locationLabel': '位置 ~ $1 *',
|
||||
'keychain.export.filenameLabel': '文件名 ~ $2 *',
|
||||
'keychain.export.advanced': '高级',
|
||||
'keychain.export.note.supportsOnly': '密钥导出目前仅支持',
|
||||
'keychain.export.note.systems': '系统。',
|
||||
'keychain.export.note.use': '请使用',
|
||||
'keychain.export.note.customize': '部分自定义导出脚本。',
|
||||
'keychain.export.scriptRequired': '脚本 *',
|
||||
'keychain.export.exportToHost': '导出到主机',
|
||||
'keychain.export.failedGeneric': '导出失败:{message}',
|
||||
'keychain.field.label': 'Label',
|
||||
'keychain.field.labelRequired': 'Label *',
|
||||
'keychain.field.labelPlaceholder': '密钥 Label',
|
||||
'keychain.field.privateKeyRequired': '私钥 *',
|
||||
'keychain.field.publicKey': '公钥',
|
||||
'keychain.field.certificatePlaceholder': '证书内容(可选)',
|
||||
'keychain.generate.keyType': '密钥类型',
|
||||
'keychain.generate.keySize': '密钥长度',
|
||||
'keychain.generate.labelPlaceholder': '密钥 Label',
|
||||
'keychain.generate.passphrasePlaceholder': 'Passphrase(可选)',
|
||||
'keychain.generate.savePassphrase': '保存 Passphrase',
|
||||
'keychain.generate.generate': '生成',
|
||||
'keychain.generate.generateSave': '生成并保存',
|
||||
'keychain.import.dropHint': '将密钥文件拖到这里',
|
||||
'keychain.import.importFromFile': '从文件导入',
|
||||
'keychain.import.saveKey': '保存密钥',
|
||||
'keychain.import.importedKeyLabel': '已导入密钥',
|
||||
'keychain.identity.usernameRequired': '用户名 *',
|
||||
'keychain.identity.method.passwordOnly': '密码',
|
||||
'keychain.identity.summary.password': '认证密码',
|
||||
'keychain.identity.summary.key': '认证密钥',
|
||||
'keychain.identity.summary.certificate': '认证证书',
|
||||
'keychain.identity.summary.passwordAndKey': '认证密码与密钥',
|
||||
'keychain.identity.summary.passwordAndCertificate': '认证密码与证书',
|
||||
'keychain.identity.summary.none': '无凭据',
|
||||
'keychain.identity.selectCredential': '选择{kind}',
|
||||
'keychain.identity.save': '保存',
|
||||
'keychain.identity.update': '更新',
|
||||
'keychain.keyDialog.newTitle': '新建密钥',
|
||||
'keychain.keyDialog.newDesc': '添加新的 SSH 密钥',
|
||||
'keychain.keyDialog.editTitle': '编辑密钥',
|
||||
'keychain.keyDialog.editDesc': '更新此 SSH 密钥',
|
||||
'keychain.keyDialog.updateKey': '更新密钥',
|
||||
|
||||
// Tabs
|
||||
'tabs.closeSessionAria': '关闭会话',
|
||||
'tabs.closeLogViewAria': '关闭日志视图',
|
||||
'tabs.logPrefix': '日志:',
|
||||
'tabs.logLocal': '本地',
|
||||
'tabs.copyTab': '复制标签页',
|
||||
'tabs.copyTabToNewWindow': '复制标签页到新窗口',
|
||||
'tabs.copyTabToNewWindowFailed': '无法在新窗口打开标签页',
|
||||
'tabs.closeOthers': '关闭其他标签',
|
||||
'tabs.closeToRight': '关闭右侧标签',
|
||||
'tabs.closeAll': '关闭所有标签',
|
||||
'keychain.edit.labelRequired': 'Label *',
|
||||
'keychain.edit.keyLabelPlaceholder': '密钥 Label',
|
||||
'keychain.edit.privateKeyRequired': '私钥 *',
|
||||
'keychain.edit.publicKey': '公钥',
|
||||
'keychain.edit.certificate': '证书',
|
||||
'keychain.edit.certificatePlaceholder': '证书内容(可选)',
|
||||
'keychain.edit.filePath': '文件路径',
|
||||
'keychain.edit.keyExport': '密钥导出',
|
||||
'keychain.edit.exportToHost': '导出到主机',
|
||||
|
||||
// Snippets
|
||||
'snippets.searchPlaceholder': '搜索脚本...',
|
||||
'snippets.action.newSnippet': '新建代码片段',
|
||||
'snippets.action.newPackage': '新建脚本包',
|
||||
'snippets.action.import': '导入',
|
||||
'snippets.action.selectSnippets': '选择代码片段',
|
||||
'snippets.panel.newTitle': '新建代码片段',
|
||||
'snippets.panel.editTitle': '编辑代码片段',
|
||||
'snippets.panel.newAutomationTitle': '新建自动化脚本',
|
||||
'snippets.panel.editAutomationTitle': '编辑自动化脚本',
|
||||
'snippets.panel.resizeWidth': '调整面板宽度',
|
||||
'snippets.field.description': '描述',
|
||||
'snippets.field.descriptionPlaceholder': '例如:check network load',
|
||||
'snippets.field.package': '添加脚本包',
|
||||
'snippets.field.packagePlaceholder': '选择或创建脚本包',
|
||||
'snippets.field.createPackage': '创建脚本包',
|
||||
'snippets.field.scriptRequired': '脚本 *',
|
||||
'snippets.scriptEditor.expand': '弹窗编辑',
|
||||
'snippets.scriptEditor.resize': '调整编辑器高度',
|
||||
'snippets.scriptEditor.modalTitle': '编辑脚本',
|
||||
'snippets.targets.title': '目标主机',
|
||||
'snippets.targets.add': '添加目标主机',
|
||||
'snippets.targets.allHosts': '对所有主机生效',
|
||||
'snippets.targets.allHostsShort': '所有主机',
|
||||
'snippets.targets.allHostsActive': '将作用于所有可连接的主机。',
|
||||
'snippets.history.title': 'Shell 历史',
|
||||
'snippets.history.subtitle': '{count} 条命令',
|
||||
'snippets.history.emptyTitle': '暂无 Shell 历史',
|
||||
'snippets.history.emptyDesc': '你执行过的命令会显示在这里',
|
||||
'snippets.history.loadMore': '加载更多',
|
||||
'snippets.history.separator': '•',
|
||||
'snippets.history.labelPlaceholder': '为此代码片段设置一个 Label',
|
||||
'snippets.history.saveAsSnippet': '保存为代码片段',
|
||||
'snippets.history.time.justNow': '刚刚',
|
||||
'snippets.history.time.minutesAgo': '{count} 分钟前',
|
||||
'snippets.history.time.hoursAgo': '{count} 小时前',
|
||||
'snippets.history.time.daysAgo': '{count} 天前',
|
||||
'snippets.breadcrumb.allPackages': '全部脚本包',
|
||||
'snippets.breadcrumb.separator': '›',
|
||||
'snippets.empty.title': '创建脚本',
|
||||
'snippets.empty.desc': '将常用命令保存为代码片段,或编写自动化脚本来处理重复运维任务。',
|
||||
'snippets.search.noResults.title': '无匹配结果',
|
||||
'snippets.search.noResults.desc': '没有脚本或脚本包与"{query}"匹配。换一个关键字,或清除搜索进行浏览。',
|
||||
'snippets.section.packages': '脚本包',
|
||||
'snippets.section.snippets': '脚本',
|
||||
'snippets.kind.codeSnippet': '代码片段',
|
||||
'snippets.kind.automationScript': '自动化脚本',
|
||||
'snippets.package.count': '{count} 个脚本',
|
||||
'snippets.commandFallback': '命令',
|
||||
'snippets.view.grid': '网格',
|
||||
'snippets.view.list': '列表',
|
||||
'snippets.selection.selected': '已选择 {count} 个',
|
||||
'snippets.selection.selectVisible': '选择当前显示',
|
||||
'snippets.selection.deselectAll': '取消选择',
|
||||
'snippets.selection.exportSelected': '导出选中({count})',
|
||||
'snippets.export.snippet': '导出代码片段',
|
||||
'snippets.export.package': '导出脚本包',
|
||||
'snippets.export.toast.empty': '没有可导出的代码片段。',
|
||||
'snippets.export.toast.successTitle': '导出已准备好',
|
||||
'snippets.export.toast.success': '已导出 {count} 个代码片段。',
|
||||
'snippets.import.toast.empty': '没有找到可导入的代码片段。',
|
||||
'snippets.import.toast.failedTitle': '导入失败',
|
||||
'snippets.import.toast.invalidDesc': '这不是有效的 Netcatty 代码片段文件。',
|
||||
'snippets.import.toast.successTitle': '导入完成',
|
||||
'snippets.import.toast.summary': '已导入 {imported} 个,覆盖 {overwritten} 个,跳过 {skipped} 个。',
|
||||
'snippets.import.modal.title': '导入代码片段',
|
||||
'snippets.import.modal.desc': '选择一个或多个 Netcatty 代码片段 JSON 文件。导入前会先解析并展示结果,不会直接写入。',
|
||||
'snippets.import.modal.exampleTitle': '示例 JSON',
|
||||
'snippets.import.modal.noFile': '还没有选择文件。可以参考下面的格式,也可以直接使用代码片段 JSON 数组,然后选择一个或多个文件。',
|
||||
'snippets.import.modal.chooseFile': '选择文件',
|
||||
'snippets.import.modal.downloadExamples': '下载示例文件',
|
||||
'snippets.import.modal.multipleFiles': '已选择 {count} 个文件',
|
||||
'snippets.import.modal.parsedSummary': '{files} 个文件,{total} 个脚本,{packages} 个脚本包,{conflicts} 个重复命令。主机绑定会被忽略。',
|
||||
'snippets.import.modal.confirm': '确认导入',
|
||||
'snippets.import.conflict.title': '导入代码片段?',
|
||||
'snippets.import.conflict.desc': '“{file}”包含 {total} 个代码片段,其中 {conflicts} 个命令已存在。',
|
||||
'snippets.import.conflict.hostBindingsNote': '代码片段的主机绑定不会被导入或导出。',
|
||||
'snippets.import.conflict.skip': '跳过重复项',
|
||||
'snippets.import.conflict.overwrite': '覆盖重复项',
|
||||
'snippets.packageDialog.title': '新建脚本包',
|
||||
'snippets.packageDialog.parent': '父级:{parent}',
|
||||
'snippets.packageDialog.root': '根目录',
|
||||
'snippets.packageDialog.placeholder': '例如:ops/maintenance',
|
||||
'snippets.packageDialog.hint': '使用 "/" 创建嵌套脚本包。',
|
||||
|
||||
// Snippets Rename Dialog
|
||||
'snippets.renameDialog.title': '重命名脚本包',
|
||||
'snippets.renameDialog.currentPath': '当前路径:{path}',
|
||||
'snippets.renameDialog.placeholder': '输入新名称',
|
||||
'snippets.renameDialog.error.empty': '脚本包名称不能为空',
|
||||
'snippets.renameDialog.error.duplicate': '已存在同名的脚本包',
|
||||
'snippets.renameDialog.error.invalidChars': '脚本包名称只能包含字母、数字、连字符和下划线',
|
||||
|
||||
'snippets.field.noAutoRun': '仅粘贴(不自动执行)',
|
||||
'snippets.field.multiLineRunMode': '多行运行方式',
|
||||
'snippets.field.multiLineRunMode.paste': '一次性发送',
|
||||
'snippets.field.multiLineRunMode.lineDelay': '逐行发送',
|
||||
'snippets.field.multiLineRunModeHint': '登录提示、网络设备宏等场景可用逐行发送。',
|
||||
// Snippet Shortkey
|
||||
'snippets.field.shortkey': '快捷键',
|
||||
'snippets.shortkey.placeholder': '点击设置快捷键',
|
||||
'snippets.shortkey.recording': '请按下快捷键组合...',
|
||||
'snippets.shortkey.hint': '在终端中按下此快捷键可快速发送命令。',
|
||||
'snippets.shortkey.clear': '清除快捷键',
|
||||
'snippets.shortkey.error.systemConflict': '此快捷键与系统快捷键冲突',
|
||||
'snippets.shortkey.error.snippetConflict': '此快捷键已被代码片段使用:{name}',
|
||||
|
||||
'snippets.variables.dialogTitle': '填写变量',
|
||||
'snippets.variables.dialogDesc': '运行「{label}」前请填写以下变量。',
|
||||
'snippets.variables.hint': '变量值将原样插入脚本(不会进行 shell 转义)。',
|
||||
'snippets.variables.preview': '预览',
|
||||
'snippets.variables.placeholder': '请输入',
|
||||
'snippets.variables.placeholderDefault': '默认:{value}',
|
||||
'snippets.variables.required': '请填写此变量',
|
||||
'snippets.variables.run': '运行',
|
||||
'snippets.field.variablesHelp': '在脚本中使用 {{名称}} 或 {{名称:默认值}} 定义变量。',
|
||||
'snippets.field.variablesDetected': '变量',
|
||||
'snippets.field.variableDefault': '默认 {value}',
|
||||
|
||||
// Serial Port
|
||||
'serial.button': '串口',
|
||||
'serial.modal.title': '连接串口',
|
||||
'serial.modal.desc': '配置串口连接参数',
|
||||
'serial.field.port': '串口',
|
||||
'serial.field.selectPort': '选择串口...',
|
||||
'serial.field.baudRate': '波特率',
|
||||
'serial.field.dataBits': '数据位',
|
||||
'serial.field.stopBits': '停止位',
|
||||
'serial.field.stopBits15Warning': '1.5 停止位在 Windows 下可能不被所有设备支持',
|
||||
'serial.field.parity': '校验位',
|
||||
'serial.field.flowControl': '流控制',
|
||||
'serial.noPorts': '未检测到串口设备。请连接设备后刷新。',
|
||||
'serial.field.customPort': '自定义串口路径',
|
||||
'serial.field.customPortPlaceholder': '例如 /dev/ttys001 或 COM1',
|
||||
'serial.type.hardware': '硬件',
|
||||
'serial.type.pseudo': '虚拟终端',
|
||||
'serial.type.custom': '自定义',
|
||||
'serial.parity.none': '无',
|
||||
'serial.parity.even': '偶校验',
|
||||
'serial.parity.odd': '奇校验',
|
||||
'serial.parity.mark': 'Mark',
|
||||
'serial.parity.space': 'Space',
|
||||
'serial.flowControl.none': '无',
|
||||
'serial.flowControl.xon/xoff': 'XON/XOFF (软件)',
|
||||
'serial.flowControl.rts/cts': 'RTS/CTS (硬件)',
|
||||
'serial.field.localEcho': '强制本地回显',
|
||||
'serial.field.localEchoDesc': '本地回显输入字符(用于没有远程回显的设备)',
|
||||
'serial.field.lineMode': '行模式',
|
||||
'serial.field.lineModeDesc': '缓冲输入,按回车后发送(而不是逐字符发送)',
|
||||
'serial.field.charset': '字符编码',
|
||||
'serial.connectionError': '连接串口失败',
|
||||
'serial.field.baudRatePlaceholder': '选择或输入波特率...',
|
||||
'serial.field.baudRateEmpty': '输入自定义波特率',
|
||||
'serial.field.customBaudRate': '使用自定义波特率',
|
||||
'serial.field.saveConfig': '保存配置',
|
||||
'serial.field.saveConfigDesc': '将此串口配置保存到主机列表以便快速访问',
|
||||
'serial.field.configLabel': '配置名称',
|
||||
'serial.field.configLabelPlaceholder': '例如 Arduino Uno',
|
||||
'serial.connectAndSave': '连接并保存',
|
||||
'serial.edit.title': '串口设置',
|
||||
|
||||
// Keyboard Interactive Authentication (2FA/MFA)
|
||||
'keyboard.interactive.title': '需要验证',
|
||||
'keyboard.interactive.desc': '服务器需要额外的身份验证。',
|
||||
'keyboard.interactive.descWithHost': '服务器 {hostname} 需要额外的身份验证。',
|
||||
'keyboard.interactive.response': '响应',
|
||||
'keyboard.interactive.enterCode': '输入验证码',
|
||||
'keyboard.interactive.enterResponse': '输入响应',
|
||||
'keyboard.interactive.submit': '提交',
|
||||
'keyboard.interactive.verifying': '验证中...',
|
||||
'keyboard.interactive.savePassword': '保存密码',
|
||||
|
||||
// Passphrase Modal for encrypted SSH keys
|
||||
'passphrase.title': 'SSH 密钥密码',
|
||||
'passphrase.desc': '请输入 {keyName} 的密码',
|
||||
'passphrase.descWithHost': '请输入 {keyName} 的密码以连接到 {hostname}',
|
||||
'passphrase.label': '密码',
|
||||
'passphrase.keyPath': '密钥',
|
||||
'passphrase.unlock': '解锁',
|
||||
'passphrase.unlocking': '解锁中...',
|
||||
'passphrase.skip': '跳过',
|
||||
'passphrase.remember': '记住此密码',
|
||||
|
||||
// Text Editor
|
||||
'sftp.editor.wordWrap': '自动换行',
|
||||
'sftp.editor.maximize': '最大化',
|
||||
'sftp.editor.unsavedTitle': '未保存的修改',
|
||||
'sftp.editor.unsavedMessage': '{fileName} 有未保存的修改,是否保存后关闭?',
|
||||
'sftp.editor.discardChanges': '不保存',
|
||||
'sftp.editor.saveAndClose': '保存并关闭',
|
||||
'sftp.editor.quitBlockedByDirty': '存在未保存的编辑器,请先处理后再退出',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,829 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhCNVaultMessages: Messages = {
|
||||
// Notes
|
||||
'notes.search.placeholder': '搜索笔记、主机、标签...',
|
||||
'notes.action.newNote': '新建笔记',
|
||||
'notes.action.newGroup': '新建文件夹',
|
||||
'notes.action.importMarkdown': '导入 Markdown',
|
||||
'notes.action.exportNote': '导出笔记',
|
||||
'notes.action.exportGroup': '导出文件夹',
|
||||
'notes.action.exportAll': '导出全部笔记',
|
||||
'notes.import.toast.success': '已导入 {count} 条笔记',
|
||||
'notes.import.toast.noNotes': '没有导入任何 Markdown 文件。',
|
||||
'notes.import.toast.failed': '导入 Markdown 文件失败。',
|
||||
'notes.import.toast.inProgress': '正在导入 Markdown 文件,请稍候。',
|
||||
'notes.import.toast.skipped': '已跳过 {count} 个不支持的文件。',
|
||||
'notes.export.toast.success': '已导出 {count} 条笔记',
|
||||
'notes.export.toast.empty': '没有可导出的笔记。',
|
||||
'notes.export.toast.failed': '导出笔记失败。',
|
||||
'notes.codeBlock.copied': '已复制',
|
||||
'notes.codeBlock.copyFailed': '复制代码块失败。',
|
||||
'notes.group.prompt': '文件夹路径',
|
||||
'notes.group.all': '笔记本',
|
||||
'notes.group.ungrouped': '未分组',
|
||||
'notes.section.notebooks': '笔记本',
|
||||
'notes.section.notes': '笔记',
|
||||
'notes.mode.edit': '编辑',
|
||||
'notes.mode.split': '分屏',
|
||||
'notes.mode.preview': '预览',
|
||||
'notes.title.placeholder': '笔记标题',
|
||||
'notes.editor.placeholder': '在这里写 Markdown 笔记...',
|
||||
'notes.preview.empty': '暂无内容可预览。',
|
||||
'notes.empty.title': '还没有笔记',
|
||||
'notes.empty.desc': '可以记录运维手册、临时备忘、服务器交接信息。',
|
||||
'notes.empty.group': '这个笔记本里还没有笔记。',
|
||||
'notes.search.noResults': '没有找到笔记。',
|
||||
|
||||
// Select Host panel
|
||||
'selectHost.title': '选择主机',
|
||||
'selectHost.noHostsFound': '未找到主机',
|
||||
'selectHost.newHost': '新建主机',
|
||||
'selectHost.continue': '继续',
|
||||
'selectHost.continueWithCount': '继续(已选 {count} 个)',
|
||||
|
||||
// Quick Connect
|
||||
'quickConnect.knownHost.title': '确认要连接吗?',
|
||||
'quickConnect.knownHost.authenticity': '无法验证 {hostname} 的真实性。',
|
||||
'quickConnect.knownHost.fingerprintLabel': '{keyType} fingerprint (SHA256):',
|
||||
'quickConnect.knownHost.addQuestion': '是否将它加入 Known Hosts?',
|
||||
'quickConnect.knownHost.addAndContinue': '加入并继续',
|
||||
'quickConnect.addKey': '添加 key',
|
||||
'quickConnect.warning.unparsedOptions': '部分 SSH 参数已被忽略: {options}',
|
||||
|
||||
// Protocol select dialog
|
||||
'protocolSelect.chooseProtocol': '选择协议',
|
||||
'protocolSelect.port': '端口:',
|
||||
// Host Details
|
||||
'hostDetails.title.details': '主机详情',
|
||||
'hostDetails.title.new': '新建主机',
|
||||
'vault.panel.resizeWidth': '调整面板宽度',
|
||||
'hostDetails.saveAria': '保存',
|
||||
'hostDetails.section.address': '地址',
|
||||
'hostDetails.hostname.placeholder': 'IP 或 主机名',
|
||||
'hostDetails.section.general': '通用',
|
||||
'hostDetails.section.sftp': 'SFTP 设置',
|
||||
'hostDetails.sftp.sudo': 'Sudo 提权模式',
|
||||
'hostDetails.sftp.sudo.desc': '使用保存的密码自动获取 Root 权限',
|
||||
'hostDetails.sftp.sudo.passwordWarning': 'Sudo 模式需要密码。请在上方配置密码,或确保服务器允许免密 sudo。',
|
||||
'hostDetails.sftp.encoding': '文件名编码',
|
||||
'hostDetails.sftp.encoding.desc': '选择用于解码和发送 SFTP 文件名的编码。',
|
||||
'hostDetails.label.placeholder': '名称(例如:Production Server)',
|
||||
'hostDetails.notes.label': '备注',
|
||||
'hostDetails.notes.placeholder': '硬件配置、项目、客户、地域、角色...',
|
||||
'hostDetails.notes.help': '支持 Markdown。请勿在此存放密码或私钥。',
|
||||
'hostDetails.notes.tab.edit': '编辑',
|
||||
'hostDetails.notes.tab.preview': '预览',
|
||||
'hostDetails.notes.preview.empty': '暂无内容可预览。',
|
||||
'hostDetails.notes.toggle.show': '显示备注编辑框',
|
||||
'hostDetails.notes.toggle.hide': '隐藏备注编辑框',
|
||||
'hostDetails.group.placeholder': '父级 Group',
|
||||
'hostDetails.section.credentials': '凭据',
|
||||
'hostDetails.section.portCredentials': '端口与凭据',
|
||||
'hostDetails.section.appearance': '外观',
|
||||
'hostDetails.distro.title': 'Linux 发行版',
|
||||
'hostDetails.distro.desc': '可在连接后自动探测,也可以手动覆盖图标所用的发行版。',
|
||||
'hostDetails.icon.title': '主机图标',
|
||||
'hostDetails.icon.sectionTitle': '颜色与图标',
|
||||
'hostDetails.icon.desc': '选择此主机使用的图标来源和颜色。',
|
||||
'hostDetails.icon.source': '来源',
|
||||
'hostDetails.icon.manualLabel': '手动指定',
|
||||
'hostDetails.icon.tab.brand': '品牌',
|
||||
'hostDetails.icon.tab.type': '类型',
|
||||
'hostDetails.icon.mode.auto': '自动',
|
||||
'hostDetails.icon.mode.custom': '自定义',
|
||||
'hostDetails.icon.reset': '重置主机图标',
|
||||
'hostDetails.icon.colorLabel': '图标颜色',
|
||||
'hostDetails.icon.colorDefault': '默认',
|
||||
'hostDetails.icon.colorAuto': '自动',
|
||||
'hostDetails.icon.colorManual': '手动',
|
||||
'hostDetails.icon.colorChoice': '颜色',
|
||||
'hostDetails.icon.autoColorLabel': '当前颜色',
|
||||
'hostDetails.icon.preview': '图标预览',
|
||||
'hostDetails.icon.showLibrary': '展开图标库',
|
||||
'hostDetails.icon.hideLibrary': '收起图标库',
|
||||
'hostDetails.icon.autoUsesDistro': '使用 Linux 发行版图标和所选颜色显示此主机。',
|
||||
'hostDetails.icon.customOverridesDistro': '内置图标会替换此主机的 Linux 发行版图标。',
|
||||
'hostDetails.icon.option.server': '服务器',
|
||||
'hostDetails.icon.option.terminal': '终端',
|
||||
'hostDetails.icon.option.database': '数据库',
|
||||
'hostDetails.icon.option.cloud': '云主机',
|
||||
'hostDetails.icon.option.router': '路由器',
|
||||
'hostDetails.icon.option.shield': '安全',
|
||||
'hostDetails.icon.option.code': '代码',
|
||||
'hostDetails.icon.option.box': '节点',
|
||||
'hostDetails.icon.option.globe': '公网',
|
||||
'hostDetails.icon.option.cpu': '计算',
|
||||
'hostDetails.icon.option.hard-drive': '存储',
|
||||
'hostDetails.icon.option.network': '网络',
|
||||
'hostDetails.icon.option.wifi': '无线',
|
||||
'hostDetails.icon.option.lock': '锁定',
|
||||
'hostDetails.icon.option.key': '密钥',
|
||||
'hostDetails.icon.option.monitor': '显示器',
|
||||
'hostDetails.icon.option.container': '容器',
|
||||
'hostDetails.icon.option.activity': '活动',
|
||||
'hostDetails.icon.option.zap': '高速',
|
||||
'hostDetails.icon.option.server-cog': '服务器设置',
|
||||
'hostDetails.icon.color.blue': '蓝色',
|
||||
'hostDetails.icon.color.green': '绿色',
|
||||
'hostDetails.icon.color.red': '红色',
|
||||
'hostDetails.icon.color.amber': '琥珀色',
|
||||
'hostDetails.icon.color.purple': '紫色',
|
||||
'hostDetails.icon.color.cyan': '青色',
|
||||
'hostDetails.icon.color.orange': '橙色',
|
||||
'hostDetails.icon.color.slate': '石板灰',
|
||||
'hostDetails.icon.color.violet': '紫罗兰',
|
||||
'hostDetails.icon.color.pink': '粉色',
|
||||
'hostDetails.icon.color.rose': '玫瑰红',
|
||||
'hostDetails.icon.color.lime': '青柠',
|
||||
'hostDetails.icon.color.teal': '蓝绿色',
|
||||
'hostDetails.icon.color.sky': '天蓝',
|
||||
'hostDetails.icon.color.indigo': '靛蓝',
|
||||
'hostDetails.icon.color.zinc': '锌灰',
|
||||
'hostDetails.icon.color.custom': '自定义颜色',
|
||||
'hostDetails.distro.mode': '来源',
|
||||
'hostDetails.distro.mode.auto': '自动探测',
|
||||
'hostDetails.distro.mode.manual': '手动覆盖',
|
||||
'hostDetails.distro.detectedLabel': '当前值',
|
||||
'hostDetails.distro.manualLabel': '手动指定',
|
||||
'hostDetails.distro.pending': '首次连接后自动探测',
|
||||
'hostDetails.distro.unknown': '未知',
|
||||
'hostDetails.distro.option.linux': '通用 Linux',
|
||||
'hostDetails.distro.option.ubuntu': 'Ubuntu',
|
||||
'hostDetails.distro.option.debian': 'Debian',
|
||||
'hostDetails.distro.option.centos': 'CentOS',
|
||||
'hostDetails.distro.option.rocky': 'Rocky Linux',
|
||||
'hostDetails.distro.option.fedora': 'Fedora',
|
||||
'hostDetails.distro.option.arch': 'Arch Linux',
|
||||
'hostDetails.distro.option.alpine': 'Alpine',
|
||||
'hostDetails.distro.option.amazon': 'Amazon Linux',
|
||||
'hostDetails.distro.option.opensuse': 'openSUSE / SLES',
|
||||
'hostDetails.distro.option.redhat': 'Red Hat / RHEL',
|
||||
'hostDetails.distro.option.almalinux': 'AlmaLinux',
|
||||
'hostDetails.distro.option.alinux': '阿里云 Linux',
|
||||
'hostDetails.distro.option.openeuler': 'openEuler',
|
||||
'hostDetails.distro.option.oracle': 'Oracle Linux',
|
||||
'hostDetails.distro.option.kali': 'Kali Linux',
|
||||
'hostDetails.distro.option.cisco': '思科',
|
||||
'hostDetails.distro.option.juniper': '瞻博网络',
|
||||
'hostDetails.distro.option.huawei': '华为',
|
||||
'hostDetails.distro.option.h3c': '新华三 / H3C',
|
||||
'hostDetails.distro.option.hpe': '慧与 / HPE',
|
||||
'hostDetails.distro.option.mikrotik': 'MikroTik',
|
||||
'hostDetails.distro.option.fortinet': '飞塔',
|
||||
'hostDetails.distro.option.paloalto': 'Palo Alto Networks',
|
||||
'hostDetails.distro.option.zyxel': '合勤',
|
||||
'hostDetails.distro.option.ruijie': '锐捷',
|
||||
'hostDetails.section.mosh': 'Mosh',
|
||||
'hostDetails.section.et': 'EternalTerminal',
|
||||
'hostDetails.et.port': 'ET 服务端口',
|
||||
'hostDetails.et.port.desc': 'etserver 监听端口(默认 2022)',
|
||||
'hostDetails.username.placeholder': '用户名',
|
||||
'hostDetails.password.placeholder': '密码',
|
||||
'hostDetails.password.show': '显示密码',
|
||||
'hostDetails.password.hide': '隐藏密码',
|
||||
'hostDetails.password.save': '保存密码',
|
||||
'hostDetails.identity.suggestions': '身份',
|
||||
'hostDetails.identity.missing': '身份不存在',
|
||||
'hostDetails.credential.keyCertificate': '密钥 / 证书 / 本地密钥',
|
||||
'hostDetails.credential.key': '密钥',
|
||||
'hostDetails.credential.certificate': '证书',
|
||||
'hostDetails.credential.localKeyFile': '本地密钥文件',
|
||||
'hostDetails.credential.localKeyFilePlaceholder': '~/.ssh/id_ed25519',
|
||||
'hostDetails.credential.browseKeyFile': '浏览…',
|
||||
'hostDetails.credential.missing': '凭据不存在',
|
||||
'hostDetails.keys.search': '搜索密钥…',
|
||||
'hostDetails.keys.empty': '暂无密钥',
|
||||
'hostDetails.certs.search': '搜索证书…',
|
||||
'hostDetails.certs.empty': '暂无证书',
|
||||
'hostDetails.agentForwarding': '转发 SSH 密钥',
|
||||
'hostDetails.agentForwarding.desc': '允许远程服务器使用本地 SSH 密钥(例如用于 git 操作)',
|
||||
'hostDetails.agentForwarding.agentNotRunning': 'SSH Agent 不可用',
|
||||
'hostDetails.agentForwarding.agentNotRunningHint': '未检测到 SSH Agent。请启用 Windows OpenSSH Authentication Agent 服务,或使用兼容的 Agent(如 Bitwarden、1Password、gpg-agent)。',
|
||||
'hostDetails.section.agentForwarding': 'SSH 代理',
|
||||
'hostDetails.x11Forwarding': '转发 X11 图形应用',
|
||||
'hostDetails.x11Forwarding.desc': '本机运行 X 服务时,让远程图形程序显示在本地桌面。',
|
||||
'hostDetails.section.x11Forwarding': 'X11 转发',
|
||||
'hostDetails.section.deviceType': '设备类型',
|
||||
'hostDetails.deviceType': '网络设备模式',
|
||||
'hostDetails.deviceType.desc': '适用于通过 SSH 连接的网络设备(交换机、路由器、防火墙)。命令将原样发送,不进行 Shell 包装,兼容华为 VRP、Cisco IOS 等厂商 CLI。',
|
||||
'hostDetails.deviceType.warning': 'AI 代理命令将直接发送,无法获取退出码。仅建议在设备不运行标准 Shell 时启用。',
|
||||
'hostDetails.section.sshAlgorithms': 'SSH 算法',
|
||||
'hostDetails.section.terminalBehavior': '终端行为',
|
||||
'hostDetails.lineTimestamps': '显示输出时间',
|
||||
'hostDetails.lineTimestamps.desc': '在终端输出行旁边显示本地时间,不改变终端文本内容。',
|
||||
'hostDetails.legacyAlgorithms': '允许旧版算法',
|
||||
'hostDetails.legacyAlgorithms.desc': '启用已弃用的 SSH 算法(diffie-hellman-group1、ssh-dss、3des-cbc 等)以连接老旧网络设备。',
|
||||
'hostDetails.legacyAlgorithms.warning': '这些算法存在已知安全漏洞,仅建议在老旧设备不支持现代加密时启用。',
|
||||
'hostDetails.skipEcdsaHostKey': '跳过 ECDSA 主机密钥',
|
||||
'hostDetails.skipEcdsaHostKey.desc': '某些老款华为 / 思科交换机的 ECDSA 主机密钥签名不规范,会导致连接报 "signature verification failed"。开启后客户端不再 advertise ecdsa-sha2-*,强制使用 RSA / Ed25519。',
|
||||
'hostDetails.algorithms.advanced': '高级算法配置',
|
||||
'hostDetails.algorithms.advanced.desc': '针对单个 host 自定义各分类的算法清单。不勾选 = 使用默认;勾选子集后将完全替换默认列表。配置错误可能导致无法连接。',
|
||||
'hostDetails.algorithms.inheritedNotice': '当前组已设置以下分类的算法 override:{categories}。本面板的"恢复默认"只会回到组的设置,而不是 NetCatty 默认列表。若要忽略组的限制,请到组的算法设置里取消。',
|
||||
'hostDetails.algorithms.customized': '已自定义',
|
||||
'hostDetails.algorithms.reset': '恢复默认',
|
||||
'hostDetails.algorithms.category.kex': '密钥交换 (KEX)',
|
||||
'hostDetails.algorithms.category.cipher': '加密算法 (Cipher)',
|
||||
'hostDetails.algorithms.category.hmac': '完整性算法 (HMAC)',
|
||||
'hostDetails.algorithms.category.serverHostKey': '主机密钥 (Host Key)',
|
||||
'hostDetails.algorithms.category.compress': '压缩 (Compression)',
|
||||
'hostDetails.section.keepalive': '会话保活',
|
||||
'hostDetails.keepalive.override': '为此主机单独配置',
|
||||
'hostDetails.keepalive.desc': '为该主机使用专属的保活策略,而不是跟随全局设置。适用于不响应 keepalive@openssh.com 请求的老旧路由器 / 交换机——将间隔设为 0 可对该主机彻底关闭保活。',
|
||||
'hostDetails.keepalive.interval': '间隔(秒)',
|
||||
'hostDetails.keepalive.countMax': '最大无响应保活次数',
|
||||
'hostDetails.keepalive.disabledHint': '间隔为 0 时该主机不发送保活包,仅依赖 TCP 层超时检测断连。',
|
||||
'hostDetails.backspaceBehavior': 'Backspace 行为',
|
||||
'hostDetails.backspaceBehavior.default': '默认',
|
||||
'hostDetails.jumpHosts': '通过主机代理',
|
||||
'hostDetails.jumpHosts.hops': '{count} 跳',
|
||||
'hostDetails.jumpHosts.direct': '直连',
|
||||
'hostDetails.jumpHosts.configure': '配置代理主机',
|
||||
'hostDetails.proxy': '通过 HTTP/SOCKS5/命令代理',
|
||||
'hostDetails.proxy.none': '无',
|
||||
'hostDetails.proxy.edit': '编辑代理',
|
||||
'hostDetails.proxy.configure': '配置代理',
|
||||
'hostDetails.envVars': '环境变量',
|
||||
'hostDetails.envVars.add': '添加环境变量',
|
||||
'hostDetails.startupCommand': '启动命令',
|
||||
'hostDetails.startupCommand.placeholder': '连接后执行的命令(例如:cd /app && ls)',
|
||||
'hostDetails.startupCommand.help': 'SSH 连接建立后将自动执行该命令。',
|
||||
'hostDetails.startupCommand.runMode': '多行运行方式',
|
||||
'hostDetails.startupCommand.runMode.help': '选择连接后如何发送多行启动命令。',
|
||||
'hostDetails.startupCommand.runMode.lineDelay': '逐行发送',
|
||||
'hostDetails.startupCommand.runMode.paste': '一次性发送',
|
||||
'hostDetails.otherProtocols': '其他协议',
|
||||
'hostDetails.telnetOn': 'Telnet on',
|
||||
'hostDetails.port': '端口',
|
||||
'hostDetails.telnet.credentials': '凭据',
|
||||
'hostDetails.telnet.identity.placeholder': '手动身份',
|
||||
'hostDetails.telnet.username': 'Telnet 用户名',
|
||||
'hostDetails.telnet.password': 'Telnet 密码',
|
||||
'hostDetails.charset.placeholder': '字符集(例如 UTF-8)',
|
||||
'hostDetails.telnet.add': '添加 Telnet 协议',
|
||||
'hostDetails.telnet.setDefault': '默认用 Telnet 连接',
|
||||
'hostDetails.tags': '标签',
|
||||
'hostDetails.group': '分组',
|
||||
'hostDetails.selectGroup': '选择分组',
|
||||
'hostDetails.addTag': '添加标签...',
|
||||
'hostDetails.createTag': '创建标签',
|
||||
'hostDetails.createGroup': '创建分组',
|
||||
|
||||
// Host form (legacy modal)
|
||||
'hostForm.title.edit': '编辑主机',
|
||||
'hostForm.title.new': '新建主机',
|
||||
'hostForm.desc.edit': '更新该主机的连接信息',
|
||||
'hostForm.desc.new': '创建一个新的 SSH 主机条目',
|
||||
'hostForm.field.label': '名称',
|
||||
'hostForm.placeholder.label': 'My Production Server',
|
||||
'hostForm.field.hostname': 'Hostname / IP',
|
||||
'hostForm.placeholder.hostname': '192.168.1.1',
|
||||
'hostForm.field.port': '端口',
|
||||
'hostForm.field.username': '用户名',
|
||||
'hostForm.field.osType': '操作系统类型',
|
||||
'hostForm.placeholder.selectOs': '选择操作系统',
|
||||
'hostForm.field.group': '分组',
|
||||
'hostForm.placeholder.group': '例如:AWS、DigitalOcean',
|
||||
'hostForm.field.tags': '标签',
|
||||
'hostForm.placeholder.addTag': '添加标签…',
|
||||
'hostForm.auth.method': '认证方式',
|
||||
'hostForm.auth.password': '密码',
|
||||
'hostForm.auth.sshKey': 'SSH密钥',
|
||||
'hostForm.auth.selectKey': '选择 SSH密钥',
|
||||
'hostForm.auth.noKeys': '暂无密钥',
|
||||
'hostForm.auth.noKeysHint': '钥匙串中未找到 SSH密钥,请先创建一个。',
|
||||
'hostForm.saveHost': '保存主机',
|
||||
|
||||
// Connection logs
|
||||
'logs.table.date': '日期',
|
||||
'logs.table.user': '用户',
|
||||
'logs.table.host': '主机',
|
||||
'logs.table.saved': '收藏',
|
||||
'logs.empty.title': '暂无连接日志',
|
||||
'logs.empty.desc': '当你连接主机或打开本地终端后,这里会显示连接历史。',
|
||||
'logs.loadMore': '加载更多 ({count} 条)',
|
||||
'logs.ongoing': '进行中',
|
||||
'logs.localTerminal': '本地终端',
|
||||
'logs.action.save': '收藏',
|
||||
'logs.action.unsave': '取消收藏',
|
||||
'logs.action.delete': '删除',
|
||||
|
||||
// Log view
|
||||
'logView.customizeAppearance': '自定义外观',
|
||||
'logView.appearance': '外观',
|
||||
'logView.readOnly': '只读',
|
||||
'logView.export': '导出',
|
||||
|
||||
// Terminal toolbar / search / context menu / auth
|
||||
'terminal.toolbar.openSftp': '打开 SFTP',
|
||||
'terminal.toolbar.availableAfterConnect': '连接后可用',
|
||||
'terminal.toolbar.sendYmodem': 'YMODEM 发送',
|
||||
'terminal.toolbar.receiveYmodem': 'YMODEM 接收',
|
||||
'terminal.toolbar.sftp': 'SFTP',
|
||||
'terminal.toolbar.more': '更多操作',
|
||||
'terminal.toolbar.scripts': '脚本',
|
||||
'terminal.toolbar.history': '命令历史',
|
||||
'terminal.toolbar.library': '库',
|
||||
'terminal.toolbar.noSnippets': '暂无代码片段',
|
||||
'terminal.toolbar.terminalSettings': '终端设置',
|
||||
'terminal.toolbar.searchTerminal': '搜索终端',
|
||||
'terminal.toolbar.search': '搜索',
|
||||
'terminal.toolbar.startSessionLog': '开始会话日志',
|
||||
'terminal.toolbar.stopSessionLog': '停止会话日志',
|
||||
'terminal.toolbar.broadcast': '广播',
|
||||
'terminal.toolbar.broadcastEnable': '启用广播模式',
|
||||
'terminal.toolbar.broadcastDisable': '关闭广播模式',
|
||||
'terminal.toolbar.composeBar': '撰写栏',
|
||||
'terminal.composeBar.placeholder': '在此输入命令,按回车发送...',
|
||||
'terminal.composeBar.send': '发送',
|
||||
'terminal.composeBar.close': '关闭撰写栏',
|
||||
'terminal.composeBar.broadcasting': '正在广播到所有会话',
|
||||
'terminal.composeBar.resize': '拖拽调整撰写栏高度',
|
||||
'terminal.composeBar.manageSnippets': '管理快捷代码片段',
|
||||
'terminal.composeBar.searchSnippets': '搜索代码片段...',
|
||||
'terminal.composeBar.noPinnedSnippets': '点击 + 固定常用代码片段',
|
||||
'terminal.composeBar.noMatchingSnippets': '没有匹配的代码片段',
|
||||
'terminal.composeBar.pinnedCount': '已固定 {count} 个',
|
||||
'terminal.composeBar.unpinSnippet': '从快捷栏移除 {label}',
|
||||
'terminal.composeBar.snippetClickHint': '单击插入 · Shift+单击直接发送',
|
||||
'terminal.toolbar.focus': '聚焦',
|
||||
'terminal.toolbar.focusMode': '聚焦模式',
|
||||
'terminal.toolbar.detach': '移出到独立标签',
|
||||
'terminal.toolbar.encoding': '终端编码',
|
||||
'terminal.toolbar.encoding.utf8': 'UTF-8',
|
||||
'terminal.toolbar.encoding.gb18030': 'GB18030',
|
||||
'terminal.toolbar.closeSession': '关闭会话',
|
||||
'terminal.toolbar.hostHighlight.title': '主机关键字高亮',
|
||||
'terminal.toolbar.hostHighlight.noRules': '此主机未定义自定义高亮规则',
|
||||
'terminal.toolbar.hostHighlight.addRule': '添加新规则',
|
||||
'terminal.toolbar.hostHighlight.labelPlaceholder': '标签(例如:错误)',
|
||||
'terminal.toolbar.hostHighlight.patternPlaceholder': '正则表达式(例如:\\bfailed\\b)',
|
||||
'terminal.toolbar.hostHighlight.invalidPattern': '无效的正则表达式',
|
||||
'terminal.toolbar.hostHighlight.clearAll': '清除全部',
|
||||
'terminal.toolbar.hostHighlight.changeColor': '更改高亮颜色',
|
||||
'terminal.toolbar.hostHighlight.selectColor': '选择新规则的颜色',
|
||||
'terminal.statusbar.copyHostname.label': '复制主机地址',
|
||||
'terminal.statusbar.copyHostname.tooltip': '复制主机地址({hostname})',
|
||||
'terminal.statusbar.copyHostname.toast': '已复制主机地址:{hostname}',
|
||||
'terminal.statusbar.copyHostname.error': '复制主机地址失败',
|
||||
'terminal.serverStats.cpu': 'CPU 使用率',
|
||||
'terminal.serverStats.cpuCores': 'CPU 核心使用率',
|
||||
'terminal.serverStats.memory': '内存使用',
|
||||
'terminal.serverStats.memoryDetails': '内存详情',
|
||||
'terminal.serverStats.memUsed': '已用',
|
||||
'terminal.serverStats.memBuffers': '缓冲区',
|
||||
'terminal.serverStats.memCached': '缓存',
|
||||
'terminal.serverStats.memFree': '空闲',
|
||||
'terminal.serverStats.swap': '交换空间',
|
||||
'terminal.serverStats.swapUsed': '已用交换',
|
||||
'terminal.serverStats.swapFree': '空闲交换',
|
||||
'terminal.serverStats.swapTotal': '总计',
|
||||
'terminal.serverStats.topProcesses': '内存占用前十进程',
|
||||
'terminal.serverStats.disk': '磁盘使用(根分区)',
|
||||
'terminal.serverStats.diskDetails': '已挂载磁盘',
|
||||
'terminal.serverStats.network': '网络速度',
|
||||
'terminal.serverStats.latency': '延迟',
|
||||
'terminal.serverStats.networkDetails': '网络接口',
|
||||
'terminal.serverStats.noData': '暂无数据',
|
||||
'terminal.dragDrop.localTitle': '拖放以插入路径',
|
||||
'terminal.dragDrop.localMessage': '文件路径将被插入到终端',
|
||||
'terminal.dragDrop.remoteTitle': '拖放以上传文件',
|
||||
'terminal.dragDrop.remoteZmodemMessage': '文件将通过 ZMODEM(PTY)上传',
|
||||
'terminal.dragDrop.remoteSftpMessage': '文件将通过 SFTP 上传',
|
||||
'terminal.dragDrop.noFiles': '没有可上传的文件',
|
||||
'terminal.dragDrop.notConnected': '无法拖放文件 - 终端未连接',
|
||||
'terminal.dragDrop.errorTitle': '拖放错误',
|
||||
'terminal.dragDrop.errorMessage': '处理拖放文件失败',
|
||||
'terminal.search.placeholder': '搜索…',
|
||||
'terminal.search.noResults': '无结果',
|
||||
'terminal.search.prevMatch': '上一个匹配 (Shift+Enter)',
|
||||
'terminal.search.nextMatch': '下一个匹配 (Enter)',
|
||||
'terminal.menu.copy': '复制',
|
||||
'terminal.menu.paste': '粘贴',
|
||||
'terminal.menu.addSelectionToAI': '添加到对话',
|
||||
'terminal.menu.pasteSelection': '粘贴选中文本',
|
||||
'terminal.menu.selectAll': '全选',
|
||||
'terminal.menu.reconnect': '重新连接',
|
||||
'terminal.menu.sendYmodem': 'YMODEM 发送',
|
||||
'terminal.menu.receiveYmodem': 'YMODEM 接收',
|
||||
'terminal.menu.splitHorizontal': '水平分屏',
|
||||
'terminal.menu.splitVertical': '垂直分屏',
|
||||
'terminal.menu.clearBuffer': '清空缓冲区',
|
||||
'terminal.menu.closeTerminal': '关闭终端',
|
||||
'terminal.menu.rename': '重命名',
|
||||
'terminal.menu.detach': '从工作区移出',
|
||||
'terminal.menu.detachSession': '移出 {name}',
|
||||
'terminal.ymodem.selectFile': '选择要发送的文件',
|
||||
'terminal.ymodem.allFiles': '所有文件',
|
||||
'terminal.ymodem.started': '正在通过 YMODEM 发送 {fileName}',
|
||||
'terminal.ymodem.complete': 'YMODEM 已发送 {fileName}',
|
||||
'terminal.ymodem.failed': 'YMODEM 发送失败',
|
||||
'terminal.ymodem.selectReceiveDirectory': '选择接收文件保存位置',
|
||||
'terminal.ymodem.receiveStarted': '正在通过 YMODEM 接收...',
|
||||
'terminal.ymodem.receiveComplete': 'YMODEM 已接收 {fileName}',
|
||||
'terminal.ymodem.receiveCompleteMultiple': 'YMODEM 已接收 {count} 个文件',
|
||||
'terminal.ymodem.receiveEmpty': '没有接收到 YMODEM 文件',
|
||||
'terminal.ymodem.receiveFailed': 'YMODEM 接收失败',
|
||||
'terminal.ymodem.unavailable': 'YMODEM 当前不可用',
|
||||
'terminal.selection.addToAI': '添加到对话',
|
||||
'terminal.selection.addToAIDesc': '将选中的终端输出作为附件加入 AI 草稿',
|
||||
'terminal.auth.password': '密码',
|
||||
'terminal.auth.sshKey': 'SSH Key',
|
||||
'terminal.auth.username': '用户名',
|
||||
'terminal.auth.username.placeholder': 'root',
|
||||
'terminal.auth.passwordLabel': '密码',
|
||||
'terminal.auth.password.placeholder': '输入密码',
|
||||
'terminal.auth.passphrase': '密码短语',
|
||||
'terminal.auth.passphrase.placeholder': '可选:所选私钥的密码短语',
|
||||
'terminal.auth.certificate': '证书',
|
||||
'terminal.auth.selectKey': '选择密钥',
|
||||
'terminal.auth.retryMessage': '身份验证失败。请检查凭据后重试。',
|
||||
'terminal.auth.retryLog': '身份验证失败,请重试。',
|
||||
'terminal.auth.noKeysHint': '暂无密钥,请先在钥匙串中添加。',
|
||||
'terminal.auth.continueSave': '继续并保存',
|
||||
'terminal.auth.credentialsUnavailable': '当前设备无法解密已保存凭据,请重新输入并再次保存。',
|
||||
'terminal.auth.jumpCredentialsUnavailable': '某个跳板机的已保存凭据无法在当前设备解密,请到主机设置中重新填写。',
|
||||
'terminal.auth.proxyCredentialsUnavailable': '代理凭据无法在当前设备解密,请到主机设置中重新填写代理密码。',
|
||||
'terminal.auth.keyUnavailableFallbackPassword': '已保存 SSH 密钥在当前设备不可用,改用密码认证。',
|
||||
'terminal.connectionErrorTitle': '连接错误',
|
||||
'terminal.progress.timeoutIn': '将在 {seconds}s 后超时',
|
||||
'terminal.progress.waitingForUserInput': '等待用户输入',
|
||||
'terminal.progress.disconnected': '已断开',
|
||||
'terminal.progress.cancelling': '正在取消...',
|
||||
'terminal.progress.startOver': '重新开始',
|
||||
'terminal.connection.dismissDisconnectedDialog': '关闭断连提示',
|
||||
'terminal.connection.chainOf': 'Chain {current} / {total}',
|
||||
'terminal.connection.showLogs': '显示日志',
|
||||
'terminal.connection.hideLogs': '隐藏日志',
|
||||
'terminal.connection.protocol.ssh': 'SSH',
|
||||
'terminal.connection.protocol.telnet': 'Telnet',
|
||||
'terminal.connection.protocol.mosh': 'Mosh',
|
||||
'terminal.connection.protocol.serial': '串口',
|
||||
'terminal.connection.protocol.local': '本地终端',
|
||||
'terminal.hostKey.unknownTitle': '确认主机指纹',
|
||||
'terminal.hostKey.changedTitle': '主机指纹已变化',
|
||||
'terminal.hostKey.unknownDescription': '尚未确认 {host} 的真实性。',
|
||||
'terminal.hostKey.changedDescription': '{host} 的已保存指纹与当前服务器不一致。',
|
||||
'terminal.hostKey.fingerprintLabel': '{keyType} 指纹为 SHA256:',
|
||||
'terminal.hostKey.savedFingerprintLabel': '已保存的指纹',
|
||||
'terminal.hostKey.unknownHint': '如果这个指纹属于你预期连接的服务器,可以记住它。',
|
||||
'terminal.hostKey.changedHint': '只有在你确认这台主机确实变更过时才继续。',
|
||||
'terminal.hostKey.addAndContinue': '记住并继续',
|
||||
'terminal.hostKey.updateAndContinue': '更新并继续',
|
||||
'terminal.themeModal.title': 'Terminal 外观',
|
||||
'terminal.themeModal.tab.theme': '主题',
|
||||
'terminal.themeModal.tab.font': '字体',
|
||||
'terminal.themeModal.tab.custom': '自定义',
|
||||
'terminal.themeModal.globalTheme': '全局主题',
|
||||
'terminal.themeModal.globalFont': '全局字体',
|
||||
'terminal.themeModal.fontSize': '字体大小',
|
||||
'terminal.themeModal.fontWeight': '字体粗细',
|
||||
'terminal.themeModal.livePreview': '实时预览',
|
||||
'terminal.themeModal.themeType': '{type} 主题',
|
||||
'terminal.hiddenTheme.title': '当前隐藏主题',
|
||||
'terminal.hiddenTheme.desc': '这个主题已从手动选择列表中隐藏;当你选择其他可见主题后,它会被替换。',
|
||||
'topTabs.toggleTheme.systemExitTitle': '当前正在跟随系统主题',
|
||||
'topTabs.toggleTheme.systemExitMessage': '请到设置里选择固定的浅色或深色主题。',
|
||||
'topTabs.toggleTheme.openSettings': '打开设置',
|
||||
|
||||
// Custom Themes
|
||||
'terminal.customTheme.section': '自定义主题',
|
||||
'terminal.customTheme.yourThemes': '我的主题',
|
||||
'terminal.customTheme.new': '新建主题',
|
||||
'terminal.customTheme.newDesc': '克隆当前主题并自定义',
|
||||
'terminal.customTheme.newTitle': '新建自定义主题',
|
||||
'terminal.customTheme.editTitle': '编辑主题',
|
||||
'terminal.customTheme.import': '导入 .itermcolors',
|
||||
'terminal.customTheme.importDesc': '从 iTerm2 配色方案文件导入',
|
||||
'terminal.customTheme.importError': '无法解析所选文件,请确保它是有效的 .itermcolors XML 文件。',
|
||||
'terminal.customTheme.delete': '删除主题',
|
||||
'terminal.customTheme.confirmDelete': '确认删除',
|
||||
'terminal.customTheme.name': '名称',
|
||||
'terminal.customTheme.namePlaceholder': '我的自定义主题',
|
||||
'terminal.customTheme.type': '类型',
|
||||
'terminal.customTheme.group.general': '通用',
|
||||
'terminal.customTheme.group.normal': '标准色',
|
||||
'terminal.customTheme.group.bright': '高亮色',
|
||||
'terminal.customTheme.color.background': '背景',
|
||||
'terminal.customTheme.color.foreground': '前景',
|
||||
'terminal.customTheme.color.cursor': '光标',
|
||||
'terminal.customTheme.color.selection': '选区',
|
||||
'terminal.customTheme.color.black': '黑色',
|
||||
'terminal.customTheme.color.red': '红色',
|
||||
'terminal.customTheme.color.green': '绿色',
|
||||
'terminal.customTheme.color.yellow': '黄色',
|
||||
'terminal.customTheme.color.blue': '蓝色',
|
||||
'terminal.customTheme.color.magenta': '品红',
|
||||
'terminal.customTheme.color.cyan': '青色',
|
||||
'terminal.customTheme.color.white': '白色',
|
||||
'terminal.customTheme.color.brightBlack': '亮黑',
|
||||
'terminal.customTheme.color.brightRed': '亮红',
|
||||
'terminal.customTheme.color.brightGreen': '亮绿',
|
||||
'terminal.customTheme.color.brightYellow': '亮黄',
|
||||
'terminal.customTheme.color.brightBlue': '亮蓝',
|
||||
'terminal.customTheme.color.brightMagenta': '亮品红',
|
||||
'terminal.customTheme.color.brightCyan': '亮青色',
|
||||
'terminal.customTheme.color.brightWhite': '亮白',
|
||||
|
||||
'cloudSync.gate.title': '端到端加密同步',
|
||||
'cloudSync.gate.desc':
|
||||
'数据会在本地加密后再同步,云端不会看到明文。设置主密钥以启用安全同步。',
|
||||
'cloudSync.gate.masterKey': '主密钥',
|
||||
'cloudSync.gate.confirmMasterKey': '确认主密钥',
|
||||
'cloudSync.gate.placeholder': '输入一个强密码',
|
||||
'cloudSync.gate.confirmPlaceholder': '再次输入密码',
|
||||
'cloudSync.gate.mismatch': '两次输入的密码不一致',
|
||||
'cloudSync.gate.warning':
|
||||
'我已了解:如果忘记主密钥,数据无法恢复,且没有密码重置功能。',
|
||||
'cloudSync.gate.enableVault': '启用加密 Vault',
|
||||
'cloudSync.gate.enabledToast': '已启用加密 Vault',
|
||||
'cloudSync.gate.setupFailed': '设置主密钥失败',
|
||||
'cloudSync.passwordStrength.tooShort': '太短',
|
||||
'cloudSync.passwordStrength.weak': '弱',
|
||||
'cloudSync.passwordStrength.moderate': '一般',
|
||||
'cloudSync.passwordStrength.strong': '强',
|
||||
'cloudSync.passwordStrength.veryStrong': '非常强',
|
||||
'cloudSync.provider.notConnected': '未连接',
|
||||
'cloudSync.provider.sync': '同步',
|
||||
'cloudSync.provider.connect': '连接',
|
||||
'cloudSync.provider.connecting': '连接中...',
|
||||
'cloudSync.provider.webdav': 'WebDAV',
|
||||
'cloudSync.provider.webdav.desc': '连接到自建 WebDAV 端点',
|
||||
'cloudSync.provider.s3': 'S3 兼容存储',
|
||||
'cloudSync.provider.s3.desc': '连接到 S3 兼容对象存储',
|
||||
'cloudSync.provider.comingSoon': '即将支持',
|
||||
'cloudSync.webdav.title': 'WebDAV 设置',
|
||||
'cloudSync.webdav.desc': '配置 WebDAV 端点用于加密同步。',
|
||||
'cloudSync.webdav.endpoint': '端点地址',
|
||||
'cloudSync.webdav.authType': '认证方式',
|
||||
'cloudSync.webdav.auth.basic': 'Basic',
|
||||
'cloudSync.webdav.auth.digest': 'Digest',
|
||||
'cloudSync.webdav.auth.token': 'Token',
|
||||
'cloudSync.webdav.username': '用户名',
|
||||
'cloudSync.webdav.password': '密码',
|
||||
'cloudSync.webdav.token': 'Token',
|
||||
'cloudSync.webdav.showSecret': '显示密钥',
|
||||
'cloudSync.webdav.allowInsecure': '允许不安全的连接(忽略证书错误)',
|
||||
'cloudSync.webdav.validation.endpoint': '请输入有效的 WebDAV 端点。',
|
||||
'cloudSync.webdav.validation.credentials': '请输入用户名和密码。',
|
||||
'cloudSync.webdav.validation.token': '请输入 Token。',
|
||||
'cloudSync.s3.title': 'S3 设置',
|
||||
'cloudSync.s3.desc': '连接到 S3 兼容对象存储以进行加密同步。',
|
||||
'cloudSync.s3.endpoint': '端点地址',
|
||||
'cloudSync.s3.region': 'Region',
|
||||
'cloudSync.s3.bucket': 'Bucket',
|
||||
'cloudSync.s3.accessKeyId': 'Access Key ID',
|
||||
'cloudSync.s3.secretAccessKey': 'Secret Access Key',
|
||||
'cloudSync.s3.sessionToken': 'Session Token(可选)',
|
||||
'cloudSync.s3.prefix': 'Key 前缀(可选)',
|
||||
'cloudSync.s3.forcePathStyle': '强制使用 path-style URL(适用于 MinIO/R2 等)',
|
||||
'cloudSync.s3.allowInsecure': '允许不安全的连接(忽略证书错误)',
|
||||
'cloudSync.s3.showSecret': '显示密钥',
|
||||
'cloudSync.s3.validation.required': '端点、Region、Bucket、Access Key 与 Secret 必填。',
|
||||
'cloudSync.smb.title': 'SMB 设置',
|
||||
'cloudSync.smb.desc': '连接到 SMB/CIFS 文件共享以进行加密同步。',
|
||||
'cloudSync.smb.share': '共享路径',
|
||||
'cloudSync.smb.username': '用户名',
|
||||
'cloudSync.smb.password': '密码',
|
||||
'cloudSync.smb.domain': '域(可选)',
|
||||
'cloudSync.smb.domainPlaceholder': '例如:WORKGROUP',
|
||||
'cloudSync.smb.port': '端口(可选)',
|
||||
'cloudSync.smb.showSecret': '显示密码',
|
||||
'cloudSync.smb.validation.share': '共享路径必填。',
|
||||
'cloudSync.smb.validation.port': '端口必须是 1 到 65535 之间的数字。',
|
||||
'cloudSync.connect.smb.success': 'SMB 已连接',
|
||||
'cloudSync.connect.smb.failedTitle': 'SMB 连接失败',
|
||||
'cloudSync.provider.smb': 'SMB 共享',
|
||||
'cloudSync.connect.webdav.success': 'WebDAV 已连接',
|
||||
'cloudSync.connect.webdav.failedTitle': 'WebDAV 连接失败',
|
||||
'cloudSync.connect.s3.success': 'S3 已连接',
|
||||
'cloudSync.connect.s3.failedTitle': 'S3 连接失败',
|
||||
'cloudSync.lastSync.never': '从未',
|
||||
'cloudSync.lastSync.justNow': '刚刚',
|
||||
'cloudSync.lastSync.minutesAgo': '{minutes} 分钟前',
|
||||
'cloudSync.changeKey': '更改 Key',
|
||||
'cloudSync.providers.title': '云服务',
|
||||
'cloudSync.syncAll': '同步所有已连接的服务',
|
||||
'cloudSync.autoSync.title': '自动同步',
|
||||
'cloudSync.autoSync.desc': '发生变更时自动同步',
|
||||
'cloudSync.strategy.title': '同步策略',
|
||||
'cloudSync.strategy.desc': '当本地和云端都发生变化时,选择如何处理。',
|
||||
'cloudSync.strategy.smartMerge': '智能合并(推荐)',
|
||||
'cloudSync.strategy.smartMergeDesc': '尽量保留两边的变化;如果无法安全判断,会再让你手动选择。',
|
||||
'cloudSync.strategy.preferCloud': '云端优先',
|
||||
'cloudSync.strategy.preferCloudDesc': '两边都有变化时,下载云端版本,并替换本地变化。',
|
||||
'cloudSync.strategy.preferLocal': '本地优先',
|
||||
'cloudSync.strategy.preferLocalDesc': '两边都有变化时,上传本地版本,并替换云端变化。',
|
||||
'cloudSync.status.title': '同步状态',
|
||||
'cloudSync.status.localVersion': '本地版本',
|
||||
'cloudSync.status.remoteVersion': '远端版本',
|
||||
'cloudSync.history.title': '同步历史',
|
||||
'cloudSync.history.upload': '上传',
|
||||
'cloudSync.history.download': '下载',
|
||||
'cloudSync.history.resolved': '已解决',
|
||||
'cloudSync.history.error': '错误',
|
||||
'cloudSync.localBackups.title': '本地备份历史',
|
||||
'cloudSync.localBackups.desc': 'Netcatty 会在版本变化前,以及恢复主机库前,自动留下一份本地恢复点。',
|
||||
'cloudSync.localBackups.retentionTitle': '备份保留数量',
|
||||
'cloudSync.localBackups.retentionDesc': '设置 Netcatty 最多保留多少份本地备份。',
|
||||
'cloudSync.localBackups.maxCount': '最多保留',
|
||||
'cloudSync.localBackups.maxSaved': '已保存保留数量:{count}',
|
||||
'cloudSync.localBackups.maxInvalid': '请输入 1 到 100 之间的数字。',
|
||||
'cloudSync.localBackups.empty': '还没有本地备份。',
|
||||
'cloudSync.localBackups.reason.appVersionChange': '版本变化前',
|
||||
'cloudSync.localBackups.reason.beforeRestore': '恢复前',
|
||||
'cloudSync.localBackups.versionChange': '{from} -> {to}',
|
||||
'cloudSync.localBackups.counts': '{hosts} 台主机,{keys} 个密钥,{snippets} 个代码片段,{notes} 条笔记',
|
||||
'cloudSync.localBackups.restore': '恢复',
|
||||
'cloudSync.localBackups.restoreSuccess': '已恢复本地备份。',
|
||||
'cloudSync.localBackups.restoreFailedTitle': '恢复失败',
|
||||
'cloudSync.localBackups.restoreMissing': '找不到这份备份。',
|
||||
'cloudSync.localBackups.protectiveBackupFailed': '无法创建保护性备份,已中止恢复以避免覆盖当前数据。请先解决底层问题(例如钥匙串访问)后重试。详情:{message}',
|
||||
'cloudSync.localBackups.restoreConfirmTitle': '确认恢复此备份?',
|
||||
'cloudSync.localBackups.restoreConfirmDesc': '当前的主机、密钥、代码片段与设置将被替换为此备份中的内容。系统会先自动创建一个保护性快照,便于撤销。',
|
||||
'cloudSync.localBackups.restoreConfirmButton': '恢复',
|
||||
'cloudSync.localBackups.restoreConfirmCancel': '取消',
|
||||
'cloudSync.localBackups.unavailableTitle': '无法使用本地备份',
|
||||
'cloudSync.localBackups.unavailableDesc': '当前平台未提供受支持的安全密钥库,Netcatty 无法安全地写入本地备份。请在支持系统钥匙串的环境中运行,或改用云同步保留恢复点。',
|
||||
'cloudSync.localBackups.lockedTitle': '需要主密钥',
|
||||
'cloudSync.localBackups.lockedDesc': '请先配置或解锁主密钥再恢复备份,以确保恢复后的凭据仍保持加密。',
|
||||
'cloudSync.revisionHistory.viewButton': '历史版本',
|
||||
'cloudSync.revisionHistory.title': '主机库版本历史',
|
||||
'cloudSync.revisionHistory.description': '浏览并恢复 Gist 修订历史中的旧版主机库数据。',
|
||||
'cloudSync.revisionHistory.empty': '未找到修订记录。',
|
||||
'cloudSync.revisionHistory.current': '当前版本',
|
||||
'cloudSync.revisionHistory.revision': '修订',
|
||||
'cloudSync.revisionHistory.revisionPreview': '修订内容',
|
||||
'cloudSync.revisionHistory.device': '设备',
|
||||
'cloudSync.revisionHistory.hosts': '主机',
|
||||
'cloudSync.revisionHistory.keys': '密钥',
|
||||
'cloudSync.revisionHistory.snippets': '代码片段',
|
||||
'cloudSync.revisionHistory.notes': '笔记',
|
||||
'cloudSync.revisionHistory.identities': '身份',
|
||||
'cloudSync.revisionHistory.restoreButton': '恢复此版本',
|
||||
'cloudSync.revisionHistory.restored': '已从选中的修订恢复主机库数据。',
|
||||
'cloudSync.revisionHistory.revisionNotFound': '修订未找到或不包含主机库数据。',
|
||||
'cloudSync.revisionHistory.decryptFailed': '无法解密此修订。可能是使用了不同的主密钥加密的。',
|
||||
'cloudSync.changeKey.title': '更改主密钥',
|
||||
'cloudSync.changeKey.current': '当前主密钥',
|
||||
'cloudSync.changeKey.new': '新的主密钥',
|
||||
'cloudSync.changeKey.confirmNew': '确认新的主密钥',
|
||||
'cloudSync.changeKey.currentPlaceholder': '输入当前主密钥',
|
||||
'cloudSync.changeKey.newPlaceholder': '输入新的主密钥',
|
||||
'cloudSync.changeKey.confirmPlaceholder': '再次输入新的主密钥',
|
||||
'cloudSync.changeKey.fillAll': '请填写所有字段',
|
||||
'cloudSync.changeKey.minLength': '新的主密钥至少 8 个字符',
|
||||
'cloudSync.changeKey.notMatch': '两次输入的主密钥不一致',
|
||||
'cloudSync.changeKey.incorrectCurrent': '当前主密钥不正确',
|
||||
'cloudSync.changeKey.failed': '更改主密钥失败',
|
||||
'cloudSync.changeKey.desc': '这将重新加密 Vault,请务必记住新的主密钥。',
|
||||
'cloudSync.changeKey.showKeys': '显示主密钥',
|
||||
'cloudSync.changeKey.updatedToast': '主密钥已更新',
|
||||
'cloudSync.changeKey.updateButton': '更新主密钥',
|
||||
'cloudSync.unlock.title': '输入主密钥',
|
||||
'cloudSync.unlock.masterKey': '主密钥',
|
||||
'cloudSync.unlock.desc': '仅需输入一次主密钥以启用加密同步,之后会通过系统 Keychain 安全存储。',
|
||||
'cloudSync.unlock.placeholder': '输入你的主密钥',
|
||||
'cloudSync.unlock.empty': '请输入主密钥',
|
||||
'cloudSync.unlock.incorrect': '主密钥不正确',
|
||||
'cloudSync.unlock.failed': '解锁 Vault 失败',
|
||||
'cloudSync.unlock.showKey': '显示主密钥',
|
||||
'cloudSync.unlock.notNow': '暂不',
|
||||
'cloudSync.unlock.readyToast': 'Vault 已就绪',
|
||||
'cloudSync.unlock.unlockButton': '解锁',
|
||||
'cloudSync.header.vaultReady': 'Vault 已就绪',
|
||||
'cloudSync.header.preparingVault': '正在准备 Vault...',
|
||||
'cloudSync.header.providersConnected': '已连接 {count} 个 provider',
|
||||
'cloudSync.githubFlow.title': '连接到 GitHub',
|
||||
'cloudSync.githubFlow.desc': '复制下面的 code,并在 GitHub 页面输入以授权 Netcatty。',
|
||||
'cloudSync.githubFlow.copyCode': '复制 code',
|
||||
'cloudSync.githubFlow.copied': '已复制',
|
||||
'cloudSync.githubFlow.openGitHub': '打开 GitHub',
|
||||
'cloudSync.githubFlow.waiting': '等待授权...',
|
||||
'cloudSync.conflict.title': '检测到版本冲突',
|
||||
'cloudSync.conflict.desc': '选择保留哪个版本',
|
||||
'cloudSync.conflict.local': '本地',
|
||||
'cloudSync.conflict.cloud': '云端',
|
||||
'cloudSync.conflict.detailsTitle': '发生变化的数据',
|
||||
'cloudSync.conflict.detailsCounts': '本地 {local} · 云端 {cloud} · 冲突 {conflicts}',
|
||||
'cloudSync.conflict.entity.hosts': '主机',
|
||||
'cloudSync.conflict.entity.keys': '密钥',
|
||||
'cloudSync.conflict.entity.identities': '身份',
|
||||
'cloudSync.conflict.entity.proxyProfiles': '代理配置',
|
||||
'cloudSync.conflict.entity.snippets': '片段',
|
||||
'cloudSync.conflict.entity.notes': '笔记',
|
||||
'cloudSync.conflict.entity.noteGroups': '笔记分组',
|
||||
'cloudSync.conflict.entity.customGroups': '分组',
|
||||
'cloudSync.conflict.entity.snippetPackages': '片段包',
|
||||
'cloudSync.conflict.entity.portForwardingRules': '端口转发',
|
||||
'cloudSync.conflict.entity.groupConfigs': '分组设置',
|
||||
'cloudSync.conflict.entity.settings': '设置',
|
||||
'cloudSync.conflict.keepLocal': '覆盖云端(保留本地)',
|
||||
'cloudSync.conflict.useCloud': '下载云端(覆盖本地)',
|
||||
'cloudSync.connect.browserContinue': '请在浏览器中完成授权',
|
||||
'cloudSync.connect.browserCancelled': '已取消上一个浏览器授权流程',
|
||||
'cloudSync.connect.github.success': 'GitHub 已连接',
|
||||
'cloudSync.connect.github.failedTitle': 'GitHub 连接失败',
|
||||
'cloudSync.connect.github.timeout': '连接 GitHub 超时,请检查网络或代理设置。',
|
||||
'cloudSync.connect.github.networkError': '无法访问 GitHub,请检查网络或代理设置。',
|
||||
'cloudSync.connect.google.failedTitle': 'Google 连接失败',
|
||||
'cloudSync.connect.onedrive.failedTitle': 'OneDrive 连接失败',
|
||||
'cloudSync.sync.success': '已同步到 {provider}',
|
||||
'cloudSync.sync.failed': '同步失败',
|
||||
'cloudSync.sync.failedTitle': '同步失败',
|
||||
'cloudSync.sync.errorTitle': '同步错误',
|
||||
'cloudSync.resolve.downloaded': '已下载云端数据',
|
||||
'cloudSync.resolve.uploaded': '已上传本地数据',
|
||||
'cloudSync.resolve.failedTitle': '冲突处理失败',
|
||||
'cloudSync.clearLocal.title': '清空本地数据',
|
||||
'cloudSync.clearLocal.desc': '重置本地版本和同步历史。下次同步将从云端下载。',
|
||||
'cloudSync.clearLocal.button': '清空',
|
||||
'cloudSync.clearLocal.dialog.title': '清空本地 Vault 数据?',
|
||||
'cloudSync.clearLocal.dialog.desc': '这将重置本地版本为 0 并清除同步历史。下次同步时会从云端下载数据,替换本地数据。',
|
||||
'cloudSync.clearLocal.dialog.cancel': '取消',
|
||||
'cloudSync.clearLocal.dialog.confirm': '确认清空',
|
||||
'cloudSync.clearLocal.toast.title': '本地数据已清空',
|
||||
'cloudSync.clearLocal.toast.desc': '本地版本已重置为 0。同步以从云端下载数据。',
|
||||
|
||||
// Common (additional)
|
||||
'common.searchPlaceholder': '搜索...',
|
||||
'common.import': '导入',
|
||||
'common.generate': '生成',
|
||||
'common.delete': '删除',
|
||||
'common.edit': '编辑',
|
||||
'sftp.context.openWithDefault': '系统默认程序打开',
|
||||
'common.clear': '清除',
|
||||
'common.optional': '可选',
|
||||
'common.selectPlaceholder': '请选择...',
|
||||
'common.error': '错误',
|
||||
'common.validation': '验证',
|
||||
'common.saveChanges': '保存修改',
|
||||
'common.advanced': '高级',
|
||||
'common.selectAHostPlaceholder': '选择主机...',
|
||||
|
||||
// Actions
|
||||
'action.duplicate': '复制',
|
||||
'action.open': '打开',
|
||||
'action.copy': '复制',
|
||||
'action.run': '运行',
|
||||
'action.start': '启动',
|
||||
'action.stop': '停止',
|
||||
|
||||
// Port Forwarding (form)
|
||||
'pf.form.labelPlaceholder': '规则标签',
|
||||
'pf.form.intermediateHost': '中转主机 *',
|
||||
'pf.form.createRule': '创建规则',
|
||||
'pf.form.openWizard': '打开向导',
|
||||
'pf.form.openWizardTitle': '打开端口转发向导',
|
||||
'pf.action.newForwarding': '新建转发',
|
||||
'pf.view.grid': '网格',
|
||||
'pf.view.list': '列表',
|
||||
'pf.rule.summary.local': '本机 {bindAddress}:{localPort} -> SSH 隧道 -> 远端 {remoteHost}:{remotePort}',
|
||||
'pf.rule.summary.remote': '远端 {bindAddress}:{localPort} -> SSH 隧道 -> 本机 {remoteHost}:{remotePort}',
|
||||
'pf.rule.summary.dynamic': 'SOCKS 监听于 {bindAddress}:{localPort}',
|
||||
'pf.tooltip.relayHost': '中转主机',
|
||||
'pf.tooltip.hostLabel': '主机',
|
||||
'pf.tooltip.hostAddress': '地址',
|
||||
'pf.tooltip.noHost': '未配置中转主机',
|
||||
'pf.tooltip.localDesc': '本地端口转发:通过 SSH 隧道访问远程服务',
|
||||
'pf.tooltip.remoteDesc': '远程端口转发:将本地服务暴露给远程主机',
|
||||
'pf.tooltip.dynamicDesc': '动态 SOCKS 代理:通过 SSH 隧道转发流量',
|
||||
'pf.deleteActive.title': '删除正在运行的端口转发?',
|
||||
'pf.deleteActive.desc': '端口转发规则 "{label}" 当前正在运行。删除前将先关闭转发连接。',
|
||||
'pf.deleteActive.confirm': '关闭并删除',
|
||||
'pf.form.autoStart': '自动启动',
|
||||
'pf.form.autoStartDesc': '应用启动时自动开启此规则',
|
||||
|
||||
// SFTP (pane + conflict)
|
||||
'sftp.pane.local': '本地',
|
||||
'sftp.pane.remote': '远端',
|
||||
'sftp.pane.selectHost': '选择主机',
|
||||
'sftp.pane.selectHostToStart': '先选择一个主机',
|
||||
'sftp.pane.chooseFilesystem': '选择要浏览的本地或远端文件系统',
|
||||
'sftp.tabs.addTab': '新建标签页',
|
||||
'sftp.tabs.closeTab': '关闭标签页',
|
||||
'sftp.tabs.newTab': '新标签页',
|
||||
'sftp.tabs.copyDefaultPath': '复制标签页(默认路径)',
|
||||
'sftp.tabs.copyCurrentPath': '复制并跳转到当前路径',
|
||||
'sftp.conflict.title': '文件冲突',
|
||||
'sftp.conflict.desc': '目标位置已存在同名文件',
|
||||
'sftp.conflict.alreadyExistsSuffix': '已存在',
|
||||
'sftp.conflict.existingFile': '已有文件',
|
||||
'sftp.conflict.newFile': '新文件',
|
||||
'sftp.conflict.size': '大小:',
|
||||
'sftp.conflict.modified': '修改时间:',
|
||||
'sftp.conflict.applyToAll': '将此操作应用到剩余的 {count} 个冲突',
|
||||
'sftp.conflict.action.stop': '停止',
|
||||
'sftp.conflict.action.skip': '跳过',
|
||||
'sftp.conflict.action.keepBoth': '保留两者',
|
||||
'sftp.conflict.action.duplicate': '创建副本',
|
||||
'sftp.conflict.action.merge': '合并',
|
||||
'sftp.conflict.action.replace': '替换',
|
||||
|
||||
// SFTP Upload Phases
|
||||
'sftp.upload.phase.compressing': '正在压缩',
|
||||
'sftp.upload.phase.uploading': '正在上传',
|
||||
'sftp.upload.phase.extracting': '正在解压',
|
||||
'sftp.upload.phase.compressed': '压缩传输',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Messages } from './types';
|
||||
import { zhTWCoreMessages } from './zh-TW/core';
|
||||
import { zhTWVaultMessages } from './zh-TW/vault';
|
||||
import { zhTWTerminalMessages } from './zh-TW/terminal';
|
||||
import { zhTWAiMessages } from './zh-TW/ai';
|
||||
import { zhTwSystemManagerMessages } from './zh-TW/systemManager';
|
||||
import { zhTWScriptsMessages } from './zh-TW/scripts';
|
||||
|
||||
export type { Messages } from './types';
|
||||
|
||||
const zhTW: Messages = {
|
||||
...zhTWCoreMessages,
|
||||
...zhTWVaultMessages,
|
||||
...zhTWTerminalMessages,
|
||||
...zhTWAiMessages,
|
||||
...zhTwSystemManagerMessages,
|
||||
...zhTWScriptsMessages,
|
||||
};
|
||||
|
||||
export default zhTW;
|
||||
@@ -0,0 +1,491 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhTWAiMessages: Messages = {
|
||||
// AI Settings
|
||||
'ai.agentSettings': 'Agent 設定',
|
||||
'ai.chat.preparing': '準備中…',
|
||||
'ai.chat.compactingContext': '正在壓縮較早的上下文…',
|
||||
'ai.chat.compactingStep': '正在為下一步整理上下文…',
|
||||
'ai.chat.compactionRetry': '請求過大,正在壓縮上下文並重試…',
|
||||
'ai.chat.compactionBanner': '上下文已壓縮:{before}K → {after}K tokens',
|
||||
'ai.title': 'AI',
|
||||
'ai.description': '設定 AI 供應商、Agent 和安全設定',
|
||||
'ai.providers': '供應商',
|
||||
'ai.agents': 'Agent',
|
||||
'ai.providers.empty': '尚未設定供應商。新增一個供應商以開始使用。',
|
||||
'ai.providers.add': '新增供應商',
|
||||
'ai.providers.active': '使用中',
|
||||
'ai.providers.apiKeyConfigured': 'API Key 已設定',
|
||||
'ai.providers.noApiKey': '未設定 API Key',
|
||||
'ai.providers.configure': '設定',
|
||||
'ai.providers.remove': '移除',
|
||||
'ai.providers.name': '顯示名稱',
|
||||
'ai.providers.name.placeholder': '例如 我的供應商',
|
||||
'ai.providers.style': '協定風格',
|
||||
'ai.providers.style.anthropic': 'Anthropic 相容',
|
||||
'ai.providers.style.openai': 'OpenAI 相容',
|
||||
'ai.providers.style.google': 'Google 相容',
|
||||
'ai.providers.style.inherited': '預設',
|
||||
'ai.providers.style.help': '決定請求使用哪種 API 格式。當第三方端點的協定與其供應商型別不一致時,可手動覆蓋。',
|
||||
'ai.providers.icon.change': '修改圖示',
|
||||
'ai.providers.icon.upload': '上傳圖片',
|
||||
'ai.providers.icon.reset': '恢復預設',
|
||||
'ai.providers.icon.close': '收起',
|
||||
'ai.providers.icon.uploadedNote': '自訂圖示(64×64 WebP)',
|
||||
'ai.providers.icon.errorType': '請選擇圖片檔案。',
|
||||
'ai.providers.apiKey': 'API Key',
|
||||
'ai.providers.apiKey.placeholder': '輸入 API Key',
|
||||
'ai.providers.apiKey.decrypting': '解密中...',
|
||||
'ai.providers.baseUrl': 'Base URL',
|
||||
'ai.providers.skipTLSVerify': '跳過 TLS 憑證驗證(用於自我簽署憑證)',
|
||||
'ai.providers.defaultModel': '預設模型',
|
||||
'ai.providers.defaultModel.placeholder': '例如 gpt-4o, claude-sonnet-4-20250514',
|
||||
'ai.providers.contextWindow': '上下文視窗',
|
||||
'ai.providers.contextWindow.placeholder': '例如 128000',
|
||||
'ai.providers.contextWindow.help': '留空時優先使用模型列表傳回的值;如果沒有,Netcatty 會使用安全預設值。',
|
||||
'ai.providers.contextWindow.error': '請輸入正整數,或留空。',
|
||||
'ai.providers.refreshModels': '重新整理模型列表',
|
||||
'ai.providers.searchModel': '搜尋或輸入模型 ID...',
|
||||
'ai.providers.filterModels': '篩選模型...',
|
||||
'ai.providers.loadingModels': '載入模型中...',
|
||||
'ai.providers.noMatchingModels': '沒有相符的模型',
|
||||
'ai.providers.clickToLoadModels': '點選載入模型',
|
||||
'ai.providers.showingModels': '顯示前 100 個,共 {count} 個模型。輸入以篩選。',
|
||||
'ai.providers.advancedParams': '高階引數',
|
||||
'ai.providers.advancedParams.hint': '留空則使用供應商預設值。',
|
||||
'ai.providers.advancedParams.maxTokens.placeholder': '例如 4096',
|
||||
'ai.providers.advancedParams.default': '供應商預設',
|
||||
|
||||
// AI Codex
|
||||
'ai.codex': 'Codex',
|
||||
'ai.codex.title': 'Codex CLI',
|
||||
'ai.codex.description': '介接 OpenAI Codex。可以在這裡登入 ChatGPT,也可以在設定裡啟用相容 OpenAI 的 API Key 和自訂介面地址。',
|
||||
'ai.codex.detecting': '偵測中...',
|
||||
'ai.codex.notFound': '未找到',
|
||||
'ai.codex.awaitingLogin': '等待登入',
|
||||
'ai.codex.connectedChatGPT': '已透過 ChatGPT 連線',
|
||||
'ai.codex.connectedApiKey': '已透過 API Key 連線',
|
||||
'ai.codex.connectedCustomConfig': '使用 ~/.codex/config.toml 自訂 provider',
|
||||
'ai.codex.customConfigIncomplete': '偵測到自訂設定(缺少環境變數)',
|
||||
'ai.codex.customConfigHint': '使用 ~/.codex/config.toml 中設定的自訂 provider "{provider}",無需 ChatGPT 登入。',
|
||||
'ai.codex.customConfigMissingEnvKey': '警告:環境變數 {envKey} 未在目前 shell 中設定。請 export 它(或從包含該變數的 shell 啟動 netcatty),否則 Codex 無法完成認證。',
|
||||
'ai.codex.notConnected': '未連線',
|
||||
'ai.codex.statusUnknown': '狀態未知',
|
||||
'ai.codex.path': '路徑:',
|
||||
'ai.codex.notFoundHint': '在 PATH 中未找到 codex。請安裝或在下方指定執行檔路徑。',
|
||||
'ai.codex.customPathPlaceholder': '例如 /usr/local/bin/codex',
|
||||
'ai.codex.check': '檢查',
|
||||
'ai.codex.resetPath': '重置',
|
||||
'ai.codex.openLogin': '開啟登入',
|
||||
'ai.codex.logout': '登出',
|
||||
'ai.codex.connectChatGPT': '連線 ChatGPT',
|
||||
'ai.codex.refreshStatus': '重新整理狀態',
|
||||
|
||||
// AI Claude Code
|
||||
'ai.claude.title': 'Claude Code',
|
||||
'ai.claude.description': 'Anthropic 的智慧程式設計助手。需要系統中已安裝 Claude Code CLI。',
|
||||
'ai.claude.detecting': '偵測中...',
|
||||
'ai.claude.detected': '已偵測到',
|
||||
'ai.claude.notFound': '未找到',
|
||||
'ai.claude.path': '路徑:',
|
||||
'ai.claude.notFoundHint': '在 PATH 中未找到 claude。請安裝或在下方指定執行檔路徑。',
|
||||
'ai.claude.customPathPlaceholder': '例如 /usr/local/bin/claude',
|
||||
'ai.claude.configSection': '認證與設定(可選)',
|
||||
'ai.claude.configDir': '設定目錄',
|
||||
'ai.claude.configDir.placeholder': '~/.claude(留空用預設)',
|
||||
'ai.claude.configDir.hint': '設定 CLAUDE_CONFIG_DIR —— 指向你已執行 `claude` 登入的目錄(含 settings.json 和憑證)。',
|
||||
'ai.claude.settings': 'Settings 檔案',
|
||||
'ai.claude.settings.placeholder': '~/team-settings.json(路徑,或內聯 {"model":"..."})',
|
||||
'ai.claude.settings.hint': '可選。settings.json 路徑或內聯 JSON,作為 SDK 的 `settings` 傳入。與上面的「設定目錄」互補且獨立(疊加合併,不是替換)。',
|
||||
'ai.claude.envVars': '環境變數',
|
||||
'ai.claude.envVars.placeholder': 'ANTHROPIC_BASE_URL=https://...\nANTHROPIC_MODEL=...',
|
||||
'ai.claude.envVars.hint': '每行一個 KEY=VALUE,傳給 Claude agent。明文存在本機——API key/憑證建議用上面的「設定目錄」(claude 登入),不要放這裡。',
|
||||
'ai.claude.check': '檢查',
|
||||
'ai.claude.resetPath': '重置',
|
||||
|
||||
// AI GitHub Copilot CLI
|
||||
'ai.copilot.title': 'GitHub Copilot CLI',
|
||||
'ai.copilot.description': '介接 GitHub Copilot CLI。偵測到後即可作為外部程式設計 Agent 使用。',
|
||||
'ai.copilot.detecting': '偵測中...',
|
||||
'ai.copilot.detected': '已偵測到',
|
||||
'ai.copilot.notFound': '未找到',
|
||||
'ai.copilot.path': '路徑:',
|
||||
'ai.copilot.notFoundHint': '在 PATH 中未找到 copilot。請安裝或在下方指定執行檔路徑。',
|
||||
'ai.copilot.customPathPlaceholder': '例如 /usr/local/bin/copilot',
|
||||
'ai.copilot.check': '檢查',
|
||||
'ai.copilot.resetPath': '重置',
|
||||
|
||||
// AI Cursor SDK
|
||||
'ai.cursor.title': 'Cursor',
|
||||
'ai.cursor.description': '使用 Cursor SDK。',
|
||||
'ai.cursor.detecting': '偵測中...',
|
||||
'ai.cursor.detected': '可用',
|
||||
'ai.cursor.notFound': '不可用',
|
||||
'ai.cursor.path': '執行環境:',
|
||||
'ai.cursor.notFoundHint': '填寫 API Key 後即可使用。',
|
||||
'ai.cursor.notInstalledHint': '未偵測到 Cursor SDK。',
|
||||
'ai.cursor.installStatus': 'Cursor SDK',
|
||||
'ai.cursor.installed': '已偵測到',
|
||||
'ai.cursor.notInstalled': '未偵測到',
|
||||
'ai.cursor.apiKeyStatus': 'API Key',
|
||||
'ai.cursor.apiKeyConfigured': '已填寫',
|
||||
'ai.cursor.apiKeyMissing': '未填寫',
|
||||
'ai.cursor.apiKeyFromEnv': '來自環境變數',
|
||||
'ai.cursor.apiKey': 'API Key',
|
||||
'ai.cursor.apiKeyPlaceholder': '輸入 Cursor API Key',
|
||||
'ai.cursor.apiKeyPlaceholder.env': '已使用 CURSOR_API_KEY;填寫後會覆蓋',
|
||||
'ai.cursor.apiKeyEnvHint': '已偵測到本機 CURSOR_API_KEY。留空即可繼續使用,填寫儲存後會覆蓋它。',
|
||||
'ai.cursor.apiKeyOverrideHint': '目前優先使用這裡儲存的 Key;清空儲存後會回到 CURSOR_API_KEY。',
|
||||
'ai.cursor.saveApiKey': '儲存',
|
||||
'ai.cursor.saved': '已儲存',
|
||||
'ai.cursor.showApiKey': '顯示 API Key',
|
||||
'ai.cursor.hideApiKey': '隱藏 API Key',
|
||||
'ai.cursor.customPathPlaceholder': '例如 /usr/local/bin/cursor',
|
||||
'ai.cursor.check': '檢查',
|
||||
|
||||
// AI CodeBuddy Code
|
||||
'ai.codebuddy.title': 'CodeBuddy Code',
|
||||
'ai.codebuddy.description': '透過官方 Agent SDK(`@tencent-ai/agent-sdk`)介接 CodeBuddy Code。偵測到後即可作為外部程式設計 Agent 使用。',
|
||||
'ai.codebuddy.detecting': '偵測中...',
|
||||
'ai.codebuddy.detected': '已偵測到',
|
||||
'ai.codebuddy.notFound': '未找到',
|
||||
'ai.codebuddy.path': '路徑:',
|
||||
'ai.codebuddy.notFoundHint': '在 PATH 中未找到 codebuddy。請安裝或在下方指定執行檔路徑。',
|
||||
'ai.codebuddy.customPathPlaceholder': '例如 /usr/local/bin/codebuddy',
|
||||
'ai.codebuddy.check': '檢查',
|
||||
'ai.codebuddy.resetPath': '重置',
|
||||
'ai.codebuddy.configSection': '認證與設定(可選)',
|
||||
'ai.codebuddy.internetEnv': '網路環境',
|
||||
'ai.codebuddy.internetEnv.default': '預設(海外)',
|
||||
'ai.codebuddy.internetEnv.internal': 'Internal',
|
||||
'ai.codebuddy.internetEnv.ioa': 'IOA',
|
||||
'ai.codebuddy.internetEnv.hint': '設定 CODEBUDDY_INTERNET_ENVIRONMENT —— 受限網路環境請選擇 Internal 或 IOA。',
|
||||
'ai.codebuddy.envVars': '環境變數',
|
||||
'ai.codebuddy.envVars.placeholder': 'CODEBUDDY_API_KEY=...\nCODEBUDDY_AUTH_TOKEN=...\nOTHER_VAR=...',
|
||||
'ai.codebuddy.envVars.hint': '每行一個 KEY=VALUE,傳給 CodeBuddy agent。可在此設定 CODEBUDDY_API_KEY 或 CODEBUDDY_AUTH_TOKEN 完成認證。明文存在本機。',
|
||||
|
||||
// AI OpenCode
|
||||
'ai.opencode.title': 'OpenCode',
|
||||
'ai.opencode.description': '透過官方 SDK 介接 OpenCode。先在 OpenCode 裡設定 provider 和金鑰,偵測到後即可作為外部程式設計 Agent 使用。',
|
||||
'ai.opencode.detecting': '偵測中...',
|
||||
'ai.opencode.detected': '已偵測到',
|
||||
'ai.opencode.notFound': '未找到',
|
||||
'ai.opencode.path': '路徑:',
|
||||
'ai.opencode.notFoundHint': '在 PATH 中未找到 opencode。請安裝或在下方指定執行檔路徑。',
|
||||
'ai.opencode.customPathPlaceholder': '例如 /usr/local/bin/opencode',
|
||||
'ai.opencode.check': '檢查',
|
||||
'ai.opencode.resetPath': '重置',
|
||||
|
||||
// AI Default Agent
|
||||
'ai.defaultAgent': '預設 Agent',
|
||||
'ai.defaultAgent.description': '建立新 AI 工作階段時使用的 Agent',
|
||||
'ai.defaultAgent.catty': 'Catty(內建)',
|
||||
'ai.toolAccess.title': '工具介接',
|
||||
'ai.toolAccess.mode': 'Netcatty 介接模式',
|
||||
'ai.toolAccess.description': '選擇外部 Agent 存取 Netcatty 工作階段的方式。MCP 會公開內建伺服器,Skills + CLI 會引導 Agent 讀取本機 Skill 並呼叫 Netcatty CLI。',
|
||||
'ai.toolAccess.mode.mcp': 'MCP',
|
||||
'ai.toolAccess.mode.skills': 'Skills + CLI',
|
||||
|
||||
'ai.externalMcp.title': '對外 MCP',
|
||||
'ai.externalMcp.description': '將 Netcatty 作為 MCP 伺服器暴露給 Codex、Claude Code、Cursor、Grok 等外部用戶端。工具面與應用內 Agent 相同。用戶端連線期間請保持 Netcatty 執行。',
|
||||
'ai.externalMcp.sessionsExposed': '作用域內工作階段:{count}',
|
||||
'ai.externalMcp.mode': '可用模式',
|
||||
'ai.externalMcp.mode.temporary': '暫時',
|
||||
'ai.externalMcp.mode.persistent': '常開',
|
||||
'ai.externalMcp.mode.description': '暫時模式會在閒置逾時後自動關閉;常開模式會在 Netcatty 啟動時還原對外 MCP。',
|
||||
'ai.externalMcp.idleTimeout': '閒置逾時',
|
||||
'ai.externalMcp.idleTimeout.description': '暫時模式下,若超過該分鐘數沒有 MCP 操作,將自動關閉對外 MCP。',
|
||||
'ai.externalMcp.idleTimeout.minutes': '分鐘',
|
||||
'ai.externalMcp.usage.title': '使用說明',
|
||||
'ai.externalMcp.usage.keepRunning': '1. 開啟對外 MCP 開關,並保持 Netcatty 執行。',
|
||||
'ai.externalMcp.usage.localhost': '2. 用戶端透過本機 launcher 連線(僅 127.0.0.1)。關閉開關後會刪除 discovery。',
|
||||
'ai.externalMcp.usage.permissions': '3. 寫入操作遵循「設定 → AI → 安全」以及命令黑名單。',
|
||||
'ai.externalMcp.usage.capabilities': '4. 完整 catalog 工具可用:終端機、SFTP、Vault、連接埠轉發。密碼 / 私鑰不會回傳。',
|
||||
'ai.externalMcp.help.ariaLabel': '對外 MCP 使用說明',
|
||||
'ai.externalMcp.security': '安全',
|
||||
'ai.externalMcp.security.description': '僅監聽 127.0.0.1,使用輪替 token,寫入操作重用 AI 權限模式,關閉時刪除 discovery。',
|
||||
'ai.externalMcp.permissionMode': '目前權限模式:{mode}',
|
||||
'ai.externalMcp.permissionMode.label': '寫入權限模式',
|
||||
'ai.externalMcp.permissionMode.hint': '與「設定 → AI → 安全」為同一項。自動:Netcatty 寫入操作不再彈本應用審批;確認:每次詢問。外部用戶端(Codex / Claude / Grok)仍可能有自己的工具審批介面。',
|
||||
'ai.externalMcp.permissionMode.unknown': '未知',
|
||||
'ai.externalMcp.discovery': 'Discovery',
|
||||
'ai.externalMcp.launcher': 'Launcher',
|
||||
'ai.externalMcp.unavailable': '無法使用',
|
||||
'ai.externalMcp.bridgeUnavailable': '對外 MCP 橋接無法使用',
|
||||
'ai.externalMcp.copy': '複製',
|
||||
'ai.externalMcp.copied': '已複製',
|
||||
'ai.externalMcp.copyFailed': '複製失敗,請手動複製。',
|
||||
'ai.externalMcp.refresh': '重新整理',
|
||||
'ai.externalMcp.clientConfiguration': '用戶端設定',
|
||||
'ai.externalMcp.clientConfiguration.description': '選擇用戶端一鍵安裝,或複製 CLI / 設定片段。',
|
||||
'ai.externalMcp.client.codex': 'Codex',
|
||||
'ai.externalMcp.client.claude': 'Claude Code',
|
||||
'ai.externalMcp.client.grok': 'Grok',
|
||||
'ai.externalMcp.client.cursor': 'Cursor',
|
||||
'ai.externalMcp.cliCommand': 'CLI 命令',
|
||||
'ai.externalMcp.configSnippet': '設定片段',
|
||||
'ai.externalMcp.addToCodex': '新增到 Codex',
|
||||
'ai.externalMcp.addToClaude': '新增到 Claude Code',
|
||||
'ai.externalMcp.addToGrok': '新增到 Grok',
|
||||
'ai.externalMcp.codexAdded': '已新增 Codex MCP 項目。請重新啟動 Codex 或開啟新工作階段。',
|
||||
'ai.externalMcp.claudeAdded': '已新增 Claude Code MCP 項目。請重新啟動 Claude Code 或開啟新工作階段。',
|
||||
'ai.externalMcp.grokAdded': '已新增 Grok MCP 項目。請重新啟動 Grok 或開啟新工作階段。',
|
||||
'ai.externalMcp.installCodex': '請先另外安裝 Codex,然後按重新整理。',
|
||||
'ai.externalMcp.installClaude': '請先另外安裝 Claude Code,然後按重新整理。',
|
||||
'ai.externalMcp.installGrok': '請先另外安裝 Grok CLI,然後按重新整理。',
|
||||
'ai.externalMcp.conflict.description': '已存在指向其他位置的 netcatty-external 項目,請手動刪除或修改。',
|
||||
'ai.externalMcp.enableForLauncher': '請先啟用對外 MCP,以取得可用的 launcher 路徑。',
|
||||
'ai.externalMcp.cursor.title': 'Cursor / 其他用戶端',
|
||||
'ai.externalMcp.cursor.description': '合併到 MCP 設定檔(例如 ~/.cursor/mcp.json)。若已有其他伺服器,請勿整檔覆蓋。',
|
||||
'ai.externalMcp.status.unavailable': '無法使用',
|
||||
'ai.externalMcp.status.disabled': '已關閉',
|
||||
'ai.externalMcp.status.running': '執行中',
|
||||
'ai.externalMcp.status.starting': '啟動中',
|
||||
'ai.externalMcp.status.error': '錯誤',
|
||||
'ai.externalMcp.status.configured': '已設定',
|
||||
'ai.externalMcp.status.notConfigured': '未設定',
|
||||
'ai.externalMcp.status.checking': '檢查中',
|
||||
'ai.externalMcp.status.codexNotFound': '找不到 Codex',
|
||||
'ai.externalMcp.status.claudeNotFound': '找不到 Claude Code',
|
||||
'ai.externalMcp.status.grokNotFound': '找不到 Grok',
|
||||
'ai.externalMcp.status.conflict': '衝突',
|
||||
'ai.userSkills.title': '使用者 Skills',
|
||||
'ai.userSkills.description': '開啟 Netcatty 的 Skills 資料夾以新增你自己的技能目錄。Netcatty 會自動掃描這些 skills,預設只注入輕量索引,只有在請求明顯命中某個 skill 時才展開正文。',
|
||||
'ai.userSkills.openFolder': '開啟 Skills 資料夾',
|
||||
'ai.userSkills.reload': '重新載入 Skills',
|
||||
'ai.userSkills.location': '位置',
|
||||
'ai.userSkills.loading': '正在掃描使用者 skills...',
|
||||
'ai.userSkills.summary': '已就緒 {ready} 個,警告 {warnings} 個',
|
||||
'ai.userSkills.empty': '暫未發現使用者 skills。開啟資料夾後可新增包含 SKILL.md 的技能目錄。',
|
||||
'ai.userSkills.unavailable': '目前環境不支援使用者 skills。',
|
||||
'ai.userSkills.status.ready': '正常',
|
||||
'ai.userSkills.status.warning': '警告',
|
||||
|
||||
// AI Quick Messages
|
||||
'ai.quickMessages.title': '快捷訊息',
|
||||
'ai.quickMessages.description': '建立常用提示詞,在 AI 聊天框輸入 / 或點選快捷按鈕即可插入到輸入框。與使用者 Skills 不同,快捷訊息會直接填入訊息內容。',
|
||||
'ai.quickMessages.add': '新增快捷訊息',
|
||||
'ai.quickMessages.createTitle': '新建快捷訊息',
|
||||
'ai.quickMessages.editTitle': '編輯快捷訊息',
|
||||
'ai.quickMessages.name': '名稱',
|
||||
'ai.quickMessages.name.placeholder': '例如:檢查磁碟空間',
|
||||
'ai.quickMessages.slug': '指令',
|
||||
'ai.quickMessages.slug.placeholder': 'disk-check',
|
||||
'ai.quickMessages.descriptionField': '說明(可選)',
|
||||
'ai.quickMessages.descriptionField.placeholder': '簡短描述這條快捷訊息的用途',
|
||||
'ai.quickMessages.content': '訊息內容',
|
||||
'ai.quickMessages.content.placeholder': '輸入選擇後要插入的完整提示詞...',
|
||||
'ai.quickMessages.empty': '還沒有快捷訊息。新增幾條常用提示,聊天時就能一鍵插入。',
|
||||
'ai.quickMessages.confirmDelete': '確定刪除快捷訊息「{name}」嗎?',
|
||||
'ai.quickMessages.error.nameRequired': '請填寫名稱。',
|
||||
'ai.quickMessages.error.invalidSlug': '指令只能包含小寫字母、數字和連字元。',
|
||||
'ai.quickMessages.error.contentRequired': '請填寫訊息內容。',
|
||||
'ai.quickMessages.error.slugTaken': '該指令已被其他快捷訊息使用。',
|
||||
'ai.quickMessages.error.slugConflictsWithSkill': '該指令與使用者 Skill「/{slug}」衝突,請換一個指令。',
|
||||
'ai.quickMessages.error.maxItems': '最多只能儲存 {max} 條快捷訊息。',
|
||||
|
||||
// AI Chat
|
||||
'ai.chat.noProvider': '尚未設定 AI 供應商。請前往 **設定 → AI → 供應商** 新增並啟用一個供應商。',
|
||||
'ai.chat.toolDenied': '操作已被使用者拒絕。',
|
||||
'ai.chat.toolApproved': '已批准',
|
||||
'ai.chat.toolApprovalHint': 'Enter 允許 · Esc 拒絕',
|
||||
'ai.chat.approve': '批准',
|
||||
'ai.chat.approveOnce': '允許',
|
||||
'ai.chat.alwaysAllow': '始終',
|
||||
'ai.chat.slashStopDesc': '停止目前 AI 回合並取消進行中的工具',
|
||||
'ai.chat.reject': '拒絕',
|
||||
'ai.chat.toolLabel': '工具',
|
||||
'ai.chat.targetLabel': '目標',
|
||||
'ai.chat.permissionRequired': '需要權限',
|
||||
'ai.chat.permissionDescription': 'AI Agent 希望執行一個需要你批准的工具呼叫。',
|
||||
'ai.chat.commandBlocked': '此指令已被安全策略攔截,無法執行。',
|
||||
'ai.chat.recommendAllow': '允許',
|
||||
'ai.chat.recommendConfirm': '確認',
|
||||
'ai.chat.recommendDeny': '拒絕',
|
||||
'ai.chat.exportConversation': '匯出對話',
|
||||
'ai.chat.exportAs': '匯出為',
|
||||
'ai.chat.exportMarkdown': 'Markdown',
|
||||
'ai.chat.exportJSON': 'JSON',
|
||||
'ai.chat.exportPlainText': '純文字',
|
||||
'ai.chat.thinking': '思考中',
|
||||
'ai.chat.thoughtFor': '思考了 {duration}',
|
||||
'ai.chat.thought': '思考',
|
||||
'ai.chat.agents': 'Agents',
|
||||
'ai.chat.detectedOnMachine': '在本機偵測到',
|
||||
'ai.chat.rescan': '重新掃描',
|
||||
'ai.chat.permObserver': '觀察',
|
||||
'ai.chat.permConfirm': '確認',
|
||||
'ai.chat.permAuto': '自動',
|
||||
'ai.chat.permObserverDesc': '只讀模式',
|
||||
'ai.chat.permConfirmDesc': '操作前詢問',
|
||||
'ai.chat.permAutoDesc': '自由執行',
|
||||
'ai.chat.emptyHint': '詢問伺服器相關問題、執行指令或取得設定協助。',
|
||||
'ai.chat.placeholder': '向 {agent} 傳送訊息 — @ 引用上下文,/ 使用指令',
|
||||
'ai.chat.placeholderDefault': '向 Catty Agent 傳送訊息...',
|
||||
'ai.chat.noModel': '未選擇模型',
|
||||
'ai.chat.noProviderModel': '未設定預設模型——前往 設定 → AI → 供應商 設定。',
|
||||
'ai.chat.selectProvider': '選擇供應商',
|
||||
'ai.chat.recent': '最近',
|
||||
'ai.chat.viewAll': '檢視全部',
|
||||
'ai.chat.untitled': '無標題',
|
||||
'ai.chat.justNow': '剛剛',
|
||||
'ai.chat.minutesAgo': '{n}分鐘前',
|
||||
'ai.chat.hoursAgo': '{n}小時前',
|
||||
'ai.chat.daysAgo': '{n}天前',
|
||||
'ai.chat.newChat': '新對話',
|
||||
'ai.chat.allSessions': '所有工作階段',
|
||||
'ai.chat.loadEarlierMessages': '載入更早的訊息(還有 {n} 條)',
|
||||
'ai.chat.usedTools': '已使用 {n} 個工具',
|
||||
'ai.chat.loadMoreSessions': '載入更多工作階段(還有 {n} 條)',
|
||||
'ai.chat.noSessions': '沒有歷史工作階段',
|
||||
'ai.chat.retryHint': '你可以重新發送訊息來重試。',
|
||||
'ai.chat.approvalTimeout': '工具審批已超時(5 分鐘)。你可以重新發送訊息來重試。',
|
||||
'ai.chat.menuHosts': '主機',
|
||||
'ai.chat.menuContext': '上下文',
|
||||
'ai.chat.menuFiles': '檔案',
|
||||
'ai.chat.menuImage': '圖片',
|
||||
'ai.chat.menuMentionHost': '提及主機',
|
||||
'ai.chat.menuUserSkills': '使用者 Skills',
|
||||
'ai.chat.menuSlashCommands': '快捷指令',
|
||||
'ai.chat.slashCommands': '快捷指令',
|
||||
'ai.chat.slashQuickMessages': '快捷訊息',
|
||||
'ai.chat.slashUserSkills': '使用者 Skills',
|
||||
'ai.chat.quickMessages': '快捷指令',
|
||||
'ai.chat.slashNoResults': '沒有相符的指令',
|
||||
'ai.chat.slashEmptyHint': '可在 設定 → AI → 快捷訊息 中新增常用提示詞。',
|
||||
|
||||
// AI 聊天快捷入口
|
||||
'ai.chatShortcuts.title': '聊天快捷入口',
|
||||
'ai.chatShortcuts.selectionAction': '選取終端內容時顯示「新增到對話」',
|
||||
'ai.chatShortcuts.selectionAction.description': '在終端裡選取文字後顯示 AI 快捷按鈕。',
|
||||
|
||||
// AI Error
|
||||
'ai.codex.bridgeError': 'Codex 主程序處理器尚未載入。請完全重啟 Netcatty 或重啟 Electron 開發程序,然後重試。',
|
||||
|
||||
// AI Web Search
|
||||
'ai.webSearch.title': '網路搜尋',
|
||||
'ai.webSearch.enable': '啟用網路搜尋',
|
||||
'ai.webSearch.enable.description': '允許 AI 代理搜尋網際網路取得最新資訊。',
|
||||
'ai.webSearch.provider': '搜尋供應商',
|
||||
'ai.webSearch.provider.description': '選擇一個網路搜尋 API 供應商。',
|
||||
'ai.webSearch.apiKey': 'API 金鑰',
|
||||
'ai.webSearch.apiKey.description': '所選搜尋供應商的 API 金鑰。',
|
||||
'ai.webSearch.apiKey.placeholder': '輸入 API 金鑰...',
|
||||
'ai.webSearch.apiHost': 'API 地址',
|
||||
'ai.webSearch.apiHost.description': '自訂 API 端點。除非使用代理,否則保持預設值。',
|
||||
'ai.webSearch.apiHost.searxngDescription': 'SearXNG 例項的 URL(必填)。',
|
||||
'ai.webSearch.maxResults': '最大結果數',
|
||||
'ai.webSearch.maxResults.description': '搜尋傳回的最大結果數(1-20)。',
|
||||
|
||||
// AI Safety Settings
|
||||
'ai.safety.title': '安全',
|
||||
'ai.safety.permissionMode': '權限模式',
|
||||
'ai.safety.permissionMode.description': '控制 AI 透過 Netcatty 存取終端工作階段的方式。觀察者模式會阻止經由 Netcatty 的寫操作;外部 Agent CLI 可能仍有自己的本機工具和審批流程。',
|
||||
'ai.safety.permissionMode.observer': '觀察者 - 只讀,禁止操作',
|
||||
'ai.safety.permissionMode.confirm': '確認 - 操作前詢問',
|
||||
'ai.safety.permissionMode.auto': '自動 - 自由執行',
|
||||
'ai.safety.commandTimeout': '指令超時',
|
||||
'ai.safety.commandTimeout.description': '透過 Netcatty 執行指令時允許執行的最長秒數,超時將被終止。',
|
||||
'ai.safety.commandTimeout.unit': '秒',
|
||||
'ai.safety.maxIterations': '最大迭代次數',
|
||||
'ai.safety.maxIterations.description': '防止 AI 失控執行的最大工具呼叫迴圈次數。外部 Agent 可能有自己的內部迭代限制,以其為準。',
|
||||
'ai.safety.blocklist': '指令黑名單',
|
||||
'ai.safety.blocklist.description': '用於攔截透過 Netcatty 執行的危險指令的正規表示式。',
|
||||
'ai.safety.blocklist.placeholder': '正規表示式...',
|
||||
'ai.safety.blocklist.reset': '恢復預設',
|
||||
'ai.safety.blocklist.add': '新增規則',
|
||||
'ai.safety.grants.title': '許可記憶表',
|
||||
'ai.safety.grants.heading': 'Confirm 模式放行規則',
|
||||
'ai.safety.grants.description': 'Confirm 模式會先詢問再執行;儲存為規則後,相符的同類操作會自動放行。規則對所有終端節點/工作階段生效,也可手動編輯。',
|
||||
'ai.safety.grants.empty': '尚無規則。可在審批時選擇「一律許可」,或手動新增。',
|
||||
'ai.safety.grants.capability': '能力',
|
||||
'ai.safety.grants.sessionPattern': '節點/工作階段模式',
|
||||
'ai.safety.grants.commandPattern': '指令模式(可選)',
|
||||
'ai.safety.grants.note': '備註(可選)',
|
||||
'ai.safety.grants.add': '新增規則',
|
||||
'ai.safety.grants.remove': '刪除',
|
||||
'ai.safety.grants.export': '匯出 JSON',
|
||||
'ai.safety.grants.import': '匯入 JSON',
|
||||
'ai.safety.note': '這些安全設定會約束經由 Netcatty 執行的操作。外部 Agent CLI 也可能提供本機工具,那部分由 Agent 自己的控制規則約束。',
|
||||
|
||||
// 統一終端工作區和頂部標籤的 tooltip 文案 (issue #954)
|
||||
'terminal.layer.addTerminal': '新增終端',
|
||||
'terminal.layer.switchToSplitView': '切換到分割檢視',
|
||||
'terminal.layer.sftp': '檔案傳輸',
|
||||
'terminal.layer.scripts': '指令碼',
|
||||
'terminal.layer.history': '指令歷史',
|
||||
'terminal.layer.theme': '主題',
|
||||
'terminal.layer.notes': '筆記',
|
||||
'terminal.layer.aiChat': 'AI 助手',
|
||||
'terminal.layer.movePanelLeft': '面板移至左側',
|
||||
'terminal.layer.movePanelRight': '面板移至右側',
|
||||
'terminal.layer.closePanel': '關閉面板',
|
||||
'terminal.layer.hostTree.search': '搜尋主機...',
|
||||
'terminal.layer.hostTree.searchButton': '搜尋',
|
||||
'terminal.layer.hostTree.tagsButton': '按標籤篩選',
|
||||
'terminal.layer.hostTree.newGroup': '新建分組',
|
||||
'terminal.layer.hostTree.localShell': '本機 Shell',
|
||||
'terminal.layer.hostTree.tagsEmpty': '暫無標籤',
|
||||
'terminal.layer.hostTree.clearTags': '清除篩選',
|
||||
'terminal.layer.hostTree.collapse': '收起主機列表',
|
||||
'terminal.layer.hostTree.expand': '展開主機列表',
|
||||
'terminal.layer.hostTree.empty': '沒有相符的主機',
|
||||
'terminal.layer.hostTree.details.host': '主機',
|
||||
'terminal.layer.hostTree.details.user': '使用者',
|
||||
'terminal.layer.hostTree.details.port': '埠',
|
||||
'terminal.layer.hostTree.details.protocol': '協定',
|
||||
'terminal.layer.hostTree.details.group': '分組',
|
||||
'terminal.layer.hostTree.details.tags': '標籤',
|
||||
'terminal.layer.hostTree.details.lastConnected': '最近連線',
|
||||
'topTabs.openQuickSwitcher': '開啟快速切換',
|
||||
'topTabs.moreTabs': '更多標籤頁',
|
||||
'topTabs.aiAssistant': 'AI 助手',
|
||||
'topTabs.windowOpacity': '視窗透明度',
|
||||
'topTabs.toggleTheme': '切換主題',
|
||||
'topTabs.openSettings': '開啟設定',
|
||||
'ai.chat.sessionHistory': '工作階段歷史',
|
||||
'ai.chat.attach': '附件',
|
||||
'ai.chat.terminalSelectionAttachment': '終端選區',
|
||||
'ai.chat.terminalSelectionLines': '{count} 行',
|
||||
'ai.chat.collapse': '收起',
|
||||
'ai.chat.expand': '展開',
|
||||
'ai.chat.enableAgent': '啟用 {name}',
|
||||
'ai.chat.artifact.noteFallback': 'Vault 筆記',
|
||||
'ai.chat.artifact.openNotes': '開啟筆記',
|
||||
'ai.chat.artifact.openHosts': '開啟主機',
|
||||
'ai.chat.artifact.notesSummary': 'Vault 中有 {count} 條筆記',
|
||||
'ai.chat.artifact.hostsSummary': 'Vault 中有 {count} 台主機',
|
||||
'ai.chat.artifact.hostsAdded': '已新增 {count} 台主機',
|
||||
'ai.chat.artifact.hostsPreview': '預覽 {count} 台主機',
|
||||
'ai.chat.artifact.failed': 'Vault 操作失敗',
|
||||
'ai.chat.artifact.unavailableTitle': '不可用',
|
||||
'ai.chat.artifact.noteMissing': '該筆記已不在 Vault 中。',
|
||||
'ai.chat.artifact.hostMissing': '該主機已不在 Vault 中。',
|
||||
'ai.chat.artifact.snippetMissing': '該 Snippet 或指令碼已不在 Vault 中。',
|
||||
'ai.chat.artifact.openSnippets': '開啟 Snippets',
|
||||
'ai.chat.artifact.snippetsSummary': 'Vault 中有 {count} 個 Snippet',
|
||||
'ai.chat.artifact.scriptsSummary': 'Vault 中有 {count} 個指令碼',
|
||||
'ai.chat.artifact.snippetFallback': 'Vault Snippet',
|
||||
'ai.chat.artifact.scriptFallback': '自動化指令碼',
|
||||
'ai.chat.artifact.scriptLanguage': '{language} 指令碼',
|
||||
'ai.chat.artifact.snippetDeleted': 'Snippet 已刪除',
|
||||
'ai.chat.artifact.scriptDeleted': '指令碼已刪除',
|
||||
'ai.chat.artifact.snippetRan': 'Snippet 已執行',
|
||||
'ai.chat.artifact.scriptStarted': '指令碼執行已啟動',
|
||||
'ai.chat.artifact.scriptRunStatus': '指令碼執行 {status}',
|
||||
'ai.chat.artifact.scriptRunsSummary': '{count} 個指令碼執行',
|
||||
'ai.chat.artifact.scriptRunStopped': '指令碼執行已停止',
|
||||
'ai.chat.artifact.scriptRunPaused': '指令碼執行已暫停',
|
||||
'ai.chat.artifact.scriptRunResumed': '指令碼執行已恢復',
|
||||
'ai.chat.artifact.scriptReference': 'nct API 參考',
|
||||
'zmodem.waitingForRemote': '等待遠端...',
|
||||
'zmodem.uploading': '上傳中',
|
||||
'zmodem.downloading': '下載中',
|
||||
'zmodem.cancelTransfer': '取消傳輸 (Ctrl+C)',
|
||||
'zmodem.overwrite.title': '遠端已存在同名檔案',
|
||||
'zmodem.overwrite.applyToRest': '套用到其餘衝突檔案',
|
||||
'zmodem.overwrite.overwrite': '覆蓋',
|
||||
'zmodem.overwrite.skip': '跳過',
|
||||
'zmodem.overwrite.cancel': '取消',
|
||||
'settings.shortcuts.resetToDefault': '重置為預設',
|
||||
};
|
||||
@@ -0,0 +1,758 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhTWCoreMessages: Messages = {
|
||||
// Common
|
||||
'common.save': '儲存',
|
||||
'common.cancel': '取消',
|
||||
'common.close': '關閉',
|
||||
'common.reset': '重置',
|
||||
'common.zoomIn': '放大',
|
||||
'common.zoomOut': '縮小',
|
||||
'common.settings': '設定',
|
||||
'common.search': '搜尋',
|
||||
'common.connect': '連線',
|
||||
'common.terminal': '終端',
|
||||
'common.create': '建立',
|
||||
'common.add': '新增',
|
||||
'common.rename': '重新命名',
|
||||
'common.refresh': '重新整理',
|
||||
'common.continue': '繼續',
|
||||
'common.enabled': '已啟用',
|
||||
'common.disabled': '已停用',
|
||||
'common.unknownError': '未知錯誤',
|
||||
'common.noResultsFound': '沒有相符結果',
|
||||
'common.back': '返回',
|
||||
'common.apply': '套用',
|
||||
'common.use': '使用',
|
||||
'common.useGlobal': '跟隨全域',
|
||||
'common.left': '左側',
|
||||
'common.right': '右側',
|
||||
'common.more': '更多',
|
||||
'common.selectAHost': '選擇主機',
|
||||
'sort.manual': '手動順序',
|
||||
'sort.az': 'A-z',
|
||||
'sort.za': 'Z-a',
|
||||
'sort.newest': '從新到舊',
|
||||
'sort.oldest': '從舊到新',
|
||||
'sort.group': '按分組',
|
||||
'field.label': 'Label',
|
||||
'field.type': '型別',
|
||||
'auth.keyType': '型別 {type}',
|
||||
'auth.showAllKeys': '顯示全部 keys',
|
||||
|
||||
// Dialogs / prompts
|
||||
'confirm.deleteHost': '刪除主機 "{name}"?',
|
||||
'confirm.deleteIdentity': '刪除身份 "{name}"?',
|
||||
'confirm.removeProvider': '移除供應商 "{name}"?',
|
||||
'confirm.closeBusyTerminal.title': '確認關閉',
|
||||
'confirm.closeBusyTerminal.message': '程序 "{command}" 仍在執行,關閉後會被終止。',
|
||||
'confirm.closeBusyTerminal.messageWithMore': '程序 "{command}" 及其他 {count} 個正在執行的程序將被終止。',
|
||||
'confirm.closeBusyTerminal.cancel': '取消',
|
||||
'confirm.closeBusyTerminal.close': '關閉',
|
||||
'dialog.renameWorkspace.title': '重新命名工作區',
|
||||
'dialog.renameSession.title': '重新命名工作階段',
|
||||
'field.name': '名稱',
|
||||
'placeholder.workspaceName': '工作區名稱',
|
||||
'placeholder.sessionName': '工作階段名稱',
|
||||
'toast.settingsUnavailable': '目前平台無法開啟設定視窗。',
|
||||
'credentials.protectionUnavailable.title': '憑證保護不可用',
|
||||
'credentials.protectionUnavailable.message': '目前裝置無法自動解密已儲存的密碼和金鑰。連線前請重新輸入憑證。',
|
||||
'credentials.protectionUnavailable.action': '開啟設定',
|
||||
|
||||
// Settings shell
|
||||
'settings.title': '設定',
|
||||
'settings.tab.application': '應用程式',
|
||||
'settings.tab.appearance': '外觀',
|
||||
'settings.tab.terminal': '終端',
|
||||
'settings.tab.shortcuts': '快捷鍵',
|
||||
'settings.tab.syncCloud': '同步與雲',
|
||||
'settings.tab.system': '系統',
|
||||
'settings.terminal.connection.verifyHostKeys': '驗證 SSH 主機金鑰',
|
||||
'settings.terminal.connection.verifyHostKeys.desc': '連線到新的或已變化的 SSH 主機金鑰前先詢問。只建議在可信內網中關閉。',
|
||||
'settings.terminal.section.sidePanel': '側邊欄',
|
||||
'settings.terminal.sidePanel.autoOpen': '開啟終端時展開側邊欄',
|
||||
'settings.terminal.sidePanel.autoOpen.desc': '終端連線成功後自動開啟一個側邊欄。',
|
||||
'settings.terminal.sidePanel.autoOpenPane': '預設側邊欄',
|
||||
'settings.terminal.sidePanel.autoOpenPane.desc': '選擇優先開啟哪個面板。',
|
||||
'settings.terminal.sidePanel.pane.sftp': 'SFTP',
|
||||
'settings.terminal.sidePanel.pane.scripts': '指令碼',
|
||||
'settings.terminal.sidePanel.pane.history': '歷史',
|
||||
'settings.terminal.sidePanel.pane.theme': '主題',
|
||||
'settings.terminal.sidePanel.pane.system': '系統',
|
||||
'settings.terminal.sidePanel.pane.notes': '筆記',
|
||||
'settings.terminal.sidePanel.pane.ai': 'AI',
|
||||
|
||||
// Settings > System
|
||||
'settings.system.title': '系統',
|
||||
'settings.system.description': '系統資訊與臨時檔案管理。',
|
||||
'settings.system.tempDirectory': '臨時檔案',
|
||||
'settings.system.location': '位置',
|
||||
'settings.system.fileCount': '檔案數量',
|
||||
'settings.system.totalSize': '佔用空間',
|
||||
'settings.system.openFolder': '開啟資料夾',
|
||||
'settings.system.refresh': '重新整理',
|
||||
'settings.system.clearTempFiles': '清理臨時檔案',
|
||||
'settings.system.clearing': '清理中...',
|
||||
'settings.system.clearResult': '已刪除 {deleted} 個檔案,{failed} 個失敗。',
|
||||
'settings.system.tempDirectoryHint': '臨時檔案在使用外部應用程式開啟遠端檔案時建立。SFTP 工作階段關閉時會自動清理。',
|
||||
'settings.system.credentials.title': '憑證保護',
|
||||
'settings.system.credentials.status': '狀態',
|
||||
'settings.system.credentials.checking': '檢查中...',
|
||||
'settings.system.credentials.available': '可用(系統鑰匙串正常)',
|
||||
'settings.system.credentials.unavailable': '不可用(無法解密已儲存憑證)',
|
||||
'settings.system.credentials.unknown': '未知(目前環境不支援)',
|
||||
'settings.system.credentials.unavailableHint': '在其他使用者或機器上加密的憑證無法在此處解密。請在目前裝置重新輸入並儲存憑證。',
|
||||
'settings.system.credentials.portabilityHint': '雲同步可跨裝置,因為使用主金鑰加密;本機 safeStorage 加密僅綁定目前系統使用者/裝置。',
|
||||
|
||||
// Settings > System > Network Proxy
|
||||
'settings.system.networkProxy.title': '網路代理',
|
||||
'settings.system.networkProxy.description': '用於雲端同步與 AI 服務商的 HTTP(S) 代理。與 SSH 的 ProxyJump / ProxyCommand 設定無關。',
|
||||
'settings.system.networkProxy.mode': '代理模式',
|
||||
'settings.system.networkProxy.mode.system': '系統代理',
|
||||
'settings.system.networkProxy.mode.direct': '直連(不使用代理)',
|
||||
'settings.system.networkProxy.mode.custom': '自訂代理',
|
||||
'settings.system.networkProxy.url': '代理位址',
|
||||
'settings.system.networkProxy.url.placeholder': 'http://127.0.0.1:7890',
|
||||
'settings.system.networkProxy.url.desc': '支援 http://、https://、socks5:// 的 host:port 位址。代理 URL 中不支援帳號密碼。',
|
||||
'settings.system.networkProxy.bypass': '略過清單',
|
||||
'settings.system.networkProxy.bypass.placeholder': '<local>,localhost,127.0.0.1',
|
||||
'settings.system.networkProxy.bypass.desc': '逗號分隔、不走代理的主機。可用 <local> 表示本機與區域網路名稱。',
|
||||
'settings.system.networkProxy.hint': '影響 Google Drive / OneDrive / GitHub 同步登入與 AI 服務商請求,不會改變 SSH 連線。',
|
||||
|
||||
// Settings > System > Crash Logs
|
||||
'settings.system.crashLogs.title': '崩潰日誌',
|
||||
'settings.system.crashLogs.description': '檢視主程序錯誤日誌,協助診斷異常行為。',
|
||||
'settings.system.crashLogs.noLogs': '未找到崩潰日誌。',
|
||||
'settings.system.crashLogs.entries': '{count} 條記錄',
|
||||
'settings.system.crashLogs.clear': '清除所有日誌',
|
||||
'settings.system.crashLogs.cleared': '已清除 {count} 個日誌檔案。',
|
||||
'settings.system.crashLogs.source': '來源',
|
||||
'settings.system.crashLogs.time': '時間',
|
||||
'settings.system.crashLogs.message': '訊息',
|
||||
'settings.system.crashLogs.stack': '堆疊追蹤',
|
||||
'settings.system.crashLogs.hint': '崩潰日誌保留 30 天,超期自動清理。',
|
||||
'settings.system.crashLogs.collapse': '收起',
|
||||
'settings.system.crashLogs.expand': '檢視詳情',
|
||||
|
||||
// Settings > System > Software Update
|
||||
'settings.update.title': '軟體更新',
|
||||
'settings.update.currentVersion': '目前版本',
|
||||
'settings.update.checkForUpdates': '檢查更新',
|
||||
'settings.update.checking': '檢查中...',
|
||||
'settings.update.upToDate': '目前已是最新版本。',
|
||||
'settings.update.available': '新版本 {version} 已釋出。',
|
||||
'settings.update.download': '下載更新',
|
||||
'settings.update.downloading': '正在下載... {percent}%',
|
||||
'settings.update.readyToInstall': '更新已下載,準備安裝。',
|
||||
'settings.update.restartNow': '重啟並更新',
|
||||
'settings.update.error': '檢查更新失敗。',
|
||||
'settings.update.downloadError': '下載失敗。',
|
||||
'settings.update.manualDownload': '前往 GitHub 下載',
|
||||
'settings.update.manualDownloadHint': '目前平台不支援自動更新,請前往 GitHub 下載最新版本。',
|
||||
'settings.update.hint': 'Netcatty 從 GitHub Releases 檢查更新。',
|
||||
'settings.update.lastCheckedJustNow': '剛剛',
|
||||
'settings.update.lastCheckedMinutesAgo': '{n} 分鐘前',
|
||||
'settings.update.lastCheckedHoursAgo': '{n} 小時前',
|
||||
'settings.update.lastCheckedPrefix': '上次檢查:',
|
||||
'settings.update.autoUpdateEnabled': '自動更新',
|
||||
'settings.update.autoUpdateEnabledDesc': '有新版本時自動檢查並下載更新。',
|
||||
|
||||
// Settings > Session Restore
|
||||
'settings.sessionRestore.title': '工作階段恢復',
|
||||
'settings.sessionRestore.restorePreviousSession': '恢復上次的標籤頁和工作區',
|
||||
'settings.sessionRestore.restorePreviousSessionDesc': '啟動時重新開啟上次的終端標籤頁和工作區佈局。',
|
||||
'settings.sessionRestore.restoreTerminalCwd': '恢復終端工作目錄',
|
||||
'settings.sessionRestore.restoreTerminalCwdDesc': '重新連線恢復的終端時,嘗試回到上次記錄的目錄;失敗時不會阻塞連線。',
|
||||
'terminal.restore.placeholder.title': '已恢復的工作階段',
|
||||
'terminal.restore.placeholder.desc': '此終端目前已斷開。準備好後可以重新連線,為這個標籤頁啟動新的 shell。',
|
||||
'terminal.restore.placeholder.reconnect': '重新連線',
|
||||
'terminal.restore.cwdLog': '正在恢復工作目錄:{cwd}',
|
||||
|
||||
// Settings > Session Logs
|
||||
'settings.sessionLogs.title': '工作階段日誌',
|
||||
'settings.sessionLogs.description': '設定工作階段日誌匯出和自動儲存設定。',
|
||||
'settings.sessionLogs.autoSave': '自動儲存',
|
||||
'settings.sessionLogs.enableAutoSave': '啟用自動儲存',
|
||||
'settings.sessionLogs.enableAutoSaveDesc': '在終端工作階段結束時自動儲存工作階段日誌。',
|
||||
'settings.sessionLogs.directory': '儲存目錄',
|
||||
'settings.sessionLogs.noDirectory': '未選擇目錄',
|
||||
'settings.sessionLogs.browse': '瀏覽',
|
||||
'settings.sessionLogs.openFolder': '開啟資料夾',
|
||||
'settings.sessionLogs.directoryHint': '日誌將按主機名組織在子目錄中。',
|
||||
'settings.sessionLogs.format': '日誌格式',
|
||||
'settings.sessionLogs.formatDesc': '選擇儲存日誌檔案的格式。',
|
||||
'settings.sessionLogs.formatTxt': '純文字 (.txt)',
|
||||
'settings.sessionLogs.formatRaw': '原始格式 (.log)',
|
||||
'settings.sessionLogs.formatHtml': 'HTML (.html)',
|
||||
'settings.sessionLogs.timestamps': '新增時間戳',
|
||||
'settings.sessionLogs.timestampsDesc': '為純文字和 HTML 日誌的每一行新增本機時間。',
|
||||
'settings.sessionLogs.hint': '工作階段日誌用於記錄終端輸出,便於故障排查和審計。',
|
||||
|
||||
// Settings > SSH Debug Logs
|
||||
'settings.sshDebugLogs.title': 'SSH 除錯日誌',
|
||||
'settings.sshDebugLogs.enable': '啟用 SSH 除錯日誌',
|
||||
'settings.sshDebugLogs.enableDesc': '記錄連線、認證、握手、斷開和錯誤原因,不記錄終端輸出。',
|
||||
'settings.sshDebugLogs.location': '日誌位置',
|
||||
'settings.sshDebugLogs.status': '狀態',
|
||||
'settings.sshDebugLogs.statusOn': '已開啟',
|
||||
'settings.sshDebugLogs.statusOff': '未開啟',
|
||||
'settings.sshDebugLogs.size': '大小',
|
||||
'settings.sshDebugLogs.hint': '開啟後,新發起的 SSH 連線會寫入診斷資訊,方便排查堡壘機、認證和異常斷開問題。',
|
||||
|
||||
// Settings > Global Hotkey (Quake Mode)
|
||||
'settings.globalHotkey.title': '全域快捷鍵',
|
||||
'settings.globalHotkey.toggleWindow': '切換視窗',
|
||||
'settings.globalHotkey.toggleWindowDesc': '按下組合鍵以設定顯示/隱藏視窗的全域快捷鍵。',
|
||||
'settings.globalHotkey.notSet': '未設定',
|
||||
'settings.globalHotkey.reset': '恢復預設',
|
||||
'settings.globalHotkey.closeToTray': '關閉時最小化到系統匣',
|
||||
'settings.globalHotkey.closeToTrayDesc': '啟用後,關閉視窗將最小化到系統匣而不是結束程式。',
|
||||
'settings.globalHotkey.enabled': '啟用全域快捷鍵',
|
||||
'settings.globalHotkey.enabledDesc': '註冊系統級鍵盤快捷鍵。停用後將取消所有全域快捷鍵註冊。',
|
||||
'settings.globalHotkey.hint': '全域快捷鍵在系統範圍內工作,可快速顯示或隱藏視窗(下拉式終端風格)。',
|
||||
|
||||
// Tray Panel
|
||||
'tray.openMainWindow': '開啟主視窗',
|
||||
'tray.sessions': '工作階段',
|
||||
'tray.portForwarding': '埠轉發',
|
||||
'tray.status.connected': '已連線',
|
||||
'tray.status.connecting': '連線中',
|
||||
'tray.status.disconnected': '已斷開',
|
||||
'tray.status.active': '已啟用',
|
||||
'tray.status.inactive': '未啟用',
|
||||
'tray.status.error': '錯誤',
|
||||
'tray.recentHosts': '最近連線的主機',
|
||||
'tray.empty.title': '一切都很安靜',
|
||||
'tray.empty.subtitle': '去連線個伺服器吧,它們想念你了 🚀',
|
||||
'tray.quit': '結束 Netcatty',
|
||||
|
||||
// Vault Sidebar
|
||||
'vault.sidebar.collapse': '收起側邊欄',
|
||||
'vault.sidebar.expand': '展開側邊欄',
|
||||
'vault.sidebar.resize': '調整側邊欄寬度',
|
||||
|
||||
// Settings > Application
|
||||
'settings.application.checkUpdates': '檢查更新',
|
||||
'settings.application.reportProblem': '反饋問題',
|
||||
'settings.application.reportProblem.subtitle': '生成預填的 GitHub issue',
|
||||
'settings.application.community': '社群',
|
||||
'settings.application.community.subtitle': 'GitHub Discussions',
|
||||
'settings.application.github': 'GitHub',
|
||||
'settings.application.github.subtitle': '原始碼',
|
||||
'settings.application.whatsNew': '更新內容',
|
||||
'settings.application.whatsNew.subtitle': '檢視釋出說明',
|
||||
'settings.application.openExternal.failedTitle': '無法開啟連結',
|
||||
'settings.application.openExternal.failedBody': '系統瀏覽器和內建瀏覽器視窗都無法開啟該連結。',
|
||||
'settings.vault.title': '主機庫',
|
||||
'settings.vault.showRecentHosts': '顯示最近連線的主機',
|
||||
'settings.vault.showRecentHostsDesc': '在主機列表頂部顯示最近連線過的主機',
|
||||
'settings.vault.showOnlyUngroupedHostsInRoot': '根目錄只顯示未分組主機',
|
||||
'settings.vault.showOnlyUngroupedHostsInRootDesc': '開啟後,主機庫根目錄的主機列表只顯示沒有分組的主機,已分組主機請從左側分組進入檢視。',
|
||||
'settings.vault.showSftpTab': '顯示 SFTP 標籤頁',
|
||||
'settings.vault.showSftpTabDesc': '在頂部標籤欄顯示獨立的 SFTP 檢視。關閉後可改用工作階段內左側的 SFTP 側欄。',
|
||||
'settings.vault.showHostTreeSidebar': '顯示主機列表側欄',
|
||||
'settings.vault.showHostTreeSidebarDesc': '在終端和編輯器標籤頁顯示主機列表側欄及頂部開關。',
|
||||
|
||||
// Update notifications
|
||||
'update.available.title': '發現新版本',
|
||||
'update.available.message': '新版本 {version} 已釋出,點選前往下載。',
|
||||
'update.checking': '正在檢查更新...',
|
||||
'update.upToDate.title': '已是最新版本',
|
||||
'update.upToDate.message': '目前版本 ({version}) 已是最新。',
|
||||
'update.error': '檢查更新失敗',
|
||||
'update.downloadNow': '立即下載',
|
||||
'update.viewInSettings': '在設定中檢視',
|
||||
'update.readyToInstall.title': '更新已就緒',
|
||||
'update.readyToInstall.message': '版本 {version} 已下載完成,準備安裝。',
|
||||
'update.restartNow': '立即重啟',
|
||||
'update.downloadFailed.title': '更新失敗',
|
||||
'update.downloadFailed.message': '下載更新失敗,可前往 GitHub 手動下載。',
|
||||
'update.needsSave.title': '有未儲存內容',
|
||||
'update.needsSave.message': '請先儲存已開啟的編輯器,然後再次點選「立即重啟」以安裝更新。',
|
||||
'update.openReleases': '開啟 Releases',
|
||||
'update.remindLater': '稍後提醒',
|
||||
'update.skipVersion': '跳過此版本',
|
||||
|
||||
// Settings > Appearance
|
||||
'settings.appearance.uiTheme': '介面主題',
|
||||
'settings.appearance.theme': '主題',
|
||||
'settings.appearance.theme.desc': '選擇淺色、深色或跟隨系統設定',
|
||||
'settings.appearance.theme.light': '淺色',
|
||||
'settings.appearance.theme.dark': '深色',
|
||||
'settings.appearance.theme.system': '系統',
|
||||
'settings.appearance.accentColor': '強調色',
|
||||
'settings.appearance.customColor': '自訂顏色',
|
||||
'settings.appearance.accentColor.mode': '使用自訂強調色',
|
||||
'settings.appearance.accentColor.mode.desc': '覆蓋主題自帶的強調色',
|
||||
'settings.appearance.accentColor.custom': '自訂強調色',
|
||||
'settings.appearance.themeColor': '主題色',
|
||||
'settings.appearance.themeColor.desc': '為淺色與深色主題選擇預設配色',
|
||||
'settings.appearance.themeColor.light': '淺色主題',
|
||||
'settings.appearance.themeColor.dark': '深色主題',
|
||||
'settings.appearance.customCss': '自訂 CSS',
|
||||
'settings.appearance.customCss.desc':
|
||||
'使用自訂 CSS 個人化介面,修改會立即生效。主要 UI 區塊都公開了 [data-section="..."] 屬性供你定位,比如:snippets-panel、host-details-panel、group-details-panel、serial-host-details-panel、ai-chat-panel、vault-sidebar、vault-main、vault-hosts-header、vault-host-list、vault-view、terminal-workspace、terminal-workspace-sidebar(Focus 模式終端列表)、terminal-host-tree-sidebar、terminal-host-tree-sidebar-content、terminal-host-tree-sidebar-row、terminal-side-panel(SFTP/指令碼/主題/AI 側欄,開啟時生效)、terminal-side-panel-tabs、terminal-side-panel-content、terminal-sftp-panel、terminal-sftp-host-header、terminal-sftp-pane、terminal-sftp-toolbar、terminal-sftp-path、terminal-sftp-filter-bar、terminal-sftp-list、terminal-sftp-list-header、terminal-sftp-list-row、terminal-sftp-tree、terminal-sftp-tree-row、terminal-sftp-transfer-queue、terminal-sftp-transfer-row、terminal-split-pane、terminal-split-resizer、top-tabs、top-tabs-host-tree-toggle、top-tabs-quick-switcher-toggle。',
|
||||
'settings.appearance.customCss.placeholder':
|
||||
'/* 範例 — 由於 Tailwind 優先順序較高,需要使用 !important */\n\n/* 隱藏頂部標籤欄裡的主機列表開關 */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* 隱藏開啟快速切換器的加號按鈕 */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* SFTP / 操作側欄邊框(關閉側欄後不會殘留) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* 修改整個操作側欄背景,而不只是頂部標籤 */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* 修改選取的 SFTP 檔案行 */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* 加粗分割分割線 */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* 標示目前聚焦的分割 */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* 也可在 設定 → 終端 → 工作區聚焦指示 → 聚焦窗格顯示邊框 */',
|
||||
'settings.appearance.language': '語言',
|
||||
'settings.appearance.language.desc': '選擇介面語言',
|
||||
'settings.appearance.uiFont': '介面字型',
|
||||
'settings.appearance.uiFont.desc': '選擇軟體介面使用的字型',
|
||||
'settings.appearance.windowOpacity': '視窗透明度',
|
||||
'settings.appearance.windowOpacity.desc': '調節整個應用程式視窗的透明度,方便疊在其他內容上方。較低時終端文字也會變淡;部分 Linux 桌面環境可能不支援。',
|
||||
'settings.appearance.appIcon': '應用程式圖示',
|
||||
'settings.appearance.appIcon.desc': '選擇 Dock/工作列圖示風格,切換應用程式時更容易找到 Netcatty。執行時生效,安裝包內的圖示不會改變。',
|
||||
'settings.appearance.appIcon.original': '原始',
|
||||
'settings.appearance.appIcon.bright': '明亮',
|
||||
'settings.appearance.appIcon.dark': '深色',
|
||||
'settings.appearance.appIcon.colorful': '彩色',
|
||||
'settings.appearance.appIcon.highContrast': '高對比',
|
||||
'settings.appearance.appIcon.group.classic': '彩色底',
|
||||
'settings.appearance.appIcon.group.white': '白底',
|
||||
'settings.appearance.appIcon.group.special': '隱藏款',
|
||||
'settings.appearance.appIcon.whiteNavy': '白底 · 海軍藍貓',
|
||||
'settings.appearance.appIcon.whiteSky': '白底 · 天藍貓',
|
||||
'settings.appearance.appIcon.whiteRose': '白底 · 玫紅貓',
|
||||
'settings.appearance.appIcon.whiteEmerald': '白底 · 翡翠貓',
|
||||
'settings.appearance.appIcon.whiteAmber': '白底 · 琥珀貓',
|
||||
'settings.appearance.appIcon.whiteViolet': '白底 · 紫羅蘭貓',
|
||||
'settings.appearance.appIcon.rainbow': '彩虹貓',
|
||||
// Context menus / common actions
|
||||
'action.newHost': '新建主機',
|
||||
'action.newSubfolder': '新建資料夾',
|
||||
'action.copyPublicKey': '複製公鑰',
|
||||
'action.keyExport': '匯出金鑰',
|
||||
'action.edit': '編輯',
|
||||
'action.delete': '刪除',
|
||||
'action.remove': '移除',
|
||||
'action.convertToHost': '轉換為主機',
|
||||
|
||||
// Sync
|
||||
'sync.cloudSync': '雲同步',
|
||||
'sync.settings': '同步設定',
|
||||
'sync.active': '雲同步已啟用',
|
||||
'sync.syncing': '正在同步…',
|
||||
'sync.error': '同步錯誤',
|
||||
'sync.notConfigured': '未設定',
|
||||
'sync.failed': '同步失敗',
|
||||
'sync.connected': '已連線',
|
||||
'sync.syncNow': '立即同步',
|
||||
'sync.recentActivity': '最近活動',
|
||||
'sync.pending': '有待同步的變更',
|
||||
'sync.history.uploaded': '已 Upload',
|
||||
'sync.history.downloaded': '已 Download',
|
||||
'sync.history.resolved': '已處理',
|
||||
'sync.toast.completedMessage': '同步完成',
|
||||
'sync.toast.errorTitle': '同步錯誤',
|
||||
'sync.autoSync.failedTitle': '同步失敗',
|
||||
'sync.autoSync.inspectFailedTitle': '同步已暫停',
|
||||
'sync.autoSync.inspectFailedMessage': '無法存取雲端以檢查變更。資料改動或下次啟動時會自動重試。',
|
||||
'sync.autoSync.syncedTitle': '已從雲端同步',
|
||||
'sync.autoSync.syncedMessage': '你的資料已從雲端更新。',
|
||||
'sync.autoSync.noProvider': '未連線雲同步 provider。請開啟 設定 → Sync & Cloud 進行連線。',
|
||||
'sync.autoSync.alreadySyncing': '同步正在進行中。',
|
||||
'sync.autoSync.restoreInProgress': '另一個視窗中的本機備份恢復正在進行中,請等待其完成。',
|
||||
'sync.autoSync.interruptedApplyTitle': '同步已暫停 — 上次恢復未完成',
|
||||
'sync.autoSync.interruptedApplyMessage': '上次本機恢復過程未正常結束,本機資料可能處於半套用狀態。請開啟「設定 → Sync & Cloud → 恢復」,從保護性備份中恢復後再讓自動同步繼續。',
|
||||
'sync.autoSync.vaultLocked': 'Vault 處於鎖定狀態。請開啟 設定 → Sync & Cloud 解鎖。',
|
||||
'sync.autoSync.conflictDetected': '偵測到同步衝突。請開啟 設定 → Sync & Cloud 處理。',
|
||||
'sync.autoSync.syncFailed': '同步失敗',
|
||||
'sync.autoSync.restoredTitle': '已恢復',
|
||||
'sync.autoSync.restoredMessage': '已從雲端恢復主機庫資料。',
|
||||
'sync.autoSync.keptLocalTitle': '已保留本機資料',
|
||||
'sync.autoSync.keptLocalMessage': '保留了空的本機主機庫,未套用雲端資料。',
|
||||
'sync.autoSync.emptyVaultConflict.title': '偵測到空主機庫',
|
||||
'sync.autoSync.emptyVaultConflict.description': '本機主機庫為空,但云端有資料。這通常發生在應用程式更新或儲存重置之後。請選擇如何處理:',
|
||||
'sync.autoSync.emptyVaultConflict.cloudLabel': '雲端',
|
||||
'sync.autoSync.emptyVaultConflict.restore': '從雲端恢復',
|
||||
'sync.autoSync.emptyVaultConflict.restoreDesc': '推薦 — 從雲端備份恢復主機、金鑰和程式碼片段',
|
||||
'sync.autoSync.emptyVaultConflict.keepEmpty': '保持為空',
|
||||
'sync.autoSync.emptyVaultConflict.keepEmptyDesc': '從頭開始,使用空的主機庫',
|
||||
'sync.autoSync.emptyVaultConflict.cloudSummary': '{hosts} 台主機,{keys} 個金鑰,{snippets} 個程式碼片段,{notes} 條筆記,{proxyProfiles} 個代理',
|
||||
'sync.autoSync.emptyVaultManual': '無法同步:本機 vault 為空。請先從本機備份恢復,或在同步面板裡使用"強制推送"。',
|
||||
|
||||
'sync.blocked.title': '同步已暫停',
|
||||
'sync.blocked.reason.bulkShrink': '即將從雲端刪除 {baseCount} 條 {entityType} 中的 {lost} 條(縮減 {percent}%)。',
|
||||
'sync.blocked.reason.largeShrink': '即將從雲端刪除 {lost} 條 {entityType}。',
|
||||
'sync.blocked.detail': '通常是本機狀態異常(鑰匙串故障、資料載入不全)導致。請從本機備份恢復,如果確實要刪這些條目請使用強制推送。',
|
||||
'sync.blocked.restoreButton': '從本機備份恢復',
|
||||
'sync.blocked.forcePushButton': '強制推送',
|
||||
|
||||
'sync.forcePush.title': '確認強制推送',
|
||||
'sync.forcePush.body': '你將從雲端移除 {lost} 條 {entityType},此操作不可復原。繼續?',
|
||||
'sync.forcePush.confirm': '確認推送',
|
||||
'sync.forcePush.cancel': '取消',
|
||||
|
||||
'sync.entityType.hosts': '主機',
|
||||
'sync.entityType.keys': '金鑰',
|
||||
'sync.entityType.identities': '身份',
|
||||
'sync.entityType.proxyProfiles': '代理設定',
|
||||
'sync.entityType.snippets': '程式碼片段',
|
||||
'sync.entityType.customGroups': '分組',
|
||||
'sync.entityType.snippetPackages': '片段包',
|
||||
'sync.entityType.knownHosts': '主機金鑰記錄',
|
||||
'sync.entityType.portForwardingRules': '埠轉發規則',
|
||||
'sync.entityType.groupConfigs': '分組設定',
|
||||
|
||||
'sync.credentialsUnavailable': '目前裝置無法解密部分已儲存憑證。請先在本機重新輸入憑證後再同步。',
|
||||
'time.never': '從未',
|
||||
'time.justNow': '剛剛',
|
||||
'time.minutesAgo': '{minutes} 分鐘前',
|
||||
|
||||
// Vault navigation
|
||||
'vault.nav.hosts': '主機',
|
||||
'vault.nav.keychain': '鑰匙串',
|
||||
'vault.nav.proxies': '代理',
|
||||
'vault.nav.portForwarding': '埠轉發',
|
||||
'vault.nav.snippets': '程式碼片段',
|
||||
'vault.nav.notes': '筆記',
|
||||
'vault.nav.knownHosts': '已知主機',
|
||||
'vault.nav.logs': '日誌',
|
||||
|
||||
'proxyProfiles.action.add': '新增代理',
|
||||
'proxyProfiles.search.placeholder': '搜尋代理…',
|
||||
'proxyProfiles.section.proxies': '代理',
|
||||
'proxyProfiles.count.items': '{count} 項',
|
||||
'proxyProfiles.empty.title': '暫無代理',
|
||||
'proxyProfiles.empty.desc': '建立可重複使用的 HTTP、SOCKS5 或指令代理,然後在主機詳情裡選擇。',
|
||||
'proxyProfiles.usage': '已關聯 {count} 處',
|
||||
'proxyProfiles.copyName': '{name} 副本',
|
||||
'proxyProfiles.panel.newTitle': '新建代理',
|
||||
'proxyProfiles.field.name': '代理名稱',
|
||||
'proxyProfiles.error.required': '名稱和代理詳情不能為空。',
|
||||
'proxyProfiles.error.port': '埠必須在 1 到 65535 之間。',
|
||||
'proxyProfiles.viewMode': '代理顯示方式',
|
||||
'proxyProfiles.delete.title': '刪除代理?',
|
||||
'proxyProfiles.delete.desc': '刪除 "{name}" 會同時從 {count} 個主機或分組設定中解除關聯。',
|
||||
|
||||
'vault.groups.title': '分組',
|
||||
'vault.groups.total': '共 {count} 個',
|
||||
'vault.groups.hostsCount': '{count} 台主機',
|
||||
'vault.groups.newSubgroup': '新建子分組',
|
||||
'vault.groups.rename': '重新命名分組',
|
||||
'vault.groups.unnamed': '未命名分組',
|
||||
'vault.groups.delete': '刪除分組',
|
||||
'vault.groups.createSubfolder': '建立子分組',
|
||||
'vault.groups.createRoot': '建立根分組',
|
||||
'vault.groups.createDialog.desc': '建立新的分組用於組織主機。',
|
||||
'vault.groups.renameDialogTitle': '重新命名分組',
|
||||
'vault.groups.renameDialog.desc': '重新命名已有分組。',
|
||||
'vault.groups.deleteDialogTitle': '刪除分組',
|
||||
'vault.groups.deleteDialog.desc': '這將永久刪除該分組並將所有主機移動到根級別。',
|
||||
'vault.groups.deleteDialog.managedDesc': '這是一個託管的 SSH config 分組。刪除後將同時刪除所有主機並斷開與原始檔的連線。',
|
||||
'vault.groups.deleteDialog.deleteHosts': '同時刪除該分組下的所有主機',
|
||||
'vault.groups.ungrouped': '未分組',
|
||||
'vault.groups.field.name': '分組名稱',
|
||||
'vault.groups.placeholder.example': '例如:Production',
|
||||
'vault.groups.parentLabel': '父級',
|
||||
'vault.groups.pathLabel': '路徑',
|
||||
'vault.groups.settings': '分組設定',
|
||||
'vault.groups.details': '分組詳情',
|
||||
'vault.groups.details.general': '常規',
|
||||
'vault.groups.details.ssh': 'SSH',
|
||||
'vault.groups.details.telnet': 'Telnet',
|
||||
'vault.groups.details.advanced': '高階',
|
||||
'vault.groups.details.appearance': '外觀',
|
||||
'vault.groups.details.mosh': 'Mosh',
|
||||
'vault.groups.details.parentGroup': '父分組',
|
||||
'vault.groups.details.none': '無',
|
||||
'vault.groups.details.inherited': '繼承自分組',
|
||||
'vault.groups.details.addProtocol': '新增協定',
|
||||
'vault.groups.details.removeProtocol': '移除協定',
|
||||
'vault.groups.details.fontFamily': '字型',
|
||||
'vault.groups.details.fontSize': '字號',
|
||||
'vault.groups.errors.required': '分組名稱不能為空。',
|
||||
'vault.groups.errors.invalidChars': "分組名稱不能包含 '/' 或 '\\\\'.",
|
||||
'vault.groups.errors.duplicatePath': '該位置已存在同名分組。',
|
||||
|
||||
'vault.deleteConfirm.title': '刪除「{name}」?',
|
||||
'vault.deleteConfirm.desc': '此操作不可復原。',
|
||||
'vault.deleteConfirm.packageDesc': '這會刪除程式碼包,包內程式碼片段會保留,並移出該程式碼包。',
|
||||
'vault.deleteConfirm.noteGroupDesc': '這會刪除資料夾,資料夾內筆記會保留,並移出該資料夾。',
|
||||
'vault.deleteConfirm.portForwardingDesc': '這會刪除此埠轉發規則。',
|
||||
|
||||
'vault.managedSource.unmanage': '取消託管',
|
||||
'vault.managedSource.unmanageSuccess': '已取消託管分組',
|
||||
|
||||
'vault.hosts.header.entries': '{count} 條',
|
||||
'vault.hosts.header.live': '{count} 個線上',
|
||||
|
||||
// Vault hosts header/actions
|
||||
'vault.hosts.search.placeholder': '查詢主機或 ssh user@hostname / ssh -p 2222 user@hostname…',
|
||||
'vault.hosts.connect': '連線',
|
||||
'vault.view.grid': '網格',
|
||||
'vault.view.list': '列表',
|
||||
'vault.view.tree': '樹形',
|
||||
'vault.tree.expandAll': '展開全部',
|
||||
'vault.tree.collapseAll': '摺疊全部',
|
||||
'vault.hosts.newHost': '新建主機',
|
||||
'vault.hosts.newGroup': '新建分組',
|
||||
'vault.hosts.import': '匯入',
|
||||
'vault.hosts.export': '匯出',
|
||||
'vault.hosts.export.toast.success': '已匯出 {count} 個主機到 CSV',
|
||||
'vault.hosts.export.toast.successWithSkipped': '已匯出 {count} 個主機到 CSV(跳過 {skipped} 個不支援的主機)',
|
||||
'vault.hosts.export.toast.noHosts': '沒有主機可匯出',
|
||||
'vault.hosts.allHosts': '全部主機',
|
||||
'vault.hosts.pinned': '已置頂',
|
||||
'vault.hosts.recentlyConnected': '最近連線',
|
||||
'vault.hosts.pinToTop': '置頂',
|
||||
'vault.hosts.unpin': '取消置頂',
|
||||
'vault.hosts.copyCredentials': '複製賬密資訊',
|
||||
'vault.hosts.copyCredentials.toast.success': '賬密資訊已複製到剪貼簿',
|
||||
'vault.hosts.copyCredentials.toast.noPassword': '該主機未儲存密碼',
|
||||
'vault.hosts.multiSelect': '多選',
|
||||
'vault.hosts.selected': '已選擇 {count} 項',
|
||||
'vault.hosts.selectAll': '全選',
|
||||
'vault.hosts.deselectAll': '取消全選',
|
||||
'vault.hosts.deleteSelected': '刪除 ({count})',
|
||||
'vault.hosts.deleteMultiple.success': '已刪除 {count} 個主機',
|
||||
'vault.hosts.connectSelected': '連線 ({count})',
|
||||
'vault.hosts.connectMultiple.success': '正在連線 {count} 個主機',
|
||||
'vault.hosts.moveToGroup.success': '已將 {host} 移動到 {group}',
|
||||
'vault.hosts.errors.nameRequired': '主機名稱不能為空。',
|
||||
'vault.hosts.empty.title': '設定你的主機',
|
||||
'vault.hosts.empty.desc': '儲存主機以快速連線到你的伺服器、虛擬機器和容器。',
|
||||
'deepLink.ssh.invalid': '無效的 ssh:// 連結',
|
||||
'deepLink.telnet.invalid': '無效的 telnet:// 連結',
|
||||
'deepLink.jms.invalid': '無效的 JumpServer 連結',
|
||||
'deepLink.jms.unsupported': '不支援的 JumpServer 協定:{{protocol}}',
|
||||
|
||||
// Vault import
|
||||
'vault.import.title': '新增資料到你的 Vault',
|
||||
'vault.import.desc': '從常見工具遷移連線資訊。選擇一種格式開始匯入。',
|
||||
'vault.import.chooseFormat': '選擇檔案格式',
|
||||
'vault.import.csv.tip': '批次匯入:可使用 CSV 模板填寫後匯入。',
|
||||
'vault.import.csv.downloadTemplate': '下載 CSV 模板',
|
||||
'vault.import.toast.start': '正在從 {format} 匯入...',
|
||||
'vault.import.toast.completedTitle': '匯入完成',
|
||||
'vault.import.toast.failedTitle': '匯入失敗',
|
||||
'vault.import.toast.noEntries': '{format} 檔案中沒有可匯入的條目。',
|
||||
'vault.import.toast.noNewHosts': '從 {format} 沒有匯入到新的主機。',
|
||||
'vault.import.toast.summary': '已匯入 {count} 個主機(跳過 {skipped},重複 {duplicates})。',
|
||||
'vault.import.toast.firstIssue': '首個問題:{issue}',
|
||||
'vault.import.sshConfig.chooseMode': '選擇如何匯入你的 SSH config 檔案。',
|
||||
'vault.import.sshConfig.modeQuestion': '你希望如何匯入?',
|
||||
'vault.import.sshConfig.importOnly': '僅匯入',
|
||||
'vault.import.sshConfig.importOnlyDesc': '一次性匯入,修改不會同步回檔案。',
|
||||
'vault.import.sshConfig.managed': '託管同步',
|
||||
'vault.import.sshConfig.managedDesc': '保持同步,修改會自動儲存回檔案。',
|
||||
'vault.import.sshConfig.managedGroup': 'ssh config',
|
||||
'vault.import.sshConfig.managedSuccess': '已匯入 {count} 個主機,檔案已託管。',
|
||||
'vault.import.sshConfig.alreadyManaged': '該檔案已被託管。',
|
||||
'vault.import.sshConfig.alreadyManagedDesc': '該檔案已在分組 "{group}" 下託管。如需重新匯入,請先移除現有的託管源。',
|
||||
'vault.import.sshConfig.noFilePath': '無法託管此檔案。',
|
||||
'vault.import.sshConfig.noFilePathDesc': '無法確定檔案路徑。託管同步需要存取檔案系統。',
|
||||
|
||||
// Known Hosts
|
||||
'knownHosts.search.placeholder': '搜尋已知主機...',
|
||||
'knownHosts.action.scanSystem': '掃描系統',
|
||||
'knownHosts.action.importFile': '匯入檔案',
|
||||
'knownHosts.action.browseFile': '瀏覽檔案',
|
||||
'knownHosts.empty.title': '暫無已知主機',
|
||||
'knownHosts.empty.desc':
|
||||
'Known Hosts 是你之前連線過的 SSH server。匯入系統的 known_hosts 檔案以開始。',
|
||||
'knownHosts.results.showingLimited': '顯示 {shown}/{total} 個主機。使用搜索查詢特定主機。',
|
||||
'knownHosts.toast.scanUnavailable': '目前平台無法掃描系統 known_hosts。',
|
||||
'knownHosts.toast.scanNoFile': '未找到系統 known_hosts 檔案。',
|
||||
'knownHosts.toast.scanNoEntries': 'known_hosts 中沒有可用條目。',
|
||||
'knownHosts.toast.scanImported': '已匯入 {count} 個新主機。',
|
||||
'knownHosts.toast.scanNoNew': '沒有發現新的主機。',
|
||||
'knownHosts.toast.scanFailed': '掃描系統 known_hosts 失敗。',
|
||||
|
||||
// Port Forwarding
|
||||
'pf.empty.title': '設定埠轉發規則',
|
||||
'pf.empty.desc': '儲存埠轉發規則,用於存取資料庫、Web 應用程式等服務。',
|
||||
'pf.title': '埠轉發規則',
|
||||
'pf.rulesCount': '{count} 條規則',
|
||||
'pf.wizard.editTitle': '編輯埠轉發規則',
|
||||
'pf.wizard.newTitle': '新建埠轉發規則',
|
||||
'pf.wizard.saveChanges': '儲存修改',
|
||||
'pf.wizard.done': '完成',
|
||||
'pf.wizard.continue': '繼續',
|
||||
'pf.wizard.cancel': '取消',
|
||||
'pf.wizard.skipWizard': '跳過精靈',
|
||||
'pf.error.hostNotFound': '未找到主機',
|
||||
'pf.toast.titleWithLabel': '埠轉發規則: {label}',
|
||||
'pf.type.local': '本機轉發',
|
||||
'pf.type.remote': '遠端轉發',
|
||||
'pf.type.dynamic': '動態轉發',
|
||||
'pf.type.menu.local': '本機轉發',
|
||||
'pf.type.menu.remote': '遠端轉發',
|
||||
'pf.type.menu.dynamic': '動態轉發',
|
||||
'pf.type.local.desc': '本機轉發讓你像存取本機一樣存取遠端服務埠。',
|
||||
'pf.type.remote.desc': '遠端轉發在遠端開啟埠,並將連線轉發到本機(目前)主機。',
|
||||
'pf.type.dynamic.desc': '動態轉發將 Netcatty 作為 SOCKS 代理使用。',
|
||||
'pf.wizard.type.title': '選擇埠轉發型別:',
|
||||
'pf.wizard.localConfig.title': '設定本機埠與繫結地址:',
|
||||
'pf.wizard.localConfig.desc': '該埠會在本機(目前裝置)開啟,並接收流量。',
|
||||
'pf.wizard.localConfig.localPort': '本機埠 *',
|
||||
'pf.wizard.bindAddress': '繫結地址',
|
||||
'pf.wizard.remoteHost.title': '選擇遠端主機:',
|
||||
'pf.wizard.remoteHost.desc': '選擇要開啟埠的遠端主機。該埠的流量將轉發到目標地址。',
|
||||
'pf.wizard.remoteConfig.title': '設定埠與繫結地址:',
|
||||
'pf.wizard.remoteConfig.desc': '將從所選主機的指定埠與網路卡地址轉發流量。',
|
||||
'pf.wizard.remoteConfig.remotePort': '遠端埠 *',
|
||||
'pf.wizard.destination.title': '設定目標地址:',
|
||||
'pf.wizard.destination.desc.local': '輸入你希望透過 tunnel 存取的遠端目標地址。',
|
||||
'pf.wizard.destination.desc.remote': '要轉發流量到的目標地址與埠。',
|
||||
'pf.wizard.destination.address': '目標地址 *',
|
||||
'pf.wizard.destination.addressPlaceholder': '例如:127.0.0.1 或 192.168.1.100',
|
||||
'pf.wizard.destination.port': '目標埠 *',
|
||||
'pf.wizard.sshServer.title': '選擇 SSH server:',
|
||||
'pf.wizard.sshServer.desc.dynamic': '選擇作為 SOCKS proxy 的 SSH server。',
|
||||
'pf.wizard.sshServer.desc.default': '選擇用於將流量 tunnel 到目標地址的 SSH server。',
|
||||
'pf.wizard.label.title': '設定 Label:',
|
||||
'pf.wizard.label.placeholder.dynamic': '例如:SOCKS Proxy',
|
||||
'pf.wizard.label.placeholder.default': '例如:MySQL Production',
|
||||
'pf.wizard.label.placeholder.remoteRule': '例如:Remote Rule',
|
||||
'pf.wizard.placeholders.portExample': '例如:{port}',
|
||||
|
||||
// SFTP
|
||||
'sftp.newFolder': '新建資料夾',
|
||||
'sftp.newFile': '新建檔案',
|
||||
'sftp.filter': '篩選',
|
||||
'sftp.filter.placeholder': '按檔名篩選...',
|
||||
'sftp.bookmark.add': '收藏此路徑',
|
||||
'sftp.bookmark.remove': '取消收藏',
|
||||
'sftp.bookmark.list': '收藏路徑',
|
||||
'sftp.bookmark.addGlobal': '+全域',
|
||||
'sftp.bookmark.addGlobalTooltip': '儲存為全域收藏(所有主機共享)',
|
||||
'sftp.bookmark.empty': '暫無收藏路徑',
|
||||
'sftp.columns.name': '名稱',
|
||||
'sftp.columns.modified': '修改時間',
|
||||
'sftp.columns.size': '大小',
|
||||
'sftp.columns.kind': '型別',
|
||||
'sftp.columns.actions': '操作',
|
||||
'sftp.emptyDirectory': '空目錄',
|
||||
'sftp.nav.up': '返回上層',
|
||||
'sftp.nav.home': '返回主目錄',
|
||||
'sftp.nav.refresh': '重新整理',
|
||||
'sftp.upload': '上傳',
|
||||
'sftp.uploadFiles': '上傳檔案',
|
||||
'sftp.uploadFolder': '上傳資料夾',
|
||||
'sftp.dragDropToUpload': '拖拽檔案到這裡上傳',
|
||||
'sftp.retry': '重試',
|
||||
'sftp.context.open': '開啟',
|
||||
'sftp.context.navigateTo': '跳轉到這裡',
|
||||
'sftp.context.moveTo': '移動到...',
|
||||
'sftp.context.moveToParent': '移動到上級目錄',
|
||||
'sftp.moveTo.title': '移動到目錄',
|
||||
'sftp.moveTo.placeholder': '輸入目標目錄路徑',
|
||||
'sftp.moveTo.confirm': '移動',
|
||||
'sftp.moveTo.pathNotFound': '目錄不存在或無法存取',
|
||||
'sftp.context.download': '下載',
|
||||
'sftp.context.copyToOtherPane': '複製到另一側',
|
||||
'sftp.copyCurrentPath': '複製目前路徑',
|
||||
'sftp.copyCurrentPath.success': '已複製目前路徑',
|
||||
'sftp.copyCurrentPath.error': '無法複製目前路徑',
|
||||
'sftp.viewMode.label': '檢視模式',
|
||||
'sftp.viewMode.list': '列表檢視',
|
||||
'sftp.viewMode.tree': '樹形檢視',
|
||||
'sftp.viewMode.switchToList': '切換到列表檢視',
|
||||
'sftp.viewMode.switchToTree': '切換到樹形檢視',
|
||||
'sftp.tree.loadError': '載入目錄失敗',
|
||||
'sftp.tree.loading': '載入中...',
|
||||
'sftp.kind.folder': '資料夾',
|
||||
'sftp.context.rename': '重新命名',
|
||||
'sftp.context.permissions': '權限',
|
||||
'sftp.context.delete': '刪除',
|
||||
'sftp.context.refresh': '重新整理',
|
||||
'sftp.context.uploadFiles': '上傳檔案...',
|
||||
'sftp.context.uploadFilesHere': '上傳檔案到這裡...',
|
||||
'sftp.context.uploadFolder': '上傳資料夾...',
|
||||
'sftp.context.uploadFolderHere': '上傳資料夾到這裡...',
|
||||
'sftp.context.downloadSelected': '下載選取項({count})',
|
||||
'sftp.context.deleteSelected': '刪除選取項({count})',
|
||||
'sftp.dropFilesHere': '拖拽檔案到這裡',
|
||||
'sftp.itemsCount': '{count} 個專案',
|
||||
'sftp.selectedCount': '已選 {count} 個',
|
||||
'sftp.path.doubleClickToEdit': '雙擊編輯路徑',
|
||||
'sftp.showHiddenPaths': '隱藏的路徑',
|
||||
'sftp.task.waiting': '等待中...',
|
||||
'sftp.transfer.preparing': '準備中...',
|
||||
'sftp.status.loading': '載入中...',
|
||||
'sftp.status.uploading': '上傳中...',
|
||||
'sftp.status.ready': '就緒',
|
||||
'sftp.transfers': '傳輸',
|
||||
'sftp.transfers.active': '{count} 個進行中',
|
||||
'sftp.transfers.clearCompleted': '清除已完成',
|
||||
'sftp.transfers.calculatingTotal': '正在統計總大小...',
|
||||
'sftp.transfers.filesCount': '{count} 個檔案',
|
||||
'sftp.transfers.filesProgress': '{current}/{total} 個檔案',
|
||||
'sftp.transfers.expandChildren': '展開檔案',
|
||||
'sftp.transfers.collapseChildren': '收起檔案',
|
||||
'sftp.transfers.expandChildList': '展開詳情',
|
||||
'sftp.transfers.collapseChildList': '收起',
|
||||
'sftp.transfers.retryAction': '重試',
|
||||
'sftp.transfers.dismissAction': '移除',
|
||||
'sftp.transfers.openTargetFolder': '開啟目標目錄',
|
||||
'sftp.transfers.openTargetFolderError': '無法開啟目標目錄',
|
||||
'sftp.transfers.copyTargetPath': '複製目標路徑',
|
||||
'sftp.transfers.copyTargetPathSuccess': '已複製目標路徑',
|
||||
'sftp.transfers.copyTargetPathError': '無法複製目標路徑',
|
||||
'sftp.transfers.resizeNameColumn': '調整檔名列寬',
|
||||
'sftp.transfers.dragToResize': '拖拽調整高度',
|
||||
'sftp.goUp': '上一級',
|
||||
'sftp.goToTerminalCwd': '定位到終端目前目錄',
|
||||
'sftp.followTerminalCwd': '追隨終端目錄',
|
||||
'sftp.followTerminalCwd.enable': '開啟追隨終端目錄',
|
||||
'sftp.followTerminalCwd.disable': '關閉追隨終端目錄',
|
||||
'sftp.encoding.label': '檔名編碼',
|
||||
'sftp.encoding.auto': '自動',
|
||||
'sftp.encoding.utf8': 'UTF-8',
|
||||
'sftp.encoding.gb18030': 'GB18030',
|
||||
'sftp.goHome': '返回主目錄',
|
||||
'sftp.folderName': '資料夾名稱',
|
||||
'sftp.folderName.placeholder': '輸入資料夾名稱',
|
||||
'sftp.fileName': '檔名稱',
|
||||
'sftp.fileName.placeholder': '輸入檔名稱',
|
||||
'sftp.prompt.newFolderName': '新建資料夾名稱?',
|
||||
'sftp.rename.title': '重新命名',
|
||||
'sftp.rename.newName': '新名稱',
|
||||
'sftp.rename.placeholder': '輸入新名稱',
|
||||
'sftp.confirm.deleteOne': '刪除 "{name}"?',
|
||||
'sftp.deleteConfirm.single': '刪除 "{name}"?',
|
||||
'sftp.deleteConfirm.title': '刪除 {count} 個專案?',
|
||||
'sftp.deleteConfirm.desc': '此操作不可復原,將刪除以下內容:',
|
||||
'sftp.deleteConfirm.descSingle': '此操作不可復原。',
|
||||
'sftp.deleteConfirm.host': '主機',
|
||||
'sftp.deleteConfirm.path': '路徑',
|
||||
'sftp.error.loadFailed': '載入目錄失敗',
|
||||
'sftp.error.downloadFailed': '下載失敗',
|
||||
'sftp.error.uploadFailed': '上傳失敗',
|
||||
'sftp.error.deleteFailed': '刪除失敗',
|
||||
'sftp.error.createFolderFailed': '建立資料夾失敗',
|
||||
'sftp.error.createFileFailed': '建立檔案失敗',
|
||||
'sftp.error.invalidFileName': '檔名包含非法字元:{chars}',
|
||||
'sftp.error.reservedName': '此檔名是系統保留名稱',
|
||||
'sftp.overwrite.title': '檔案已存在',
|
||||
'sftp.overwrite.desc': '名為"{name}"的檔案已存在。是否要替換它?',
|
||||
'sftp.overwrite.confirm': '替換',
|
||||
'sftp.error.renameFailed': '重新命名失敗',
|
||||
'sftp.picker.title': '選擇主機',
|
||||
'sftp.picker.desc': '為{side}窗格選擇主機',
|
||||
'sftp.picker.searchPlaceholder': '搜尋主機...',
|
||||
'sftp.picker.local.title': '本機檔案系統',
|
||||
'sftp.picker.local.desc': '瀏覽本機檔案',
|
||||
'sftp.picker.local.badge': '本機',
|
||||
'sftp.picker.connected.section': '已連線',
|
||||
'sftp.picker.connected.badge': '已連線',
|
||||
'sftp.picker.connected.connecting': '連線中',
|
||||
'sftp.picker.noMatch': '沒有相符的主機',
|
||||
'sftp.permissions.title': '編輯權限',
|
||||
'sftp.permissions.owner': '所有者',
|
||||
'sftp.permissions.group': '群組',
|
||||
'sftp.permissions.others': '其他',
|
||||
'sftp.permissions.octal': '八進位制',
|
||||
'sftp.permissions.symbolic': '符號',
|
||||
'sftp.permissions.success': '權限已更新',
|
||||
'sftp.permissions.failed': '權限更新失敗',
|
||||
|
||||
// Quick Switcher
|
||||
'qs.search.placeholder': '搜尋主機或標籤頁',
|
||||
'qs.jumpTo': '跳轉到',
|
||||
'qs.localTerminal': '本機終端',
|
||||
'qs.localShells': '本機 Shell',
|
||||
'qs.default': '預設',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
export const zhTWScriptsMessages = {
|
||||
'scripts.meta.name': '名稱',
|
||||
'scripts.meta.language': '語言',
|
||||
'scripts.meta.description': '描述',
|
||||
'scripts.meta.descriptionPlaceholder': '可選,指令碼的簡要說明',
|
||||
'scripts.meta.trigger': '觸發方式',
|
||||
'scripts.meta.triggerPattern': '輸出比對(正規表示式)',
|
||||
'scripts.meta.code': '指令碼程式碼',
|
||||
'scripts.trigger.manual': '手動執行',
|
||||
'scripts.trigger.onConnect': '連線時執行',
|
||||
'scripts.trigger.onOutput': '輸出比對時執行',
|
||||
'scripts.trigger.onOutputHint': '僅在伺服器端輸出比對到正規表示式時觸發(會忽略本機按鍵回顯)。未設定目標主機時,對目前已連線的工作階段生效;設定目標後僅監聽對應主機。進入備用畫面的應用程式(如 vim、htop)期間不會觸發。本 session 有其他指令碼執行期間會抑制;指令碼結束後再檢查 buffer。流程內等待一般文字用 nct.screen.waitForText,正規表示式用 nct.screen.waitForRegex。',
|
||||
'scripts.actions.save': '儲存',
|
||||
'scripts.actions.runNow': '立即執行',
|
||||
'scripts.actions.openEditor': '彈出視窗編輯',
|
||||
'scripts.actions.openEditorHint': '在更大的視窗中編輯指令碼',
|
||||
'scripts.editor.modalTitle': '指令碼編輯器',
|
||||
'scripts.editor.modalSubtitle': '在更大的工作區中編輯指令碼的中繼資料與程式碼。',
|
||||
'scripts.editor.lineCount': '{count} 行',
|
||||
'scripts.editor.resize': '調整編輯器高度',
|
||||
'scripts.targets.hint': '自動觸發會作用於所選主機;勾選「對所有主機生效」則作用於全部可連線的主機。',
|
||||
'scripts.targets.connectOrderHint': '連線指令碼的執行順序請在主機詳情 → 自動化 中調整。',
|
||||
'scripts.actions.runNowHint': '在所選目標主機上執行;勾選「對所有主機生效」時在所有可連線的主機上執行。',
|
||||
'scripts.actions.runParallel': '在所有標籤頁並行執行',
|
||||
'scripts.actions.runSequential': '在所有標籤頁依序執行',
|
||||
'scripts.actions.skippedConnectingSessions': '有 {count} 個標籤仍在連線中,已跳過',
|
||||
'scripts.actions.noRunnableHosts': '沒有與指令碼目標相符的可連線主機',
|
||||
'scripts.sidePanel.library': '指令碼庫',
|
||||
'scripts.sidePanel.running': '執行中',
|
||||
'scripts.sidePanel.newScript': '新建指令碼',
|
||||
'scripts.running.empty': '目前工作階段沒有正在執行的指令碼。',
|
||||
'scripts.running.unnamed': '未命名指令碼',
|
||||
'scripts.running.status.running': '執行中',
|
||||
'scripts.running.status.paused': '已暫停',
|
||||
'scripts.running.status.completed': '已完成',
|
||||
'scripts.running.status.failed': '失敗',
|
||||
'scripts.running.waitingFor': '等待 {pattern}',
|
||||
'scripts.running.waitingForLabel': '等待',
|
||||
'scripts.running.waitingForShellPrompt': 'Shell 提示字元(# 或 $)',
|
||||
'scripts.running.lastSent': '已傳送:{command}',
|
||||
'scripts.recording.start': '開始錄製',
|
||||
'scripts.recording.active': '停止錄製',
|
||||
'scripts.recording.startHint': '在目前終端工作階段中錄製操作,自動產生 nct 指令碼程式碼',
|
||||
'scripts.recording.unavailableHint': '此入口不支援錄製,請用終端右側的指令碼側欄',
|
||||
'scripts.recording.activeHint': '正在錄製目前終端。在終端中正常輸入指令;點「停止錄製」或終端工具列的 REC 結束並儲存。',
|
||||
'scripts.recording.started': '已開始錄製,請在終端中操作',
|
||||
'scripts.recording.noSession': '請先連線終端(單一標籤或工作區皆可)',
|
||||
'scripts.recording.alreadyActive': '已有其他終端正在錄製',
|
||||
'scripts.recording.stop': '停止錄製',
|
||||
'scripts.recording.pause': '暫停錄製',
|
||||
'scripts.recording.resume': '繼續錄製',
|
||||
'scripts.recording.saveTitle': '儲存錄製的指令碼',
|
||||
'scripts.recording.namePlaceholder': '指令碼名稱',
|
||||
'scripts.recording.packagePlaceholder': '儲存到資料夾',
|
||||
'scripts.recording.rootPackage': '根目錄',
|
||||
'scripts.recording.save': '儲存',
|
||||
'scripts.recording.saveAndEdit': '儲存並編輯',
|
||||
'scripts.recording.helpTitle': '如何錄製指令碼',
|
||||
'scripts.recording.helpIntro': '錄製會把你在終端裡的操作自動轉成可重複執行的自動化指令碼,適合部署、巡檢等重複性任務。',
|
||||
'scripts.recording.helpStep1': '連線主機。單一終端標籤或工作區裡的終端都可以,開啟右側的指令碼側欄即可。',
|
||||
'scripts.recording.helpStep2': '點選「開始錄製」。終端工具列出現紅色 REC 表示已開始。',
|
||||
'scripts.recording.helpStep3': '在終端裡像平常一樣輸入指令,按 Enter 執行。每次按 Enter 都會記為一步。',
|
||||
'scripts.recording.helpStep4': '全部操作完成後,點選「停止錄製」,或點終端工具列 REC 旁的停止按鈕。',
|
||||
'scripts.recording.helpStep5': '填寫指令碼名稱並儲存。需要微調時可選「儲存並編輯」,在指令碼編輯器裡修改產生的程式碼。',
|
||||
'scripts.recording.helpTipsTitle': '小提示',
|
||||
'scripts.recording.helpTip1': '兩次操作之間停頓超過 1 秒,會自動加入等待時間,避免回放太快。',
|
||||
'scripts.recording.helpTip2': '每條指令後會等待 shell 提示字元(如 $ 或 #)出現,再記錄下一步,減少亂序。',
|
||||
'scripts.recording.helpTip3': '密碼輸入會被標記為敏感內容,不會以明文寫入指令碼。',
|
||||
'scripts.recording.helpTip4': 'vim、互動式選單等複雜操作,建議先錄主幹指令,再在編輯器裡手動補充。',
|
||||
'scripts.dialog.title': '指令碼',
|
||||
'scripts.dialog.ok': '確定',
|
||||
'scripts.dialog.required': '必填',
|
||||
'scripts.dialog.numberInvalid': '請輸入有效數字',
|
||||
'scripts.dialog.numberMin': '不能小於 {min}',
|
||||
'scripts.dialog.numberMax': '不能大於 {max}',
|
||||
'scripts.dialog.numberStep': '步進必須為 {step} 的倍數',
|
||||
'scripts.dialog.waitForTimeoutTitle': '等待逾時',
|
||||
'scripts.dialog.retry': '重試',
|
||||
'scripts.dialog.skip': '跳過',
|
||||
'scripts.dialog.abort': '終止',
|
||||
'scripts.running.stepProgress': '步驟 {current} / {total}',
|
||||
'scripts.running.determinateProgress': '{label} {current}/{total}',
|
||||
'scripts.running.progressFallback': '進度',
|
||||
'scripts.running.operationsCount': '已執行 {count} 個操作',
|
||||
'scripts.running.opsPrefix': '已執行 ',
|
||||
'scripts.running.opsSuffix': ' 個操作',
|
||||
'scripts.running.elapsedLabel': '耗時',
|
||||
'scripts.running.lastSentLabel': '已傳送:',
|
||||
'scripts.running.elapsed': '耗時 {elapsed}',
|
||||
'scripts.running.completedSummary': '已完成 · 共 {count} 個操作 · {elapsed}',
|
||||
'scripts.running.dismissHint': '點選關閉',
|
||||
'scripts.running.dismiss': '關閉',
|
||||
'scripts.running.viewLogs': '檢視日誌',
|
||||
'scripts.running.logTitle': '{name} · 執行日誌',
|
||||
'scripts.running.logEmpty': '暫無日誌輸出',
|
||||
'scripts.running.pause': '暫停',
|
||||
'scripts.running.resume': '繼續',
|
||||
'scripts.running.stop': '停止',
|
||||
'scripts.recording.saved': '指令碼已儲存',
|
||||
'scripts.recording.savedNamed': '已儲存「{name}」',
|
||||
'scripts.observer.blocked': 'Observer 模式禁止執行會寫入終端的指令碼。',
|
||||
'vault.section.scripts': '指令碼',
|
||||
'vault.nav.scripts': '指令碼',
|
||||
'snippets.action.newScript': '新建自動化指令碼',
|
||||
'hostDetails.section.automation': '自動化',
|
||||
'hostDetails.automation.loginScript': '登入指令碼',
|
||||
'hostDetails.automation.loginScriptPlaceholder': '選擇指令碼',
|
||||
'hostDetails.automation.none': '無',
|
||||
'hostDetails.automation.outputTriggers': '輸出觸發器',
|
||||
'hostDetails.automation.addTrigger': '新增觸發器',
|
||||
'hostDetails.automation.triggerPatternPlaceholder': '正規表示式',
|
||||
'hostDetails.automation.linkedScripts': '已關聯指令碼',
|
||||
'hostDetails.automation.linkedScriptsEmpty': '尚未關聯任何指令碼。',
|
||||
'hostDetails.automation.linkScriptPlaceholder': '關聯既有指令碼…',
|
||||
'hostDetails.automation.unlink': '取消關聯',
|
||||
'hostDetails.automation.equivalenceHint': '此處關聯會將本主機加入 Scripts 中該指令碼的目標列表,兩側檢視同步。',
|
||||
'hostDetails.automation.queueHint': '連線本主機時依序執行指令碼。全域指令碼最先執行,再執行下方佇列。',
|
||||
'hostDetails.automation.globalScripts': '全域連線指令碼',
|
||||
'hostDetails.automation.globalScriptsHint': '每次連線時最先執行。順序在指令碼庫排序中調整。',
|
||||
'hostDetails.automation.connectQueue': '本主機執行佇列',
|
||||
'hostDetails.automation.connectQueueEmpty': '尚未為本主機設定連線指令碼佇列。',
|
||||
'hostDetails.automation.addToQueuePlaceholder': '新增指令碼到佇列…',
|
||||
'hostDetails.automation.moveUp': '上移',
|
||||
'hostDetails.automation.moveDown': '下移',
|
||||
'hostDetails.automation.removeFromQueue': '從佇列移除',
|
||||
'hostDetails.automation.dragHandle': '拖曳排序',
|
||||
'hostDetails.automation.queueDragHint': '拖動項目即可調整連線時的執行順序。',
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhTwSystemManagerMessages: Messages = {
|
||||
'terminal.layer.system': '系統',
|
||||
|
||||
'systemManager.noSession': '沒有使用中的終端工作階段。',
|
||||
'systemManager.notConnected': '請先連線到主機以管理程序與服務。',
|
||||
'systemManager.empty': '暫無資料。',
|
||||
'systemManager.tabs.overview': '概覽',
|
||||
'systemManager.tabs.processes': '程序',
|
||||
'systemManager.tabs.tmux': 'tmux',
|
||||
'systemManager.tabs.docker': 'Docker',
|
||||
'systemManager.popup.loading': '正在開啟終端…',
|
||||
'systemManager.popup.startupFailed': '啟動指令未順利完成。請確認目標仍可使用後再試一次。',
|
||||
|
||||
'systemManager.errors.loadProcesses': '載入程序列表失敗',
|
||||
'systemManager.errors.loadTmux': '載入 tmux 工作階段失敗',
|
||||
'systemManager.errors.loadTmuxWindows': '載入 tmux 視窗失敗',
|
||||
'systemManager.errors.loadTmuxPanes': '載入 tmux 窗格失敗',
|
||||
'systemManager.errors.loadTmuxClients': '載入 tmux 用戶端失敗',
|
||||
'systemManager.errors.actionFailed': '操作失敗',
|
||||
'systemManager.errors.loadDocker': '載入容器列表失敗',
|
||||
'systemManager.errors.loadDockerStats': '載入容器效能資料失敗',
|
||||
'systemManager.errors.loadDockerImages': '載入映像列表失敗',
|
||||
'systemManager.errors.loadOverview': '載入系統概覽失敗',
|
||||
'systemManager.errors.sshChannelUnavailable': '伺服器拒絕開啟新的執行通道。請稍後重試,或重新連線目前主機。',
|
||||
|
||||
'systemManager.overview.empty': '暫無系統概覽資料。',
|
||||
'systemManager.overview.loading': '正在載入系統概覽…',
|
||||
'systemManager.overview.memory': '記憶體',
|
||||
'systemManager.overview.disk': '磁碟',
|
||||
'systemManager.overview.network': '網路',
|
||||
'systemManager.overview.rx': '下行',
|
||||
'systemManager.overview.tx': '上行',
|
||||
'systemManager.overview.cores': '{{count}} 核心',
|
||||
'systemManager.overview.load': '負載',
|
||||
'systemManager.overview.uptime': '執行時間',
|
||||
'systemManager.overview.duration.daysHours': '{{days}}天 {{hours}}小時',
|
||||
'systemManager.overview.duration.hoursMinutes': '{{hours}}小時 {{minutes}}分鐘',
|
||||
'systemManager.overview.duration.minutes': '{{minutes}}分鐘',
|
||||
'systemManager.overview.system': '系統',
|
||||
'systemManager.overview.kernel': '核心',
|
||||
'systemManager.overview.swap': '交換區',
|
||||
'systemManager.overview.latency': '延遲',
|
||||
'systemManager.overview.cpuCores': 'CPU 核心',
|
||||
'systemManager.overview.disks': '磁碟',
|
||||
'systemManager.overview.interfaces': '網路介面',
|
||||
'systemManager.overview.topProcesses': '記憶體占用最高程序',
|
||||
'systemManager.overview.noData': '暫無資料',
|
||||
'systemManager.overview.noDisks': '暫無磁碟資料',
|
||||
'systemManager.overview.noInterfaces': '暫無網卡資料',
|
||||
'systemManager.overview.noTopProcesses': '暫無程序資料',
|
||||
|
||||
'systemManager.processes.search': '搜尋程序…',
|
||||
'systemManager.processes.command': '指令',
|
||||
'systemManager.processes.user': '使用者',
|
||||
'systemManager.processes.term': '終止',
|
||||
'systemManager.processes.kill': '強制終止',
|
||||
'systemManager.processes.stop': '暫停 (SIGSTOP)',
|
||||
'systemManager.processes.cont': '繼續 (SIGCONT)',
|
||||
'systemManager.processes.hup': '掛斷 (SIGHUP)',
|
||||
'systemManager.processes.renice': '調整優先順序',
|
||||
'systemManager.processes.renicePrompt': 'Nice 值 (-20 到 19)',
|
||||
'systemManager.processes.reniceInvalid': 'Nice 值必須在 -20 到 19 之間',
|
||||
'systemManager.processes.confirmKill': '向程序 {{pid}} 傳送 SIGKILL?',
|
||||
'systemManager.processes.confirmSignal': '向程序 {{pid}} 傳送 SIG{{signal}}?',
|
||||
'systemManager.processes.filter.all': '全部',
|
||||
'systemManager.processes.filter.running': '執行中',
|
||||
'systemManager.processes.ppid': '父程序 PID',
|
||||
'systemManager.processes.rss': '實體記憶體',
|
||||
'systemManager.processes.vsz': '虛擬記憶體',
|
||||
'systemManager.processes.elapsed': '執行時長',
|
||||
'systemManager.processes.stat': '狀態',
|
||||
'systemManager.processes.meta': '{{count}} 個程序',
|
||||
'systemManager.processes.loading': '正在載入程序…',
|
||||
'systemManager.processes.loadingMore': '正在載入更多程序…',
|
||||
'systemManager.processes.state.running': '執行中',
|
||||
'systemManager.processes.state.sleeping': '睡眠',
|
||||
'systemManager.processes.state.stopped': '已暫停',
|
||||
'systemManager.processes.state.zombie': '殭屍',
|
||||
'systemManager.processes.sort.cpu': 'CPU',
|
||||
'systemManager.processes.sort.mem': '記憶體',
|
||||
'systemManager.processes.sort.pid': 'PID',
|
||||
'systemManager.processes.sort.command': '指令',
|
||||
'systemManager.processes.sort.user': '使用者',
|
||||
|
||||
'systemManager.common.dismiss': '關閉',
|
||||
'systemManager.common.checkingAvailability': '正在檢查可用狀態…',
|
||||
'systemManager.common.loading': '正在載入…',
|
||||
'systemManager.common.loadingDetails': '正在載入詳情…',
|
||||
'systemManager.common.loadingStats': '正在載入效能資料…',
|
||||
|
||||
'systemManager.tmux.new': '新建',
|
||||
'systemManager.tmux.search': '搜尋工作階段…',
|
||||
'systemManager.tmux.newSessionTitle': '新建 tmux 工作階段',
|
||||
'systemManager.tmux.newSessionDesc': '為工作階段命名,並可選在啟動時執行指令碼。',
|
||||
'systemManager.tmux.newSessionTabCustom': '自訂指令',
|
||||
'systemManager.tmux.newSessionTabSnippet': '從程式碼片段',
|
||||
'systemManager.tmux.pickSnippet': '從程式碼片段選擇',
|
||||
'systemManager.tmux.pickSnippetEmpty': '暫無程式碼片段,可在指令碼側欄或倉庫中新增。',
|
||||
'systemManager.tmux.selectedSnippet': '已選片段:{{label}}',
|
||||
'systemManager.tmux.newSessionName': '工作階段名稱',
|
||||
'systemManager.tmux.newSessionCommand': '啟動指令',
|
||||
'systemManager.tmux.newSessionCommandPlaceholder': '例如 htop 或 npm run dev(可選)',
|
||||
'systemManager.tmux.newSessionCommandHint': '留空則建立預設 shell 工作階段。',
|
||||
'systemManager.tmux.creating': '建立中…',
|
||||
'systemManager.tmux.newSessionPlaceholder': 'my-session',
|
||||
'systemManager.tmux.newSessionRequired': '請先輸入工作階段名稱',
|
||||
'systemManager.tmux.empty': '沒有 tmux 工作階段',
|
||||
'systemManager.tmux.attach': '附加',
|
||||
'systemManager.tmux.attached': '已附加',
|
||||
'systemManager.tmux.detached': '未附加',
|
||||
'systemManager.tmux.windows': '{{count}} 個視窗',
|
||||
'systemManager.tmux.created': '建立時間',
|
||||
'systemManager.tmux.activity': '活動時間',
|
||||
'systemManager.tmux.rename': '重新命名',
|
||||
'systemManager.tmux.detach': '全部分離',
|
||||
'systemManager.tmux.killSession': '結束工作階段',
|
||||
'systemManager.tmux.killServer': '結束 tmux 伺服器',
|
||||
'systemManager.tmux.loadingDetails': '正在載入詳情…',
|
||||
'systemManager.tmux.clients': '已附加用戶端',
|
||||
'systemManager.tmux.windowList': '視窗',
|
||||
'systemManager.tmux.newWindow': '新建視窗',
|
||||
'systemManager.tmux.newWindowPlaceholder': '視窗名稱(可選)',
|
||||
'systemManager.tmux.noWindows': '沒有視窗',
|
||||
'systemManager.tmux.unavailable': '此主機未偵測到 tmux',
|
||||
'systemManager.docker.unavailable': '此主機未偵測到 Docker',
|
||||
'systemManager.tmux.windowsMismatch': '工作階段顯示有 {{count}} 個視窗,但 list-windows 未傳回任何視窗',
|
||||
'systemManager.tmux.lastCommand': '最後執行的指令:{{command}}',
|
||||
'systemManager.tmux.noPanes': '沒有窗格',
|
||||
'systemManager.tmux.panes': '{{count}} 個窗格',
|
||||
'systemManager.tmux.active': '使用中',
|
||||
'systemManager.tmux.unnamedWindow': '未命名視窗',
|
||||
'systemManager.tmux.unnamedPane': '未命名窗格',
|
||||
'systemManager.tmux.attachWindow': '附加到視窗',
|
||||
'systemManager.tmux.selectWindow': '選擇視窗',
|
||||
'systemManager.tmux.killWindow': '關閉視窗',
|
||||
'systemManager.tmux.killPane': '關閉窗格',
|
||||
'systemManager.tmux.splitHorizontal': '水平分割',
|
||||
'systemManager.tmux.splitVertical': '垂直分割',
|
||||
'systemManager.tmux.sendKeys': '傳送按鍵',
|
||||
'systemManager.tmux.sendKeysTo': '向視窗 {{window}} 窗格 {{pane}} 傳送按鍵',
|
||||
'systemManager.tmux.sendKeysPlaceholder': '指令或文字…',
|
||||
'systemManager.tmux.renameSessionPrompt': '重新命名工作階段',
|
||||
'systemManager.tmux.renameWindowPrompt': '重新命名視窗',
|
||||
'systemManager.tmux.windowName': '視窗名稱',
|
||||
'systemManager.tmux.confirmKillSession': '確定結束 tmux 工作階段「{{name}}」?',
|
||||
'systemManager.tmux.confirmDetachSession': '確定將所有用戶端從「{{name}}」分離?',
|
||||
'systemManager.tmux.confirmKillWindow': '確定關閉視窗「{{name}}」?',
|
||||
'systemManager.tmux.confirmKillPane': '確定關閉窗格 #{{index}}?',
|
||||
'systemManager.tmux.confirmKillServer': '確定結束 tmux 伺服器?所有工作階段將被終止。',
|
||||
'systemManager.tmux.meta': '{{count}} 個工作階段',
|
||||
|
||||
'systemManager.docker.title': '容器',
|
||||
'systemManager.docker.subTabs.containers': '容器',
|
||||
'systemManager.docker.subTabs.images': '映像',
|
||||
'systemManager.docker.empty': '未找到容器',
|
||||
'systemManager.docker.imagesEmpty': '未找到映像',
|
||||
'systemManager.docker.search': '搜尋容器…',
|
||||
'systemManager.docker.searchImages': '搜尋映像…',
|
||||
'systemManager.docker.filter.all': '全部',
|
||||
'systemManager.docker.filter.running': '執行中',
|
||||
'systemManager.docker.filter.stopped': '已停止',
|
||||
'systemManager.docker.filter.paused': '已暫停',
|
||||
'systemManager.docker.shell': 'Shell',
|
||||
'systemManager.docker.logs': '日誌',
|
||||
'systemManager.docker.details': '詳情',
|
||||
'systemManager.docker.inspect': 'Inspect',
|
||||
'systemManager.docker.imageInspect': '映像 Inspect',
|
||||
'systemManager.docker.confirmRemove': '確定刪除此容器?',
|
||||
'systemManager.docker.confirmKill': '確定強制終止此容器?',
|
||||
'systemManager.docker.confirmRemoveImage': '確定刪除映像「{{name}}」?',
|
||||
'systemManager.docker.confirmPrune': '確定清理懸空映像?',
|
||||
'systemManager.docker.confirmPruneAll': '確定清理所有未使用映像?',
|
||||
'systemManager.docker.pause': '暫停',
|
||||
'systemManager.docker.unpause': '恢復',
|
||||
'systemManager.docker.restart': '重新啟動',
|
||||
'systemManager.docker.kill': '強制終止',
|
||||
'systemManager.docker.renamePrompt': '容器名稱',
|
||||
'systemManager.docker.prune': '清理懸空',
|
||||
'systemManager.docker.pruneAll': '清理全部',
|
||||
'systemManager.docker.tag': '標記',
|
||||
'systemManager.docker.tagRepoPrompt': '儲存庫名稱',
|
||||
'systemManager.docker.tagNamePrompt': '標籤名稱',
|
||||
'systemManager.docker.meta': '{{count}} 個容器',
|
||||
'systemManager.docker.imagesMeta': '{{count}} 個映像',
|
||||
'systemManager.docker.start': '啟動',
|
||||
'systemManager.docker.stop': '停止',
|
||||
|
||||
'systemManager.inspect.status': '狀態',
|
||||
'systemManager.inspect.image': '映像',
|
||||
'systemManager.inspect.created': '建立時間',
|
||||
'systemManager.inspect.started': '啟動時間',
|
||||
'systemManager.inspect.restartPolicy': '重新啟動原則',
|
||||
'systemManager.inspect.command': '啟動指令',
|
||||
'systemManager.inspect.ports': '連接埠',
|
||||
'systemManager.inspect.networks': '網路',
|
||||
'systemManager.inspect.mounts': '掛載',
|
||||
'systemManager.inspect.env': '環境變數',
|
||||
'systemManager.inspect.labels': '標籤',
|
||||
'systemManager.inspect.tags': '映像標籤',
|
||||
'systemManager.inspect.digests': '摘要',
|
||||
'systemManager.inspect.size': '大小',
|
||||
'systemManager.inspect.platform': '平台',
|
||||
'systemManager.inspect.workdir': '工作目錄',
|
||||
'systemManager.inspect.exposedPorts': '對外開放的連接埠',
|
||||
'systemManager.inspect.showRaw': 'JSON',
|
||||
'systemManager.inspect.hideRaw': '收起 JSON',
|
||||
};
|
||||
@@ -0,0 +1,818 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhTWTerminalMessages: Messages = {
|
||||
'terminal.sudoHint.pressEnter': '按 Enter 貼上 sudo 密碼',
|
||||
'terminal.menu.rename': '重新命名',
|
||||
'terminal.menu.uploadClipboardImage': '上傳剪貼簿圖片',
|
||||
'terminal.clipboardImageUpload.noImage': '剪貼簿中沒有圖片',
|
||||
'terminal.clipboardImageUpload.failed': '無法上傳剪貼簿圖片',
|
||||
'terminal.toolbar.configureOsc7': '設定目錄追蹤',
|
||||
'terminal.toolbar.detach': '移出到獨立標籤',
|
||||
'terminal.menu.detach': '從工作區移出',
|
||||
'terminal.osc7Setup.title': '設定目錄追蹤',
|
||||
'terminal.osc7Setup.desc': 'Netcatty 會為目前遠端使用者新增 OSC 7 提示字元設定。這樣在 sudo 或 su 之後,SFTP 也能繼續跟隨終端目錄。',
|
||||
'terminal.osc7Setup.targets': '可能寫入的檔案',
|
||||
'terminal.osc7Setup.command': '將執行的指令',
|
||||
'terminal.osc7Setup.run': '執行設定',
|
||||
'terminal.osc7Setup.running': '正在設定...',
|
||||
'terminal.osc7Setup.configured': '目錄追蹤已設定',
|
||||
'terminal.osc7Setup.failed': '目錄追蹤設定失敗',
|
||||
'terminal.osc7Setup.sent': '目錄追蹤設定已傳送到終端',
|
||||
'terminal.toolbar.timestampsEnable': '顯示時間戳',
|
||||
'terminal.toolbar.timestampsDisable': '隱藏時間戳',
|
||||
'terminal.progress.enterReconnectHint': '按 Enter 重新連線',
|
||||
'terminal.progress.autoReconnectScheduled': '連線已斷開,將在 {seconds} 秒後自動重連(第 {attempt} 次)。',
|
||||
'terminal.progress.autoReconnectAttempt': '正在自動重連(第 {attempt} 次)...',
|
||||
'terminal.connection.protocol.et': 'EternalTerminal',
|
||||
'terminal.et.proxyUnsupported': 'EternalTerminal 目前不支援 Netcatty 的代理設定。請改用 SSH,或移除該主機的代理。',
|
||||
'terminal.et.multiJumpUnsupported': 'EternalTerminal 目前在 Netcatty 中最多支援一個跳板機。',
|
||||
// Command history side panel
|
||||
'history.scope.label': '歷史範圍',
|
||||
'history.tab.host': '主機',
|
||||
'history.tab.global': '全域',
|
||||
'history.searchPlaceholder': '搜尋歷史指令...',
|
||||
'history.loading': '正在讀取遠端歷史...',
|
||||
'history.meta.count': '{count} 條',
|
||||
'history.empty.noSession': '請先開啟一個遠端工作階段以檢視其指令歷史。',
|
||||
'history.empty.unsupportedProtocol': '僅 SSH/Mosh/ET 工作階段支援指令歷史。',
|
||||
'history.empty.noHistory': '該主機上未找到指令歷史。',
|
||||
'history.empty.noGlobalHistory': '暫無全域指令歷史。你執行的指令會記錄在這裡。',
|
||||
'history.action.refresh': '重新整理',
|
||||
'history.action.retry': '重試',
|
||||
'history.action.paste': '貼上到終端',
|
||||
'history.action.run': '在終端執行',
|
||||
'history.action.saveAsSnippet': '儲存為程式碼片段',
|
||||
// SFTP File Opener
|
||||
'sftp.context.copyPath': '複製檔案路徑',
|
||||
'sftp.context.openWith': '開啟方式...',
|
||||
'sftp.context.edit': '編輯',
|
||||
'sftp.context.preview': '預覽',
|
||||
'sftp.opener.title': '開啟方式',
|
||||
'sftp.opener.desc': '選擇一個應用程式來開啟此檔案',
|
||||
'sftp.opener.builtInEditor': '內建編輯器',
|
||||
'sftp.opener.editDescription': '編輯文字檔案',
|
||||
'sftp.opener.builtInImageViewer': '內建圖片預覽',
|
||||
'sftp.opener.previewDescription': '預覽圖片',
|
||||
'sftp.opener.systemApp': '選擇應用程式...',
|
||||
'sftp.opener.systemAppDescription': '從本機選擇一個應用程式',
|
||||
'sftp.opener.onlySystemApp': '此檔案只能用外部應用程式開啟',
|
||||
'sftp.opener.noAppsAvailable': '無可用應用程式',
|
||||
'sftp.opener.noExtension': '無副檔名檔案',
|
||||
'sftp.opener.setDefault': '始終使用此方式開啟 {ext} 檔案',
|
||||
'sftp.opener.confirmTitle': '設為預設?',
|
||||
'sftp.opener.confirmDescription': '是否始終使用 {app} 開啟 {ext} 檔案?',
|
||||
'sftp.opener.yesRemember': '是,記住此選擇',
|
||||
'sftp.opener.justOnce': '僅此一次',
|
||||
'sftp.opener.confirm.title': '設定預設應用程式',
|
||||
'sftp.opener.confirm.desc': '是否始終使用此應用程式開啟 .{ext} 檔案?',
|
||||
'sftp.editor.title': '文字編輯器',
|
||||
'sftp.editor.save': '儲存到遠端',
|
||||
'sftp.editor.saving': '儲存中...',
|
||||
'sftp.editor.saved': '儲存成功',
|
||||
'sftp.editor.saveFailed': '儲存檔案失敗',
|
||||
'sftp.editor.unsavedChanges': '你有未儲存的更改。確定要關閉嗎?',
|
||||
'sftp.editor.syntaxHighlight': '語法標示',
|
||||
'sftp.preview.title': '圖片預覽',
|
||||
'sftp.preview.zoomIn': '放大',
|
||||
'sftp.preview.zoomOut': '縮小',
|
||||
'sftp.preview.resetZoom': '重置縮放',
|
||||
'sftp.preview.fitToWindow': '適應視窗',
|
||||
|
||||
// Settings > SFTP File Associations
|
||||
'settings.tab.sftpFileAssociations': 'SFTP',
|
||||
'settings.sftp.transferConcurrency': '傳輸併發數',
|
||||
'settings.sftp.transferConcurrency.desc': '上傳或下載資料夾時並行傳輸的檔案數量。較高的值可能提高速度,但可能導致某些伺服器過載。',
|
||||
'settings.sftp.defaultOpener': '預設檔案開啟方式',
|
||||
'settings.sftp.defaultOpener.desc': '選擇沒有特定檔案關聯時的預設開啟方式',
|
||||
'settings.sftp.defaultOpener.ask': '每次詢問',
|
||||
'settings.sftp.defaultOpener.askDesc': '每次開啟檔案時彈出選擇對話方塊',
|
||||
'settings.sftp.defaultOpener.builtInDesc': '預設使用內建編輯器開啟文字檔案',
|
||||
'settings.sftp.defaultOpener.systemApp': '選擇應用程式...',
|
||||
'settings.sftp.defaultOpener.systemAppDesc': '預設使用指定的外部應用程式開啟檔案',
|
||||
'settings.sftpFileAssociations.title': 'SFTP 檔案關聯',
|
||||
'settings.sftpFileAssociations.desc': '設定按副檔名開啟檔案的預設應用程式',
|
||||
'settings.sftpFileAssociations.extension': '副檔名',
|
||||
'settings.sftpFileAssociations.application': '應用程式',
|
||||
'settings.sftpFileAssociations.noAssociations': '未設定檔案關聯',
|
||||
'settings.sftpFileAssociations.remove': '移除',
|
||||
'settings.sftpFileAssociations.removeConfirm': '確定移除 .{ext} 的關聯嗎?',
|
||||
|
||||
// Settings > SFTP Behavior
|
||||
'settings.sftp.doubleClickBehavior': '雙擊行為',
|
||||
'settings.sftp.doubleClickBehavior.desc': '選擇在 SFTP 檢視中雙擊檔案時的操作',
|
||||
'settings.sftp.doubleClickBehavior.open': '開啟檔案',
|
||||
'settings.sftp.doubleClickBehavior.transfer': '傳輸到另一側',
|
||||
'settings.sftp.doubleClickBehavior.openDesc': '使用預設應用程式開啟檔案',
|
||||
'settings.sftp.doubleClickBehavior.transferDesc': '將檔案傳輸到另一窗格的使用中主機',
|
||||
'settings.sshDeepLink.title': 'SSH 和 Telnet 連結',
|
||||
'settings.sshDeepLink.enable': '用 Netcatty 開啟 ssh:// 和 telnet:// 連結',
|
||||
'settings.sshDeepLink.enableDesc': '允許 Netcatty 接管來自瀏覽器和其他應用程式的 ssh://、telnet:// 連結。',
|
||||
'settings.jmsDeepLink.title': 'JumpServer 連結',
|
||||
'settings.jmsDeepLink.enable': '處理 jms:// 連結',
|
||||
'settings.jmsDeepLink.enableDesc': '將 Netcatty 註冊為 JumpServer jms:// 用戶端連結的處理程式,可能與官方 JumpServer 用戶端衝突。',
|
||||
|
||||
// Settings > SFTP Auto Sync
|
||||
'settings.sftp.autoSync': '自動同步到遠端',
|
||||
'settings.sftp.autoSync.desc': '使用外部應用程式開啟檔案時,自動將檔案更改同步回遠端伺服器',
|
||||
'settings.sftp.autoSync.enable': '啟用自動同步',
|
||||
'settings.sftp.autoSync.enableDesc': '在外部應用程式中儲存檔案時,更改將自動上傳到遠端伺服器',
|
||||
|
||||
// Settings > SFTP 自動開啟側欄
|
||||
'settings.sftp.autoOpenSidebar': '連線時自動開啟側欄',
|
||||
'settings.sftp.autoOpenSidebar.desc': '連線到主機時自動開啟 SFTP 檔案瀏覽器側欄',
|
||||
'settings.sftp.autoOpenSidebar.enable': '啟用自動開啟側欄',
|
||||
'settings.sftp.autoOpenSidebar.enableDesc': '當終端工作階段連線到遠端主機時,SFTP 側欄將自動開啟',
|
||||
|
||||
'settings.sftp.followTerminalCwd': '追隨終端目錄',
|
||||
'settings.sftp.followTerminalCwd.desc': '在側欄 SFTP 中自動跟隨終端目前工作目錄變化(可在工具欄切換)',
|
||||
'settings.sftp.followTerminalCwd.enable': '預設開啟追隨終端目錄',
|
||||
'settings.sftp.followTerminalCwd.enableDesc': '開啟側欄 SFTP 時預設啟用追隨模式,終端執行 cd 後檔案瀏覽器會自動跳轉',
|
||||
|
||||
'settings.sftp.defaultViewMode': '預設檢視模式',
|
||||
'settings.sftp.defaultViewMode.desc': '選擇開啟新 SFTP 標籤頁時的預設檢視模式。每個主機的偏好設定會覆蓋此全域設定。',
|
||||
'settings.sftp.defaultViewMode.list': '列表檢視',
|
||||
'settings.sftp.defaultViewMode.listDesc': '以平面列表顯示目前目錄的檔案',
|
||||
'settings.sftp.defaultViewMode.tree': '樹形檢視',
|
||||
'settings.sftp.defaultViewMode.treeDesc': '以層級樹形結構顯示檔案',
|
||||
|
||||
'sftp.autoSync.success': '檔案已同步到遠端:{fileName}',
|
||||
'sftp.autoSync.error': '同步檔案失敗:{error}',
|
||||
|
||||
// SFTP Folder Upload Progress
|
||||
'sftp.upload.progress': '正在上傳 {current}/{total} 個檔案...',
|
||||
'sftp.upload.uploading': '正在上傳...',
|
||||
'sftp.upload.compressing': '正在壓縮...',
|
||||
'sftp.upload.extracting': '正在解壓...',
|
||||
'sftp.upload.scanning': '正在掃描檔案...',
|
||||
'sftp.upload.completed': '已完成',
|
||||
'sftp.upload.compressed': '壓縮傳輸',
|
||||
'sftp.upload.currentFile': '目前: {fileName}',
|
||||
'sftp.upload.cancelled': '上傳已取消',
|
||||
'sftp.upload.cancel': '取消',
|
||||
'sftp.upload.completedToPath': '已上傳至 {path}',
|
||||
|
||||
// SFTP Download
|
||||
'sftp.download.completed': '已下載',
|
||||
'sftp.download.cancelled': '下載已取消',
|
||||
|
||||
// SFTP Reconnecting
|
||||
'sftp.reconnecting.title': '正在重連...',
|
||||
'sftp.reconnecting.desc': '連線已斷開,正在嘗試重新連線',
|
||||
'sftp.reconnected': '連線已恢復',
|
||||
'sftp.error.reconnectFailed': '重連失敗,請重試。',
|
||||
'sftp.error.connectionLostManual': '連線已斷開,請手動重新連線。',
|
||||
'sftp.error.connectionLostReconnecting': '連線已斷開,正在重連...',
|
||||
'sftp.error.sessionLost': 'SFTP 工作階段已斷開,請重新連線。',
|
||||
|
||||
// Settings > SFTP Show Hidden Files
|
||||
'settings.sftp.showHiddenFiles': '顯示隱藏檔案',
|
||||
'settings.sftp.showHiddenFiles.desc': '在 SFTP 檔案瀏覽器中顯示隱藏檔案(Unix/macOS 點檔案和 Windows 隱藏屬性檔案)。',
|
||||
'settings.sftp.showHiddenFiles.enable': '顯示隱藏檔案',
|
||||
'settings.sftp.showHiddenFiles.enableDesc': '瀏覽本機和遠端檔案系統時顯示隱藏檔案',
|
||||
|
||||
// Settings > SFTP Compressed Upload
|
||||
'settings.sftp.compressedUpload': '資料夾壓縮傳輸',
|
||||
'settings.sftp.compressedUpload.desc': '上傳前壓縮資料夾,可大幅減少傳輸時間。',
|
||||
'settings.sftp.compressedUpload.enable': '啟用資料夾壓縮',
|
||||
'settings.sftp.compressedUpload.enableDesc': '自動使用 tar 壓縮資料夾後再傳輸。需要伺服器支援 tar 指令,不支援時自動回退到普通傳輸。',
|
||||
|
||||
// Settings > Terminal
|
||||
'settings.terminal.section.theme': '終端主題',
|
||||
'settings.terminal.themeModal.title': '選擇主題',
|
||||
'settings.terminal.themeModal.darkThemes': '深色主題',
|
||||
'settings.terminal.themeModal.lightThemes': '淺色主題',
|
||||
'settings.terminal.theme.selectButton': '選擇主題',
|
||||
'settings.terminal.theme.followApp': '跟隨應用程式主題',
|
||||
'settings.terminal.theme.followApp.desc': '終端背景色自動配合目前應用程式主題,保持視覺一致性。',
|
||||
'settings.terminal.theme.darkTheme': '深色模式終端主題',
|
||||
'settings.terminal.theme.lightTheme': '淺色模式終端主題',
|
||||
'settings.terminal.theme.auto': '自動(跟隨介面主題)',
|
||||
'settings.terminal.theme.autoDesc': '跟隨目前介面主題預設',
|
||||
'settings.terminal.section.font': '字型',
|
||||
'settings.terminal.section.cursor': '游標',
|
||||
'settings.terminal.section.keyboard': '鍵盤',
|
||||
'settings.terminal.section.accessibility': '無障礙',
|
||||
'settings.terminal.section.behavior': '行為',
|
||||
'settings.terminal.section.scrollback': '捲動緩衝區',
|
||||
'settings.terminal.section.keywordHighlight': '關鍵字標示',
|
||||
'settings.terminal.font.family': '字型',
|
||||
'settings.terminal.font.family.desc': '終端字型',
|
||||
'settings.terminal.font.cjk': '中文 / CJK 字型',
|
||||
'settings.terminal.font.cjk.desc': '用於渲染中 / 日 / 韓字元的字型;"Auto" 會按主字型智慧搭配',
|
||||
'settings.terminal.font.cjk.option.auto': 'Auto · 按主字型智慧搭配',
|
||||
'settings.terminal.font.cjk.option.sarasaSC': 'Sarasa Mono SC (更紗黑體 簡)',
|
||||
'settings.terminal.font.cjk.option.sarasaTC': 'Sarasa Mono TC (更紗黑體 繁)',
|
||||
'settings.terminal.font.cjk.option.mapleCN': 'Maple Mono CN',
|
||||
'settings.terminal.font.cjk.option.sourceHan': 'Source Han Mono SC (思源等寬)',
|
||||
'settings.terminal.font.cjk.option.notoCJK': 'Noto Sans Mono CJK SC',
|
||||
'settings.terminal.font.cjk.option.lxgwWenkai': 'LXGW WenKai Mono (霞鶩文楷等寬)',
|
||||
'settings.terminal.font.cjk.option.simSun': 'SimSun (宋體)',
|
||||
'settings.terminal.font.cjk.option.legacy': '{font} · 不推薦(非等寬字型)',
|
||||
'settings.terminal.font.size': '字型大小',
|
||||
'settings.terminal.font.size.desc': '終端文字大小',
|
||||
'settings.terminal.font.weight': '字重',
|
||||
'settings.terminal.font.weight.desc': '常規文字字重 (100-900)',
|
||||
'settings.terminal.font.weight.thin': '極細',
|
||||
'settings.terminal.font.weight.extraLight': '特細',
|
||||
'settings.terminal.font.weight.light': '細',
|
||||
'settings.terminal.font.weight.normal': '常規',
|
||||
'settings.terminal.font.weight.medium': '中等',
|
||||
'settings.terminal.font.weight.semiBold': '半粗',
|
||||
'settings.terminal.font.weight.bold': '粗',
|
||||
'settings.terminal.font.weight.extraBold': '特粗',
|
||||
'settings.terminal.font.weight.black': '黑體',
|
||||
'settings.terminal.font.weightBold': '粗體字重',
|
||||
'settings.terminal.font.weightBold.desc': '粗體文字字重 (100-900)',
|
||||
'settings.terminal.font.smoothing': '字型平滑',
|
||||
'settings.terminal.font.smoothing.desc': '使用 macOS 原生字型抗鋸齒',
|
||||
'settings.terminal.font.linePadding': '行間距',
|
||||
'settings.terminal.font.linePadding.desc': '行之間的額外間距 (0-10)',
|
||||
'settings.terminal.font.emulationType': '終端模擬型別',
|
||||
'settings.terminal.cursor.style': '游標樣式',
|
||||
'settings.terminal.cursor.style.block': '塊',
|
||||
'settings.terminal.cursor.style.bar': '豎線',
|
||||
'settings.terminal.cursor.style.underline': '下劃線',
|
||||
'settings.terminal.cursor.blink': '游標閃爍',
|
||||
'settings.terminal.keyboard.altAsMeta': '將 Option 作為 Meta 鍵',
|
||||
'settings.terminal.keyboard.altAsMeta.desc': '使用 Option (Alt) 作為 Meta 鍵,而不是用於輸入特殊字元',
|
||||
'settings.terminal.keyboard.optionArrowWordJump': 'Option+←/→ 按單字跳轉',
|
||||
'settings.terminal.keyboard.optionArrowWordJump.desc': '按 Option+左/右 時傳送 Meta-b / Meta-f,讓 Shell 按單字移動游標(而非預設的 ^[[1;3D / ^[[1;3C)',
|
||||
'settings.terminal.accessibility.minimumContrastRatio': '最小對比度',
|
||||
'settings.terminal.accessibility.minimumContrastRatio.desc': '調整顏色以滿足對比度要求 (1 = 停用, 21 = 最大)',
|
||||
'settings.terminal.behavior.rightClick': '右鍵行為',
|
||||
'settings.terminal.behavior.rightClick.desc': '在終端中右鍵時執行的操作',
|
||||
'settings.terminal.behavior.rightClick.menu': '顯示選單',
|
||||
'settings.terminal.behavior.rightClick.paste': '貼上',
|
||||
'settings.terminal.behavior.rightClick.selectWord': '選擇單字',
|
||||
'settings.terminal.behavior.copyOnSelect': '選擇即複製',
|
||||
'settings.terminal.behavior.copyOnSelect.desc': '自動複製選取的文字。在 tmux/vim 滑鼠模式下,macOS 按住 Option,Windows/Linux 按住 Shift 拖選即可選取文字',
|
||||
'settings.terminal.behavior.middleClickPaste': '中鍵貼上',
|
||||
'settings.terminal.behavior.middleClickPaste.desc': '中鍵點選時貼上剪貼簿內容',
|
||||
'settings.terminal.behavior.middleClick': '中鍵行為',
|
||||
'settings.terminal.behavior.middleClick.desc': '在終端中點選滑鼠中鍵時執行的操作',
|
||||
'settings.terminal.behavior.middleClick.menu': '顯示選單',
|
||||
'settings.terminal.behavior.middleClick.paste': '貼上',
|
||||
'settings.terminal.behavior.middleClick.disabled': '無動作',
|
||||
'settings.terminal.behavior.wordSeparators': '單字分隔符',
|
||||
'settings.terminal.behavior.wordSeparators.desc':
|
||||
'雙擊選取文字時作為邊界的字元。空格也算;可加入 =、逗號或 :,方便選取 uid=name 裡的部分內容。',
|
||||
'settings.terminal.behavior.bracketedPaste': '括號貼上模式',
|
||||
'settings.terminal.behavior.bracketedPaste.desc':
|
||||
'貼上文字時使用轉義序列包裹,以便終端區分貼上和鍵入。如果出現 ^[[200~ 字樣請關閉此選項。',
|
||||
'settings.terminal.behavior.shiftEnterNewline': 'Shift+Enter 傳送文字',
|
||||
'settings.terminal.behavior.shiftEnterNewline.desc':
|
||||
'在終端中按 Shift+Enter 時傳送設定的文字,而不是普通 Enter。',
|
||||
'settings.terminal.behavior.shiftEnterNewlineText': '傳送內容',
|
||||
'settings.terminal.behavior.shiftEnterNewlineText.desc':
|
||||
'使用 \\n 表示換行,\\t 表示 Tab,\\\\ 表示反斜線。',
|
||||
'settings.terminal.behavior.clearWipesScrollback': '`clear` 同時清空捲動緩衝',
|
||||
'settings.terminal.behavior.clearWipesScrollback.desc':
|
||||
'`clear` 指令同時清空捲動緩衝(POSIX 預設行為)。關閉則保留歷史。',
|
||||
'settings.terminal.behavior.preserveSelectionOnInput': '輸入時保留選區',
|
||||
'settings.terminal.behavior.preserveSelectionOnInput.desc':
|
||||
'鍵盤輸入時不清除滑鼠選取的文字,方便選取路徑後輸入 `sz ` 之類指令再貼上。',
|
||||
'settings.terminal.behavior.forcePromptNewLine': '提示字元另起一行',
|
||||
'settings.terminal.behavior.forcePromptNewLine.desc':
|
||||
'當指令輸出的最後一行未以換行符結束時,將識別到的 shell 提示字元移動到下一行顯示。',
|
||||
'settings.terminal.behavior.dynamicTabTitle': '動態標籤頁標題',
|
||||
'settings.terminal.behavior.dynamicTabTitle.desc':
|
||||
'選擇標籤頁什麼時候跟隨 Shell 報告的視窗標題。',
|
||||
'settings.terminal.behavior.dynamicTabTitle.off': '關閉',
|
||||
'settings.terminal.behavior.dynamicTabTitle.agent': '僅 Agent',
|
||||
'settings.terminal.behavior.dynamicTabTitle.all': '全部工作階段',
|
||||
'settings.terminal.behavior.osc52Clipboard': 'OSC-52 剪貼簿',
|
||||
'settings.terminal.behavior.osc52Clipboard.desc':
|
||||
'允許遠端程式(tmux、vim 等)透過 OSC-52 轉義序列存取本機剪貼簿。',
|
||||
'settings.terminal.behavior.osc52Clipboard.off': '關閉',
|
||||
'settings.terminal.behavior.osc52Clipboard.writeOnly': '僅寫入',
|
||||
'settings.terminal.behavior.osc52Clipboard.readWrite': '讀寫',
|
||||
'settings.terminal.behavior.osc52Clipboard.prompt': '寫入 + 讀取時詢問',
|
||||
'terminal.osc52.readPrompt.title': '剪貼簿讀取請求',
|
||||
'terminal.osc52.readPrompt.desc': '遠端程式正在請求讀取你的剪貼簿,是否允許?',
|
||||
'terminal.osc52.readPrompt.allow': '允許',
|
||||
'terminal.osc52.readPrompt.deny': '拒絕',
|
||||
'settings.terminal.behavior.scrollOnInput': '輸入時自動捲動',
|
||||
'settings.terminal.behavior.scrollOnInput.desc': '輸入時將終端捲動到底部',
|
||||
'settings.terminal.behavior.scrollOnOutput': '輸出時自動捲動',
|
||||
'settings.terminal.behavior.scrollOnOutput.desc': '有新輸出時將終端捲動到底部',
|
||||
'settings.terminal.behavior.scrollOnKeyPress': '按鍵時自動捲動',
|
||||
'settings.terminal.behavior.scrollOnKeyPress.desc': '按鍵(例如 Enter)時將終端捲動到底部',
|
||||
'settings.terminal.behavior.scrollOnPaste': '貼上時自動捲動',
|
||||
'settings.terminal.behavior.scrollOnPaste.desc': '貼上文字時將終端捲動到底部',
|
||||
'settings.terminal.behavior.smoothScrolling': '平滑捲動',
|
||||
'settings.terminal.behavior.smoothScrolling.desc': '捲動終端可視區域時使用平滑動畫',
|
||||
'settings.terminal.behavior.linkModifier': '連結修飾鍵',
|
||||
'settings.terminal.behavior.linkModifier.desc': '按住此鍵再點選終端中的連結',
|
||||
'settings.terminal.behavior.linkModifier.none': '無(直接點選)',
|
||||
'settings.terminal.behavior.linkModifier.ctrl': 'Ctrl',
|
||||
'settings.terminal.behavior.linkModifier.alt': 'Alt / Option',
|
||||
'settings.terminal.behavior.linkModifier.meta': 'Cmd / Win',
|
||||
'settings.terminal.scrollback.desc': '限制終端行數。設為 0 表示不限制。',
|
||||
'settings.terminal.scrollback.rows': '行數 *',
|
||||
'settings.terminal.section.startupCommand': '啟動指令',
|
||||
'settings.terminal.startupCommandDelay.label': '啟動指令延遲(毫秒)',
|
||||
'settings.terminal.startupCommandDelay.desc': '連線建立後等待多久再發送啟動指令;啟動指令為多行時,行與行之間也使用該間隔。慢連線可調大。',
|
||||
'settings.terminal.keywordHighlight.title': '關鍵字標示',
|
||||
'settings.terminal.keywordHighlight.resetColors': '重置為預設顏色',
|
||||
'settings.terminal.keywordHighlight.resetDefaults': '把內建規則恢復為預設',
|
||||
'settings.terminal.keywordHighlight.resetBuiltIn': '恢復內建標籤與正規表示式',
|
||||
'settings.terminal.keywordHighlight.addCustom': '新增自訂規則',
|
||||
'settings.terminal.keywordHighlight.editCustom': '編輯規則',
|
||||
'settings.terminal.keywordHighlight.editBuiltIn': '編輯內建規則',
|
||||
'settings.terminal.keywordHighlight.labelField': '標籤與顏色',
|
||||
'settings.terminal.keywordHighlight.labelPlaceholder': '標籤(如 Down)',
|
||||
'settings.terminal.keywordHighlight.patternField': '正規表示式',
|
||||
'settings.terminal.keywordHighlight.patternPlaceholder': '每行一個正規表示式(如 \\bdown\\b)',
|
||||
'settings.terminal.keywordHighlight.patternHint': '每行一個正規表示式。比對時忽略大小寫,全域比對。',
|
||||
'settings.terminal.keywordHighlight.invalidPattern': '無效的正規表示式',
|
||||
'settings.terminal.keywordHighlight.preview': '預覽',
|
||||
'settings.terminal.section.localShell': '本機 Shell',
|
||||
'settings.terminal.localShell.shell': 'Shell 執行檔',
|
||||
'settings.terminal.localShell.shell.desc': 'Shell 執行檔的路徑(例如 /bin/zsh、pwsh.exe)。留空使用系統預設。',
|
||||
'settings.terminal.localShell.shell.placeholder': '系統預設',
|
||||
'settings.terminal.localShell.shell.detected': '偵測到',
|
||||
'settings.terminal.localShell.shell.notFound': '未找到 Shell 執行檔',
|
||||
'settings.terminal.localShell.shell.isDirectory': '路徑是目錄,不是執行檔',
|
||||
'settings.terminal.localShell.shell.default': '系統預設',
|
||||
'settings.terminal.localShell.shell.custom': '自訂...',
|
||||
'settings.terminal.localShell.shell.customPath': 'Shell 執行檔路徑',
|
||||
'settings.terminal.localShell.shell.customArgs': '啟動引數',
|
||||
'settings.terminal.localShell.shell.customArgs.placeholder': '例如 --login -i',
|
||||
'settings.terminal.localShell.shell.customArgs.desc': '傳給 Shell 的啟動引數。部分 Shell 必須指定才能正常工作,例如 msys2 bash 需要 --login -i 才能載入環境變數。',
|
||||
'settings.terminal.localShell.shell.commonPaths': '常用路徑',
|
||||
'settings.terminal.localShell.shell.pathValid': '路徑有效',
|
||||
'settings.terminal.localShell.startDir': '起始目錄',
|
||||
'settings.terminal.localShell.startDir.desc': '開啟本機終端時的起始目錄。留空使用使用者主目錄。',
|
||||
'settings.terminal.localShell.startDir.placeholder': '使用者主目錄',
|
||||
'settings.terminal.localShell.startDir.notFound': '目錄不存在',
|
||||
'settings.terminal.localShell.startDir.isFile': '路徑是檔案,不是目錄',
|
||||
'settings.terminal.section.connection': '連線',
|
||||
'settings.terminal.connection.sshAutoReconnectEnabled': 'SSH 斷線後自動重連',
|
||||
'settings.terminal.connection.sshAutoReconnectEnabled.desc': '開啟後,已連上的 SSH 工作階段意外斷開時會每 5 秒嘗試重新連線,直到關閉標籤頁或連線恢復。',
|
||||
'settings.terminal.connection.keepaliveInterval': '工作階段保持間隔',
|
||||
'settings.terminal.connection.keepaliveInterval.desc': '向伺服器傳送 SSH 保活資料包的頻率(秒)。設為 0 表示全域停用——單個主機可在自己的設定裡覆蓋此值。',
|
||||
'settings.terminal.connection.keepaliveCountMax': '最大無響應保活次數',
|
||||
'settings.terminal.connection.keepaliveCountMax.desc': '判定連線死亡前允許的無響應保活次數。值越大對短暫網路抖動和響應慢的 SSH 服務越寬容。',
|
||||
'settings.terminal.connection.x11Display': 'X11 顯示地址',
|
||||
'settings.terminal.connection.x11Display.desc': '可選的本機 X11 顯示地址。留空則使用系統預設值。',
|
||||
'settings.terminal.connection.x11Display.placeholder': '自動(:0 或 DISPLAY)',
|
||||
'settings.terminal.section.serverStats': '伺服器狀態(Linux)',
|
||||
'settings.terminal.serverStats.show': '顯示伺服器狀態',
|
||||
'settings.terminal.serverStats.show.desc': '在終端狀態列顯示 CPU、記憶體和磁碟使用情況(僅限 Linux 伺服器)。',
|
||||
'settings.terminal.serverStats.refreshInterval': '重新整理間隔',
|
||||
'settings.terminal.serverStats.refreshInterval.desc': '伺服器狀態重新整理的頻率。',
|
||||
'settings.terminal.serverStats.seconds': '秒',
|
||||
'settings.terminal.section.systemManager': '系統管理',
|
||||
'settings.terminal.systemManager.processRefreshInterval': '程序列表重新整理間隔',
|
||||
'settings.terminal.systemManager.processRefreshInterval.desc': '系統管理側欄中程序列表的重新整理頻率。',
|
||||
'settings.terminal.systemManager.tmuxRefreshInterval': 'tmux 工作階段重新整理間隔',
|
||||
'settings.terminal.systemManager.tmuxRefreshInterval.desc': 'tmux 工作階段列表的重新整理頻率。',
|
||||
'settings.terminal.systemManager.dockerListRefreshInterval': 'Docker 容器列表重新整理間隔',
|
||||
'settings.terminal.systemManager.dockerListRefreshInterval.desc': 'Docker 容器列表的重新整理頻率。',
|
||||
'settings.terminal.systemManager.dockerStatsRefreshInterval': 'Docker 效能資料重新整理間隔',
|
||||
'settings.terminal.systemManager.dockerStatsRefreshInterval.desc': 'Docker 容器 CPU/記憶體/網路指標的重新整理頻率。',
|
||||
|
||||
// Settings > Terminal > Rendering
|
||||
'settings.terminal.section.rendering': '渲染',
|
||||
'settings.terminal.rendering.renderer': '渲染器',
|
||||
'settings.terminal.rendering.renderer.desc': '選擇終端渲染技術。自動模式會在低記憶體裝置上使用 DOM 渲染。更改將在新終端工作階段中生效。',
|
||||
'settings.terminal.rendering.auto': '自動',
|
||||
'settings.terminal.rendering.hibernateHiddenTabs': '隱藏標籤頁休眠',
|
||||
'settings.terminal.rendering.hibernateHiddenTabs.desc': '標籤頁離開視野後釋放終端渲染器以節省記憶體,SSH 連線保持不斷。檔案傳輸期間不會休眠。',
|
||||
'settings.terminal.rendering.hibernateHiddenTabsDelay': '休眠延遲',
|
||||
'settings.terminal.rendering.hibernateHiddenTabsDelay.desc': '標籤頁離開視野多久後釋放渲染器(5–600 秒)。',
|
||||
'settings.terminal.rendering.hibernateSkipAltScreen': '全屏 TUI 跳過休眠',
|
||||
'settings.terminal.rendering.hibernateSkipAltScreen.desc': 'vim、htop 或 Agent TUI 佔用 alternate screen 時保持終端渲染器,避免慢恢復。',
|
||||
'settings.terminal.rendering.hibernateKeepRendererCount': '保留隱藏渲染器數量',
|
||||
'settings.terminal.rendering.hibernateKeepRendererCount.desc': '多少個不可見標籤頁僅暫停 WebGL、不銷燬 xterm,超出後才完整休眠。',
|
||||
'settings.terminal.rendering.hibernateReplayChunkBytes': '喚醒分片大小',
|
||||
'settings.terminal.rendering.hibernateReplayChunkBytes.desc': '恢復休眠終端時每幀回放的 ANSI 位元組數(4–64 KB)。',
|
||||
'settings.terminal.rendering.hibernatePreferWasmSerialize': '優先 WASM 序列化',
|
||||
'settings.terminal.rendering.hibernatePreferWasmSerialize.desc': '在可用時使用 WASM 終端序列化,否則回退到 JS SerializeAddon。',
|
||||
|
||||
// Settings > Terminal > Workspace Focus Indicator
|
||||
'settings.terminal.section.workspaceFocus': '工作區焦點提示',
|
||||
'settings.terminal.workspaceFocus.style': '焦點提示樣式',
|
||||
'settings.terminal.workspaceFocus.style.desc': '在分割檢視中如何標識目前聚焦的窗格。',
|
||||
'settings.terminal.workspaceFocus.dim': '淡化未聚焦窗格',
|
||||
'settings.terminal.workspaceFocus.border': '為聚焦窗格顯示邊框',
|
||||
|
||||
// Settings > Terminal > Autocomplete
|
||||
'settings.terminal.section.autocomplete': '自動補全',
|
||||
'settings.terminal.autocomplete.enabled': '啟用自動補全',
|
||||
'settings.terminal.autocomplete.enabled.desc': '輸入時根據歷史指令和指令規範顯示補全建議。',
|
||||
'settings.terminal.autocomplete.ghostText': '行內建議',
|
||||
'settings.terminal.autocomplete.ghostText.desc': '在游標後顯示灰色的建議文字(類似 fish shell)。',
|
||||
'settings.terminal.autocomplete.popupMenu': '彈出選單',
|
||||
'settings.terminal.autocomplete.popupMenu.desc': '顯示包含多個建議的浮動列表。',
|
||||
|
||||
// Settings > Shortcuts
|
||||
'settings.shortcuts.section.scheme': '快捷鍵方案',
|
||||
'settings.shortcuts.scheme.label': '鍵盤快捷鍵',
|
||||
'settings.shortcuts.scheme.desc': '選擇快捷鍵使用的鍵盤佈局',
|
||||
'settings.shortcuts.scheme.disabled': '停用',
|
||||
'settings.shortcuts.scheme.mac': 'Mac (Cmd)',
|
||||
'settings.shortcuts.scheme.pc': 'PC (Ctrl)',
|
||||
'settings.shortcuts.disableTerminalFontZoom.label': '停用終端縮放',
|
||||
'settings.shortcuts.disableTerminalFontZoom.desc': '關閉終端文字縮放快捷操作,包括 Cmd/Ctrl 加滾輪。',
|
||||
'settings.shortcuts.shellOnlyTabNumberShortcuts.label': '標籤快捷鍵跳過固定標籤',
|
||||
'settings.shortcuts.shellOnlyTabNumberShortcuts.desc': '開啟後,Cmd/Ctrl+[1...9] 和 Ctrl+Tab 僅在終端、工作區、編輯器等可關閉標籤頁之間切換,不包括固定的 Vault 和 SFTP 標籤頁。',
|
||||
'settings.shortcuts.section.custom': '自訂快捷鍵',
|
||||
'settings.shortcuts.resetAll': '全部重置',
|
||||
'settings.shortcuts.recording': '請按鍵...',
|
||||
'settings.shortcuts.none': '無',
|
||||
'settings.shortcuts.setDisabled': '設為停用',
|
||||
'settings.shortcuts.category.tabs': '標籤頁',
|
||||
'settings.shortcuts.category.terminal': '終端',
|
||||
'settings.shortcuts.category.navigation': '導航',
|
||||
'settings.shortcuts.category.app': '應用程式',
|
||||
'settings.shortcuts.category.sftp': 'SFTP',
|
||||
'settings.shortcuts.binding.switch-tab-1-9': '切換到標籤頁 [1...9]',
|
||||
'settings.shortcuts.binding.next-tab': '下一個標籤頁',
|
||||
'settings.shortcuts.binding.prev-tab': '上一個標籤頁',
|
||||
'settings.shortcuts.binding.close-tab': '關閉標籤頁',
|
||||
'settings.shortcuts.binding.close-session': '關閉工作階段窗格',
|
||||
'settings.shortcuts.binding.new-tab': '新建本機標籤頁',
|
||||
'settings.shortcuts.binding.copy': '從終端複製',
|
||||
'settings.shortcuts.binding.paste': '貼上到終端',
|
||||
'settings.shortcuts.binding.paste-selection': '將選區貼上到終端',
|
||||
'settings.shortcuts.binding.select-all': '全選終端內容',
|
||||
'settings.shortcuts.binding.clear-buffer': '清空終端緩衝區',
|
||||
'settings.shortcuts.binding.search-terminal': '開啟終端搜尋',
|
||||
'settings.shortcuts.binding.increase-terminal-font-size': '增大終端字號',
|
||||
'settings.shortcuts.binding.decrease-terminal-font-size': '減小終端字號',
|
||||
'settings.shortcuts.binding.reset-terminal-font-size': '重置終端字號',
|
||||
'settings.shortcuts.binding.move-focus': '在分割間移動焦點',
|
||||
'settings.shortcuts.binding.split-horizontal': '水平分割',
|
||||
'settings.shortcuts.binding.split-vertical': '垂直分割',
|
||||
'settings.shortcuts.binding.toggle-pane-zoom': '切換窗格縮放',
|
||||
'settings.shortcuts.binding.open-hosts': '開啟主機列表',
|
||||
'settings.shortcuts.binding.open-local': '開啟本機終端',
|
||||
'settings.shortcuts.binding.open-sftp': '開啟 SFTP',
|
||||
'settings.shortcuts.binding.open-settings': '開啟設定',
|
||||
'settings.shortcuts.binding.port-forwarding': '開啟埠轉發',
|
||||
'settings.shortcuts.binding.command-palette': '開啟指令面板',
|
||||
'settings.shortcuts.binding.quick-switch': '快速切換',
|
||||
'settings.shortcuts.binding.new-workspace': '新建工作區',
|
||||
'settings.shortcuts.binding.snippets': '開啟程式碼片段',
|
||||
'settings.shortcuts.binding.broadcast': '切換廣播模式',
|
||||
'settings.shortcuts.binding.toggle-side-panel': '切換側邊欄',
|
||||
'settings.shortcuts.binding.sftp-copy': '複製檔案',
|
||||
'settings.shortcuts.binding.sftp-cut': '剪下檔案',
|
||||
'settings.shortcuts.binding.sftp-paste': '貼上檔案',
|
||||
'settings.shortcuts.binding.sftp-select-all': '全選檔案',
|
||||
'settings.shortcuts.binding.sftp-rename': '重新命名檔案',
|
||||
'settings.shortcuts.binding.sftp-delete': '刪除檔案',
|
||||
'settings.shortcuts.binding.sftp-refresh': '重新整理',
|
||||
'settings.shortcuts.binding.sftp-new-folder': '新建資料夾',
|
||||
'settings.shortcuts.binding.sftp-open': '開啟檔案 / 進入目錄',
|
||||
'settings.shortcuts.binding.sftp-go-parent': '轉到上級目錄',
|
||||
'settings.shortcuts.binding.sftp-navigate-to': '轉到選取的目錄',
|
||||
|
||||
// Host Details (sub-panels)
|
||||
'hostDetails.proxyPanel.title': '透過 HTTP/SOCKS5/指令代理',
|
||||
'hostDetails.proxyPanel.hostPlaceholder': '代理主機',
|
||||
'hostDetails.proxyPanel.command': 'ProxyCommand',
|
||||
'hostDetails.proxyPanel.commandPlaceholder': 'cloudflared access ssh --hostname %h',
|
||||
'hostDetails.proxyPanel.commandHelp': '使用 %h 表示目標主機,%p 表示目標埠,%% 表示字面百分號。',
|
||||
'hostDetails.proxyPanel.credentials': '憑證',
|
||||
'hostDetails.proxyPanel.usernamePlaceholder': '使用者名稱',
|
||||
'hostDetails.proxyPanel.passwordPlaceholder': '密碼',
|
||||
'hostDetails.proxyPanel.identities': '身份',
|
||||
'hostDetails.proxyPanel.keychainIdentity': '鑰匙串身份',
|
||||
'hostDetails.proxyPanel.manualCredentials': '手動憑證',
|
||||
'hostDetails.proxyPanel.remove': '移除代理',
|
||||
'hostDetails.proxyPanel.savedProxy': '已儲存代理',
|
||||
'hostDetails.proxyPanel.selectSaved': '選擇已儲存代理',
|
||||
'hostDetails.proxyPanel.customProxy': '自訂代理',
|
||||
'hostDetails.proxyPanel.missing': '缺失',
|
||||
'hostDetails.proxyPanel.missingSaved': '儲存的代理不存在',
|
||||
'hostDetails.proxyPanel.missingIdentity': '鑰匙串身份不存在',
|
||||
'hostDetails.proxyPanel.incompleteIdentity': '代理身份需要使用者名稱和密碼',
|
||||
'hostDetails.proxyPanel.unreadableIdentity': '此裝置無法讀取代理身份密碼',
|
||||
'hostDetails.proxyPanel.error.required': '代理主機和埠,或 ProxyCommand 不能為空。',
|
||||
'hostDetails.envVars.title': '環境變數',
|
||||
'hostDetails.envVars.desc': '為 {host} 設定環境變數。',
|
||||
'hostDetails.envVars.note': '部分 SSH 伺服器預設只允許以 LC_ 和 LANG_ 為字首的變數。',
|
||||
'hostDetails.envVars.variable': '變數',
|
||||
'hostDetails.envVars.value': '值',
|
||||
'hostDetails.envVars.newVariable': '新變數',
|
||||
'hostDetails.envVars.variableName': '變數名',
|
||||
'hostDetails.chain.title': '編輯鏈路',
|
||||
'hostDetails.chain.desc': '新增另一台主機將建立到 {host} 的連線。',
|
||||
'hostDetails.chain.addHost': '新增主機',
|
||||
'hostDetails.chain.target': '目標',
|
||||
'hostDetails.chain.availableHosts': '可用主機',
|
||||
'hostDetails.chain.clear': '清空',
|
||||
'hostDetails.group.title': '新建分組',
|
||||
'hostDetails.group.general': '常規',
|
||||
'hostDetails.group.namePlaceholder': '分組名稱',
|
||||
'hostDetails.group.parentPlaceholder': '父分組',
|
||||
'hostDetails.group.cloudSync': '雲同步',
|
||||
'hostDetails.group.addProtocol': '新增協定',
|
||||
|
||||
// Keychain
|
||||
'keychain.filter.key': '金鑰',
|
||||
'keychain.filter.certificate': '憑證',
|
||||
'keychain.action.generateKey': '生成金鑰',
|
||||
'keychain.action.importKey': '匯入金鑰',
|
||||
'keychain.action.newIdentity': '新建身份',
|
||||
'keychain.action.importCertificate': '匯入憑證',
|
||||
'keychain.view.grid': '網格',
|
||||
'keychain.view.list': '列表',
|
||||
'keychain.section.keys': '金鑰',
|
||||
'keychain.section.identities': '身份',
|
||||
'keychain.count.items': '{count} 項',
|
||||
'keychain.empty.title': '設定金鑰',
|
||||
'keychain.empty.desc': '匯入或生成 SSH 金鑰用於安全認證。',
|
||||
'keychain.panel.generateKey': '生成金鑰',
|
||||
'keychain.panel.newKey': '新建金鑰',
|
||||
'keychain.panel.keyDetails': '金鑰詳情',
|
||||
'keychain.panel.editKey': '編輯金鑰',
|
||||
'keychain.panel.editIdentity': '編輯身份',
|
||||
'keychain.panel.newIdentity': '新建身份',
|
||||
'keychain.panel.keyExport': '金鑰匯出',
|
||||
'keychain.validation.labelRequired': '請填寫金鑰的 Label',
|
||||
'keychain.validation.labelAndPrivateKeyRequired': 'Label 和私鑰為必填項',
|
||||
'keychain.validation.labelAndUsernameRequired': 'Label 和使用者名稱為必填項',
|
||||
'keychain.error.generationUnavailable': '無法生成金鑰:請確保應用程式執行在 Electron 環境',
|
||||
'keychain.error.generateKeyPairFailed': '生成金鑰對失敗',
|
||||
'keychain.error.generateKeyFailed': '生成金鑰失敗',
|
||||
'keychain.error.keyGenerationTitle': '金鑰生成',
|
||||
'keychain.export.exportTo': '匯出到 *',
|
||||
'keychain.export.selectHost': '選擇主機',
|
||||
'keychain.export.location': '位置 ~ $1 *',
|
||||
'keychain.export.filename': '檔名 ~ $2 *',
|
||||
'keychain.export.note': '金鑰匯出目前僅支援 {unix} 系統。請在 {advanced} 部分自訂匯出指令碼。',
|
||||
'keychain.export.script': '指令碼 *',
|
||||
'keychain.export.scriptPlaceholder': '匯出指令碼...',
|
||||
'keychain.export.missingCredentials': '主機未儲存密碼或金鑰。請先為該主機新增密碼憑證。',
|
||||
'keychain.export.successTitle': '匯出成功',
|
||||
'keychain.export.successMessage': '已匯出公鑰並繫結到 {host}',
|
||||
'keychain.export.failedTitle': '匯出失敗',
|
||||
'keychain.export.failedMessage': '匯出金鑰失敗:{error}',
|
||||
'keychain.export.failedPrefix': '匯出失敗:{error}',
|
||||
'keychain.export.exitCode': '指令結束代碼 {code}',
|
||||
'keychain.export.exporting': '匯出中...',
|
||||
'keychain.export.exportAndAttach': '匯出並繫結',
|
||||
'keychain.export.title': '金鑰匯出',
|
||||
'keychain.export.exportToRequired': '匯出到 *',
|
||||
'keychain.export.selectHostPlaceholder': '選擇主機...',
|
||||
'keychain.export.locationLabel': '位置 ~ $1 *',
|
||||
'keychain.export.filenameLabel': '檔名 ~ $2 *',
|
||||
'keychain.export.advanced': '高階',
|
||||
'keychain.export.note.supportsOnly': '金鑰匯出目前僅支援',
|
||||
'keychain.export.note.systems': '系統。',
|
||||
'keychain.export.note.use': '請使用',
|
||||
'keychain.export.note.customize': '部分自訂匯出指令碼。',
|
||||
'keychain.export.scriptRequired': '指令碼 *',
|
||||
'keychain.export.exportToHost': '匯出到主機',
|
||||
'keychain.export.failedGeneric': '匯出失敗:{message}',
|
||||
'keychain.field.label': 'Label',
|
||||
'keychain.field.labelRequired': 'Label *',
|
||||
'keychain.field.labelPlaceholder': '金鑰 Label',
|
||||
'keychain.field.privateKeyRequired': '私鑰 *',
|
||||
'keychain.field.publicKey': '公鑰',
|
||||
'keychain.field.certificatePlaceholder': '憑證內容(可選)',
|
||||
'keychain.generate.keyType': '金鑰型別',
|
||||
'keychain.generate.keySize': '金鑰長度',
|
||||
'keychain.generate.labelPlaceholder': '金鑰 Label',
|
||||
'keychain.generate.passphrasePlaceholder': 'Passphrase(可選)',
|
||||
'keychain.generate.savePassphrase': '儲存 Passphrase',
|
||||
'keychain.generate.generate': '生成',
|
||||
'keychain.generate.generateSave': '生成並儲存',
|
||||
'keychain.import.dropHint': '將金鑰檔案拖到這裡',
|
||||
'keychain.import.importFromFile': '從檔案匯入',
|
||||
'keychain.import.saveKey': '儲存金鑰',
|
||||
'keychain.import.importedKeyLabel': '已匯入金鑰',
|
||||
'keychain.identity.usernameRequired': '使用者名稱 *',
|
||||
'keychain.identity.method.passwordOnly': '密碼',
|
||||
'keychain.identity.summary.password': '認證密碼',
|
||||
'keychain.identity.summary.key': '認證金鑰',
|
||||
'keychain.identity.summary.certificate': '認證憑證',
|
||||
'keychain.identity.summary.passwordAndKey': '認證密碼與金鑰',
|
||||
'keychain.identity.summary.passwordAndCertificate': '認證密碼與憑證',
|
||||
'keychain.identity.summary.none': '無憑證',
|
||||
'keychain.identity.selectCredential': '選擇{kind}',
|
||||
'keychain.identity.save': '儲存',
|
||||
'keychain.identity.update': '更新',
|
||||
'keychain.keyDialog.newTitle': '新建金鑰',
|
||||
'keychain.keyDialog.newDesc': '新增新的 SSH 金鑰',
|
||||
'keychain.keyDialog.editTitle': '編輯金鑰',
|
||||
'keychain.keyDialog.editDesc': '更新此 SSH 金鑰',
|
||||
'keychain.keyDialog.updateKey': '更新金鑰',
|
||||
|
||||
// Tabs
|
||||
'tabs.closeSessionAria': '關閉工作階段',
|
||||
'tabs.closeLogViewAria': '關閉日誌檢視',
|
||||
'tabs.logPrefix': '日誌:',
|
||||
'tabs.logLocal': '本機',
|
||||
'tabs.copyTab': '複製標籤頁',
|
||||
'tabs.copyTabToNewWindow': '複製標籤頁到新視窗',
|
||||
'tabs.copyTabToNewWindowFailed': '無法在新視窗開啟標籤頁',
|
||||
'tabs.closeOthers': '關閉其他標籤',
|
||||
'tabs.closeToRight': '關閉右側標籤',
|
||||
'tabs.closeAll': '關閉所有標籤',
|
||||
'keychain.edit.labelRequired': 'Label *',
|
||||
'keychain.edit.keyLabelPlaceholder': '金鑰 Label',
|
||||
'keychain.edit.privateKeyRequired': '私鑰 *',
|
||||
'keychain.edit.publicKey': '公鑰',
|
||||
'keychain.edit.certificate': '憑證',
|
||||
'keychain.edit.certificatePlaceholder': '憑證內容(可選)',
|
||||
'keychain.edit.filePath': '檔案路徑',
|
||||
'keychain.edit.keyExport': '金鑰匯出',
|
||||
'keychain.edit.exportToHost': '匯出到主機',
|
||||
|
||||
// Snippets
|
||||
'snippets.searchPlaceholder': '搜尋程式碼片段...',
|
||||
'snippets.action.newSnippet': '新建程式碼片段',
|
||||
'snippets.action.newPackage': '新建程式碼包',
|
||||
'snippets.action.import': '匯入',
|
||||
'snippets.action.selectSnippets': '選擇程式碼片段',
|
||||
'snippets.panel.newTitle': '新建程式碼片段',
|
||||
'snippets.panel.editTitle': '編輯程式碼片段',
|
||||
'snippets.field.description': '描述',
|
||||
'snippets.field.descriptionPlaceholder': '例如:check network load',
|
||||
'snippets.field.package': '新增程式碼包',
|
||||
'snippets.field.packagePlaceholder': '選擇或建立程式碼包',
|
||||
'snippets.field.createPackage': '建立程式碼包',
|
||||
'snippets.field.scriptRequired': '指令碼 *',
|
||||
'snippets.scriptEditor.expand': '彈出視窗編輯',
|
||||
'snippets.scriptEditor.resize': '調整編輯器高度',
|
||||
'snippets.scriptEditor.modalTitle': '編輯指令碼',
|
||||
'snippets.targets.title': '目標主機',
|
||||
'snippets.targets.add': '新增目標主機',
|
||||
'snippets.history.title': 'Shell 歷史',
|
||||
'snippets.history.subtitle': '{count} 條指令',
|
||||
'snippets.history.emptyTitle': '暫無 Shell 歷史',
|
||||
'snippets.history.emptyDesc': '你執行過的指令會顯示在這裡',
|
||||
'snippets.history.loadMore': '載入更多',
|
||||
'snippets.history.separator': '•',
|
||||
'snippets.history.labelPlaceholder': '為此程式碼片段設定一個 Label',
|
||||
'snippets.history.saveAsSnippet': '儲存為程式碼片段',
|
||||
'snippets.history.time.justNow': '剛剛',
|
||||
'snippets.history.time.minutesAgo': '{count} 分鐘前',
|
||||
'snippets.history.time.hoursAgo': '{count} 小時前',
|
||||
'snippets.history.time.daysAgo': '{count} 天前',
|
||||
'snippets.breadcrumb.allPackages': '全部程式碼包',
|
||||
'snippets.breadcrumb.separator': '›',
|
||||
'snippets.empty.title': '建立程式碼片段',
|
||||
'snippets.empty.desc': '將常用指令儲存為程式碼片段,一鍵重複使用。',
|
||||
'snippets.panel.newAutomationTitle': '新建自動化指令碼',
|
||||
'snippets.panel.editAutomationTitle': '編輯自動化指令碼',
|
||||
'snippets.panel.resizeWidth': '調整面板寬度',
|
||||
'snippets.targets.allHosts': '對所有主機生效',
|
||||
'snippets.targets.allHostsShort': '所有主機',
|
||||
'snippets.targets.allHostsActive': '將作用於所有可連線的主機。',
|
||||
'snippets.kind.codeSnippet': '程式碼片段',
|
||||
'snippets.kind.automationScript': '自動化指令碼',
|
||||
'snippets.search.noResults.title': '無相符結果',
|
||||
'snippets.search.noResults.desc': '沒有程式碼片段或程式碼包與"{query}"相符。換一個關鍵字,或清除搜尋進行瀏覽。',
|
||||
'snippets.section.packages': '程式碼包',
|
||||
'snippets.section.snippets': '程式碼片段',
|
||||
'snippets.package.count': '{count} 個程式碼片段',
|
||||
'snippets.commandFallback': '指令',
|
||||
'snippets.view.grid': '網格',
|
||||
'snippets.view.list': '列表',
|
||||
'snippets.selection.selected': '已選擇 {count} 個',
|
||||
'snippets.selection.selectVisible': '選擇目前顯示',
|
||||
'snippets.selection.deselectAll': '取消選擇',
|
||||
'snippets.selection.exportSelected': '匯出選取({count})',
|
||||
'snippets.export.snippet': '匯出程式碼片段',
|
||||
'snippets.export.package': '匯出程式碼包',
|
||||
'snippets.export.toast.empty': '沒有可匯出的程式碼片段。',
|
||||
'snippets.export.toast.successTitle': '匯出已準備好',
|
||||
'snippets.export.toast.success': '已匯出 {count} 個程式碼片段。',
|
||||
'snippets.import.toast.empty': '沒有找到可匯入的程式碼片段。',
|
||||
'snippets.import.toast.failedTitle': '匯入失敗',
|
||||
'snippets.import.toast.invalidDesc': '這不是有效的 Netcatty 程式碼片段檔案。',
|
||||
'snippets.import.toast.successTitle': '匯入完成',
|
||||
'snippets.import.toast.summary': '已匯入 {imported} 個,覆蓋 {overwritten} 個,跳過 {skipped} 個。',
|
||||
'snippets.import.modal.title': '匯入程式碼片段',
|
||||
'snippets.import.modal.desc': '選擇一個或多個 Netcatty 程式碼片段 JSON 檔案。匯入前會先解析並展示結果,不會直接寫入。',
|
||||
'snippets.import.modal.exampleTitle': '範例 JSON',
|
||||
'snippets.import.modal.noFile': '還沒有選擇檔案。可以參考下面的格式,也可以直接使用程式碼片段 JSON 陣列,然後選擇一個或多個檔案。',
|
||||
'snippets.import.modal.chooseFile': '選擇檔案',
|
||||
'snippets.import.modal.downloadExamples': '下載範例檔案',
|
||||
'snippets.import.modal.multipleFiles': '已選擇 {count} 個檔案',
|
||||
'snippets.import.modal.parsedSummary': '{files} 個檔案,{total} 個程式碼片段,{packages} 個程式碼包,{conflicts} 個重複指令。主機繫結會被忽略。',
|
||||
'snippets.import.modal.confirm': '確認匯入',
|
||||
'snippets.import.conflict.title': '匯入程式碼片段?',
|
||||
'snippets.import.conflict.desc': '“{file}”包含 {total} 個程式碼片段,其中 {conflicts} 個指令已存在。',
|
||||
'snippets.import.conflict.hostBindingsNote': '程式碼片段的主機繫結不會被匯入或匯出。',
|
||||
'snippets.import.conflict.skip': '跳過重複項',
|
||||
'snippets.import.conflict.overwrite': '覆蓋重複項',
|
||||
'snippets.packageDialog.title': '新建程式碼包',
|
||||
'snippets.packageDialog.parent': '父級:{parent}',
|
||||
'snippets.packageDialog.root': '根目錄',
|
||||
'snippets.packageDialog.placeholder': '例如:ops/maintenance',
|
||||
'snippets.packageDialog.hint': '使用 "/" 建立巢狀程式碼包。',
|
||||
|
||||
// Snippets Rename Dialog
|
||||
'snippets.renameDialog.title': '重新命名程式碼包',
|
||||
'snippets.renameDialog.currentPath': '目前路徑:{path}',
|
||||
'snippets.renameDialog.placeholder': '輸入新名稱',
|
||||
'snippets.renameDialog.error.empty': '程式碼包名稱不能為空',
|
||||
'snippets.renameDialog.error.duplicate': '已存在同名的程式碼包',
|
||||
'snippets.renameDialog.error.invalidChars': '程式碼包名稱只能包含字母、數字、連字元和下劃線',
|
||||
|
||||
'snippets.field.noAutoRun': '僅貼上(不自動執行)',
|
||||
'snippets.field.multiLineRunMode': '多行執行方式',
|
||||
'snippets.field.multiLineRunMode.paste': '一次性傳送',
|
||||
'snippets.field.multiLineRunMode.lineDelay': '逐行傳送',
|
||||
'snippets.field.multiLineRunModeHint': '登入提示、網路設備巨集等情境可用逐行傳送。',
|
||||
// Snippet Shortkey
|
||||
'snippets.field.shortkey': '快捷鍵',
|
||||
'snippets.shortkey.placeholder': '點選設定快捷鍵',
|
||||
'snippets.shortkey.recording': '請按下快捷鍵組合...',
|
||||
'snippets.shortkey.hint': '在終端中按下此快捷鍵可快速傳送指令。',
|
||||
'snippets.shortkey.clear': '清除快捷鍵',
|
||||
'snippets.shortkey.error.systemConflict': '此快捷鍵與系統快捷鍵衝突',
|
||||
'snippets.shortkey.error.snippetConflict': '此快捷鍵已被程式碼片段使用:{name}',
|
||||
|
||||
'snippets.variables.dialogTitle': '填寫變數',
|
||||
'snippets.variables.dialogDesc': '執行「{label}」前請填寫以下變數。',
|
||||
'snippets.variables.hint': '變數值將原樣插入指令碼(不會進行 shell 轉義)。',
|
||||
'snippets.variables.preview': '預覽',
|
||||
'snippets.variables.placeholder': '請輸入',
|
||||
'snippets.variables.placeholderDefault': '預設:{value}',
|
||||
'snippets.variables.required': '請填寫此變數',
|
||||
'snippets.variables.run': '執行',
|
||||
'snippets.field.variablesHelp': '在指令碼中使用 {{名稱}} 或 {{名稱:預設值}} 定義變數。',
|
||||
'snippets.field.variablesDetected': '變數',
|
||||
'snippets.field.variableDefault': '預設 {value}',
|
||||
|
||||
// Serial Port
|
||||
'serial.button': '序列埠',
|
||||
'serial.modal.title': '連線序列埠',
|
||||
'serial.modal.desc': '設定序列埠連線引數',
|
||||
'serial.field.port': '序列埠',
|
||||
'serial.field.selectPort': '選擇序列埠...',
|
||||
'serial.field.baudRate': '波特率',
|
||||
'serial.field.dataBits': '資料位',
|
||||
'serial.field.stopBits': '停止位',
|
||||
'serial.field.stopBits15Warning': '1.5 停止位在 Windows 下可能不被所有裝置支援',
|
||||
'serial.field.parity': '同位位元',
|
||||
'serial.field.flowControl': '流控制',
|
||||
'serial.noPorts': '未偵測到序列埠裝置。請連線裝置後重新整理。',
|
||||
'serial.field.customPort': '自訂序列埠路徑',
|
||||
'serial.field.customPortPlaceholder': '例如 /dev/ttys001 或 COM1',
|
||||
'serial.type.hardware': '硬體',
|
||||
'serial.type.pseudo': '虛擬終端',
|
||||
'serial.type.custom': '自訂',
|
||||
'serial.parity.none': '無',
|
||||
'serial.parity.even': '偶同位',
|
||||
'serial.parity.odd': '奇同位',
|
||||
'serial.parity.mark': 'Mark',
|
||||
'serial.parity.space': 'Space',
|
||||
'serial.flowControl.none': '無',
|
||||
'serial.flowControl.xon/xoff': 'XON/XOFF (軟體)',
|
||||
'serial.flowControl.rts/cts': 'RTS/CTS (硬體)',
|
||||
'serial.field.localEcho': '強制本機回顯',
|
||||
'serial.field.localEchoDesc': '本機回顯輸入字元(用於沒有遠端回顯的裝置)',
|
||||
'serial.field.lineMode': '行模式',
|
||||
'serial.field.lineModeDesc': '緩衝輸入,按回車後傳送(而不是逐字元傳送)',
|
||||
'serial.field.charset': '字元編碼',
|
||||
'serial.connectionError': '連線序列埠失敗',
|
||||
'serial.field.baudRatePlaceholder': '選擇或輸入波特率...',
|
||||
'serial.field.baudRateEmpty': '輸入自訂波特率',
|
||||
'serial.field.customBaudRate': '使用自訂波特率',
|
||||
'serial.field.saveConfig': '儲存設定',
|
||||
'serial.field.saveConfigDesc': '將此序列埠設定儲存到主機列表以便快速存取',
|
||||
'serial.field.configLabel': '設定名稱',
|
||||
'serial.field.configLabelPlaceholder': '例如 Arduino Uno',
|
||||
'serial.connectAndSave': '連線並儲存',
|
||||
'serial.edit.title': '序列埠設定',
|
||||
|
||||
// Keyboard Interactive Authentication (2FA/MFA)
|
||||
'keyboard.interactive.title': '需要驗證',
|
||||
'keyboard.interactive.desc': '伺服器需要額外的身份驗證。',
|
||||
'keyboard.interactive.descWithHost': '伺服器 {hostname} 需要額外的身份驗證。',
|
||||
'keyboard.interactive.response': '響應',
|
||||
'keyboard.interactive.enterCode': '輸入驗證碼',
|
||||
'keyboard.interactive.enterResponse': '輸入響應',
|
||||
'keyboard.interactive.submit': '提交',
|
||||
'keyboard.interactive.verifying': '驗證中...',
|
||||
'keyboard.interactive.savePassword': '儲存密碼',
|
||||
|
||||
// Passphrase Modal for encrypted SSH keys
|
||||
'passphrase.title': 'SSH 金鑰密碼',
|
||||
'passphrase.desc': '請輸入 {keyName} 的密碼',
|
||||
'passphrase.descWithHost': '請輸入 {keyName} 的密碼以連線到 {hostname}',
|
||||
'passphrase.label': '密碼',
|
||||
'passphrase.keyPath': '金鑰',
|
||||
'passphrase.unlock': '解鎖',
|
||||
'passphrase.unlocking': '解鎖中...',
|
||||
'passphrase.skip': '跳過',
|
||||
'passphrase.remember': '記住此密碼',
|
||||
|
||||
// Text Editor
|
||||
'sftp.editor.wordWrap': '自動換行',
|
||||
'sftp.editor.maximize': '最大化',
|
||||
'sftp.editor.unsavedTitle': '未儲存的修改',
|
||||
'sftp.editor.unsavedMessage': '{fileName} 有未儲存的修改,是否儲存後關閉?',
|
||||
'sftp.editor.discardChanges': '不儲存',
|
||||
'sftp.editor.saveAndClose': '儲存並關閉',
|
||||
'sftp.editor.quitBlockedByDirty': '存在未儲存的編輯器,請先處理後再離開',
|
||||
|
||||
};
|
||||
@@ -0,0 +1,829 @@
|
||||
import type { Messages } from '../types';
|
||||
|
||||
export const zhTWVaultMessages: Messages = {
|
||||
// Notes
|
||||
'notes.search.placeholder': '搜尋筆記、主機、標籤...',
|
||||
'notes.action.newNote': '新建筆記',
|
||||
'notes.action.newGroup': '新建資料夾',
|
||||
'notes.action.importMarkdown': '匯入 Markdown',
|
||||
'notes.action.exportNote': '匯出筆記',
|
||||
'notes.action.exportGroup': '匯出資料夾',
|
||||
'notes.action.exportAll': '匯出全部筆記',
|
||||
'notes.import.toast.success': '已匯入 {count} 條筆記',
|
||||
'notes.import.toast.noNotes': '沒有匯入任何 Markdown 檔案。',
|
||||
'notes.import.toast.failed': '匯入 Markdown 檔案失敗。',
|
||||
'notes.import.toast.inProgress': '正在匯入 Markdown 檔案,請稍候。',
|
||||
'notes.import.toast.skipped': '已跳過 {count} 個不支援的檔案。',
|
||||
'notes.export.toast.success': '已匯出 {count} 條筆記',
|
||||
'notes.export.toast.empty': '沒有可匯出的筆記。',
|
||||
'notes.export.toast.failed': '匯出筆記失敗。',
|
||||
'notes.codeBlock.copied': '已複製',
|
||||
'notes.codeBlock.copyFailed': '複製程式碼塊失敗。',
|
||||
'notes.group.prompt': '資料夾路徑',
|
||||
'notes.group.all': '筆記本',
|
||||
'notes.group.ungrouped': '未分組',
|
||||
'notes.section.notebooks': '筆記本',
|
||||
'notes.section.notes': '筆記',
|
||||
'notes.mode.edit': '編輯',
|
||||
'notes.mode.split': '分割',
|
||||
'notes.mode.preview': '預覽',
|
||||
'notes.title.placeholder': '筆記標題',
|
||||
'notes.editor.placeholder': '在這裡寫 Markdown 筆記...',
|
||||
'notes.preview.empty': '暫無內容可預覽。',
|
||||
'notes.empty.title': '還沒有筆記',
|
||||
'notes.empty.desc': '可以記錄運維手冊、臨時備忘、伺服器交接資訊。',
|
||||
'notes.empty.group': '這個筆記本里還沒有筆記。',
|
||||
'notes.search.noResults': '沒有找到筆記。',
|
||||
|
||||
// Select Host panel
|
||||
'selectHost.title': '選擇主機',
|
||||
'selectHost.noHostsFound': '未找到主機',
|
||||
'selectHost.newHost': '新建主機',
|
||||
'selectHost.continue': '繼續',
|
||||
'selectHost.continueWithCount': '繼續(已選 {count} 個)',
|
||||
|
||||
// Quick Connect
|
||||
'quickConnect.knownHost.title': '確認要連線嗎?',
|
||||
'quickConnect.knownHost.authenticity': '無法驗證 {hostname} 的真實性。',
|
||||
'quickConnect.knownHost.fingerprintLabel': '{keyType} fingerprint (SHA256):',
|
||||
'quickConnect.knownHost.addQuestion': '是否將它加入 Known Hosts?',
|
||||
'quickConnect.knownHost.addAndContinue': '加入並繼續',
|
||||
'quickConnect.addKey': '新增 key',
|
||||
'quickConnect.warning.unparsedOptions': '部分 SSH 引數已被忽略: {options}',
|
||||
|
||||
// Protocol select dialog
|
||||
'protocolSelect.chooseProtocol': '選擇協定',
|
||||
'protocolSelect.port': '埠:',
|
||||
// Host Details
|
||||
'hostDetails.title.details': '主機詳情',
|
||||
'hostDetails.title.new': '新建主機',
|
||||
'hostDetails.saveAria': '儲存',
|
||||
'hostDetails.section.address': '地址',
|
||||
'hostDetails.hostname.placeholder': 'IP 或 主機名',
|
||||
'hostDetails.section.general': '通用',
|
||||
'hostDetails.section.sftp': 'SFTP 設定',
|
||||
'hostDetails.sftp.sudo': 'Sudo 提權模式',
|
||||
'hostDetails.sftp.sudo.desc': '使用儲存的密碼自動取得 Root 權限',
|
||||
'hostDetails.sftp.sudo.passwordWarning': 'Sudo 模式需要密碼。請在上方設定密碼,或確保伺服器允許免密 sudo。',
|
||||
'hostDetails.sftp.encoding': '檔名編碼',
|
||||
'hostDetails.sftp.encoding.desc': '選擇用於解碼和傳送 SFTP 檔名的編碼。',
|
||||
'hostDetails.label.placeholder': '名稱(例如:Production Server)',
|
||||
'hostDetails.notes.label': '備註',
|
||||
'hostDetails.notes.placeholder': '硬體設定、專案、客戶、地域、角色...',
|
||||
'hostDetails.notes.help': '支援 Markdown。請勿在此存放密碼或私鑰。',
|
||||
'hostDetails.notes.tab.edit': '編輯',
|
||||
'hostDetails.notes.tab.preview': '預覽',
|
||||
'hostDetails.notes.preview.empty': '暫無內容可預覽。',
|
||||
'hostDetails.notes.toggle.show': '顯示備註編輯框',
|
||||
'hostDetails.notes.toggle.hide': '隱藏備註編輯框',
|
||||
'hostDetails.group.placeholder': '父級 Group',
|
||||
'hostDetails.section.credentials': '憑證',
|
||||
'hostDetails.section.portCredentials': '埠與憑證',
|
||||
'hostDetails.section.appearance': '外觀',
|
||||
'hostDetails.distro.title': 'Linux 發行版',
|
||||
'hostDetails.distro.desc': '可在連線後自動探測,也可以手動覆蓋圖示所用的發行版。',
|
||||
'hostDetails.icon.title': '主機圖示',
|
||||
'hostDetails.icon.preview': '圖示預覽',
|
||||
'vault.panel.resizeWidth': '調整面板寬度',
|
||||
'hostDetails.icon.sectionTitle': '顏色與圖示',
|
||||
'hostDetails.icon.desc': '選擇此主機使用的圖示來源和顏色。',
|
||||
'hostDetails.icon.source': '來源',
|
||||
'hostDetails.icon.manualLabel': '手動指定',
|
||||
'hostDetails.icon.tab.brand': '品牌',
|
||||
'hostDetails.icon.tab.type': '型別',
|
||||
'hostDetails.icon.mode.auto': '自動',
|
||||
'hostDetails.icon.mode.custom': '自訂',
|
||||
'hostDetails.icon.reset': '重置主機圖示',
|
||||
'hostDetails.icon.colorLabel': '圖示顏色',
|
||||
'hostDetails.icon.colorDefault': '預設',
|
||||
'hostDetails.icon.colorAuto': '自動',
|
||||
'hostDetails.icon.colorManual': '手動',
|
||||
'hostDetails.icon.colorChoice': '顏色',
|
||||
'hostDetails.icon.autoColorLabel': '目前顏色',
|
||||
'hostDetails.icon.showLibrary': '展開圖示庫',
|
||||
'hostDetails.icon.hideLibrary': '收起圖示庫',
|
||||
'hostDetails.icon.autoUsesDistro': '使用 Linux 發行版圖示和所選顏色顯示此主機。',
|
||||
'hostDetails.icon.customOverridesDistro': '內建圖示會替換此主機的 Linux 發行版圖示。',
|
||||
'hostDetails.icon.option.server': '伺服器',
|
||||
'hostDetails.icon.option.terminal': '終端',
|
||||
'hostDetails.icon.option.database': '資料庫',
|
||||
'hostDetails.icon.option.cloud': '雲主機',
|
||||
'hostDetails.icon.option.router': '路由器',
|
||||
'hostDetails.icon.option.shield': '安全',
|
||||
'hostDetails.icon.option.code': '程式碼',
|
||||
'hostDetails.icon.option.box': '節點',
|
||||
'hostDetails.icon.option.globe': '公網',
|
||||
'hostDetails.icon.option.cpu': '計算',
|
||||
'hostDetails.icon.option.hard-drive': '儲存',
|
||||
'hostDetails.icon.option.network': '網路',
|
||||
'hostDetails.icon.option.wifi': '無線',
|
||||
'hostDetails.icon.option.lock': '鎖定',
|
||||
'hostDetails.icon.option.key': '金鑰',
|
||||
'hostDetails.icon.option.monitor': '顯示器',
|
||||
'hostDetails.icon.option.container': '容器',
|
||||
'hostDetails.icon.option.activity': '活動',
|
||||
'hostDetails.icon.option.zap': '高速',
|
||||
'hostDetails.icon.option.server-cog': '伺服器設定',
|
||||
'hostDetails.icon.color.blue': '藍色',
|
||||
'hostDetails.icon.color.green': '綠色',
|
||||
'hostDetails.icon.color.red': '紅色',
|
||||
'hostDetails.icon.color.amber': '琥珀色',
|
||||
'hostDetails.icon.color.purple': '紫色',
|
||||
'hostDetails.icon.color.cyan': '青色',
|
||||
'hostDetails.icon.color.orange': '橙色',
|
||||
'hostDetails.icon.color.slate': '石板灰',
|
||||
'hostDetails.icon.color.violet': '紫羅蘭',
|
||||
'hostDetails.icon.color.pink': '粉色',
|
||||
'hostDetails.icon.color.rose': '玫瑰紅',
|
||||
'hostDetails.icon.color.lime': '青檸',
|
||||
'hostDetails.icon.color.teal': '藍綠色',
|
||||
'hostDetails.icon.color.sky': '天藍',
|
||||
'hostDetails.icon.color.indigo': '靛藍',
|
||||
'hostDetails.icon.color.zinc': '鋅灰',
|
||||
'hostDetails.icon.color.custom': '自訂顏色',
|
||||
'hostDetails.distro.mode': '來源',
|
||||
'hostDetails.distro.mode.auto': '自動探測',
|
||||
'hostDetails.distro.mode.manual': '手動覆蓋',
|
||||
'hostDetails.distro.detectedLabel': '目前值',
|
||||
'hostDetails.distro.manualLabel': '手動指定',
|
||||
'hostDetails.distro.pending': '首次連線後自動探測',
|
||||
'hostDetails.distro.unknown': '未知',
|
||||
'hostDetails.distro.option.linux': '通用 Linux',
|
||||
'hostDetails.distro.option.ubuntu': 'Ubuntu',
|
||||
'hostDetails.distro.option.debian': 'Debian',
|
||||
'hostDetails.distro.option.centos': 'CentOS',
|
||||
'hostDetails.distro.option.rocky': 'Rocky Linux',
|
||||
'hostDetails.distro.option.fedora': 'Fedora',
|
||||
'hostDetails.distro.option.arch': 'Arch Linux',
|
||||
'hostDetails.distro.option.alpine': 'Alpine',
|
||||
'hostDetails.distro.option.amazon': 'Amazon Linux',
|
||||
'hostDetails.distro.option.opensuse': 'openSUSE / SLES',
|
||||
'hostDetails.distro.option.redhat': 'Red Hat / RHEL',
|
||||
'hostDetails.distro.option.almalinux': 'AlmaLinux',
|
||||
'hostDetails.distro.option.alinux': '阿里雲 Linux',
|
||||
'hostDetails.distro.option.openeuler': 'openEuler',
|
||||
'hostDetails.distro.option.oracle': 'Oracle Linux',
|
||||
'hostDetails.distro.option.kali': 'Kali Linux',
|
||||
'hostDetails.distro.option.cisco': '思科',
|
||||
'hostDetails.distro.option.juniper': '瞻博網路',
|
||||
'hostDetails.distro.option.huawei': '華為',
|
||||
'hostDetails.distro.option.h3c': '新華三 / H3C',
|
||||
'hostDetails.distro.option.hpe': '慧與 / HPE',
|
||||
'hostDetails.distro.option.mikrotik': 'MikroTik',
|
||||
'hostDetails.distro.option.fortinet': '飛塔',
|
||||
'hostDetails.distro.option.paloalto': 'Palo Alto Networks',
|
||||
'hostDetails.distro.option.zyxel': '合勤',
|
||||
'hostDetails.distro.option.ruijie': '銳捷',
|
||||
'hostDetails.section.mosh': 'Mosh',
|
||||
'hostDetails.section.et': 'EternalTerminal',
|
||||
'hostDetails.et.port': 'ET 服務埠',
|
||||
'hostDetails.et.port.desc': 'etserver 監聽埠(預設 2022)',
|
||||
'hostDetails.username.placeholder': '使用者名稱',
|
||||
'hostDetails.password.placeholder': '密碼',
|
||||
'hostDetails.password.show': '顯示密碼',
|
||||
'hostDetails.password.hide': '隱藏密碼',
|
||||
'hostDetails.password.save': '儲存密碼',
|
||||
'hostDetails.identity.suggestions': '身份',
|
||||
'hostDetails.identity.missing': '身份不存在',
|
||||
'hostDetails.credential.keyCertificate': '金鑰 / 憑證 / 本機金鑰',
|
||||
'hostDetails.credential.key': '金鑰',
|
||||
'hostDetails.credential.certificate': '憑證',
|
||||
'hostDetails.credential.localKeyFile': '本機金鑰檔案',
|
||||
'hostDetails.credential.localKeyFilePlaceholder': '~/.ssh/id_ed25519',
|
||||
'hostDetails.credential.browseKeyFile': '瀏覽…',
|
||||
'hostDetails.credential.missing': '憑證不存在',
|
||||
'hostDetails.keys.search': '搜尋金鑰…',
|
||||
'hostDetails.keys.empty': '暫無金鑰',
|
||||
'hostDetails.certs.search': '搜尋憑證…',
|
||||
'hostDetails.certs.empty': '暫無憑證',
|
||||
'hostDetails.agentForwarding': '轉發 SSH 金鑰',
|
||||
'hostDetails.agentForwarding.desc': '允許遠端伺服器使用本機 SSH 金鑰(例如用於 git 操作)',
|
||||
'hostDetails.agentForwarding.agentNotRunning': 'SSH Agent 不可用',
|
||||
'hostDetails.agentForwarding.agentNotRunningHint': '未偵測到 SSH Agent。請啟用 Windows OpenSSH Authentication Agent 服務,或使用相容的 Agent(如 Bitwarden、1Password、gpg-agent)。',
|
||||
'hostDetails.section.agentForwarding': 'SSH 代理',
|
||||
'hostDetails.x11Forwarding': '轉發 X11 圖形應用程式',
|
||||
'hostDetails.x11Forwarding.desc': '本機執行 X 服務時,讓遠端圖形程式顯示在本機桌面。',
|
||||
'hostDetails.section.x11Forwarding': 'X11 轉發',
|
||||
'hostDetails.section.deviceType': '裝置型別',
|
||||
'hostDetails.deviceType': '網路裝置模式',
|
||||
'hostDetails.deviceType.desc': '適用於透過 SSH 連線的網路裝置(交換機、路由器、防火牆)。指令將原樣傳送,不進行 Shell 包裝,相容華為 VRP、Cisco IOS 等廠商 CLI。',
|
||||
'hostDetails.deviceType.warning': 'AI 代理指令將直接傳送,無法取得結束代碼。僅建議在裝置不執行標準 Shell 時啟用。',
|
||||
'hostDetails.section.sshAlgorithms': 'SSH 演算法',
|
||||
'hostDetails.section.terminalBehavior': '終端行為',
|
||||
'hostDetails.lineTimestamps': '顯示輸出時間',
|
||||
'hostDetails.lineTimestamps.desc': '在終端輸出行旁邊顯示本機時間,不改變終端文字內容。',
|
||||
'hostDetails.legacyAlgorithms': '允許舊版演算法',
|
||||
'hostDetails.legacyAlgorithms.desc': '啟用已棄用的 SSH 演算法(diffie-hellman-group1、ssh-dss、3des-cbc 等)以連線老舊網路裝置。',
|
||||
'hostDetails.legacyAlgorithms.warning': '這些演算法存在已知安全漏洞,僅建議在老舊裝置不支援現代加密時啟用。',
|
||||
'hostDetails.skipEcdsaHostKey': '跳過 ECDSA 主機金鑰',
|
||||
'hostDetails.skipEcdsaHostKey.desc': '某些老款華為 / 思科交換機的 ECDSA 主機金鑰簽名不規範,會導致連線報 "signature verification failed"。開啟後用戶端不再 advertise ecdsa-sha2-*,強制使用 RSA / Ed25519。',
|
||||
'hostDetails.algorithms.advanced': '高階演算法設定',
|
||||
'hostDetails.algorithms.advanced.desc': '針對單個 host 自訂各分類的演算法清單。不勾選 = 使用預設;勾選子集後將完全替換預設列表。設定錯誤可能導致無法連線。',
|
||||
'hostDetails.algorithms.inheritedNotice': '目前組已設定以下分類的演算法 override:{categories}。本面板的"恢復預設"只會回到組的設定,而不是 NetCatty 預設列表。若要忽略組的限制,請到組的演算法設定裡取消。',
|
||||
'hostDetails.algorithms.customized': '已自訂',
|
||||
'hostDetails.algorithms.reset': '恢復預設',
|
||||
'hostDetails.algorithms.category.kex': '金鑰交換 (KEX)',
|
||||
'hostDetails.algorithms.category.cipher': '加密演算法 (Cipher)',
|
||||
'hostDetails.algorithms.category.hmac': '完整性演算法 (HMAC)',
|
||||
'hostDetails.algorithms.category.serverHostKey': '主機金鑰 (Host Key)',
|
||||
'hostDetails.algorithms.category.compress': '壓縮 (Compression)',
|
||||
'hostDetails.section.keepalive': '工作階段保活',
|
||||
'hostDetails.keepalive.override': '為此主機單獨設定',
|
||||
'hostDetails.keepalive.desc': '為該主機使用專屬的保活策略,而不是跟隨全域設定。適用於不響應 keepalive@openssh.com 請求的老舊路由器 / 交換機——將間隔設為 0 可對該主機徹底關閉保活。',
|
||||
'hostDetails.keepalive.interval': '間隔(秒)',
|
||||
'hostDetails.keepalive.countMax': '最大無響應保活次數',
|
||||
'hostDetails.keepalive.disabledHint': '間隔為 0 時該主機不傳送保活包,僅依賴 TCP 層超時偵測斷連。',
|
||||
'hostDetails.backspaceBehavior': 'Backspace 行為',
|
||||
'hostDetails.backspaceBehavior.default': '預設',
|
||||
'hostDetails.jumpHosts': '透過主機代理',
|
||||
'hostDetails.jumpHosts.hops': '{count} 跳',
|
||||
'hostDetails.jumpHosts.direct': '直連',
|
||||
'hostDetails.jumpHosts.configure': '設定代理主機',
|
||||
'hostDetails.proxy': '透過 HTTP/SOCKS5/指令代理',
|
||||
'hostDetails.proxy.none': '無',
|
||||
'hostDetails.proxy.edit': '編輯代理',
|
||||
'hostDetails.proxy.configure': '設定代理',
|
||||
'hostDetails.envVars': '環境變數',
|
||||
'hostDetails.envVars.add': '新增環境變數',
|
||||
'hostDetails.startupCommand': '啟動指令',
|
||||
'hostDetails.startupCommand.placeholder': '連線後執行的指令(例如:cd /app && ls)',
|
||||
'hostDetails.startupCommand.help': 'SSH 連線建立後將自動執行該指令。',
|
||||
'hostDetails.startupCommand.runMode': '多行執行方式',
|
||||
'hostDetails.startupCommand.runMode.help': '選擇連線後如何傳送多行啟動指令。',
|
||||
'hostDetails.startupCommand.runMode.lineDelay': '逐行傳送',
|
||||
'hostDetails.startupCommand.runMode.paste': '一次性傳送',
|
||||
'hostDetails.otherProtocols': '其他協定',
|
||||
'hostDetails.telnetOn': 'Telnet on',
|
||||
'hostDetails.port': '埠',
|
||||
'hostDetails.telnet.credentials': '憑證',
|
||||
'hostDetails.telnet.identity.placeholder': '手動身份',
|
||||
'hostDetails.telnet.username': 'Telnet 使用者名稱',
|
||||
'hostDetails.telnet.password': 'Telnet 密碼',
|
||||
'hostDetails.charset.placeholder': '字元集(例如 UTF-8)',
|
||||
'hostDetails.telnet.add': '新增 Telnet 協定',
|
||||
'hostDetails.telnet.setDefault': '預設用 Telnet 連線',
|
||||
'hostDetails.tags': '標籤',
|
||||
'hostDetails.group': '分組',
|
||||
'hostDetails.selectGroup': '選擇分組',
|
||||
'hostDetails.addTag': '新增標籤...',
|
||||
'hostDetails.createTag': '建立標籤',
|
||||
'hostDetails.createGroup': '建立分組',
|
||||
|
||||
// Host form (legacy modal)
|
||||
'hostForm.title.edit': '編輯主機',
|
||||
'hostForm.title.new': '新建主機',
|
||||
'hostForm.desc.edit': '更新該主機的連線資訊',
|
||||
'hostForm.desc.new': '建立一個新的 SSH 主機條目',
|
||||
'hostForm.field.label': '名稱',
|
||||
'hostForm.placeholder.label': 'My Production Server',
|
||||
'hostForm.field.hostname': 'Hostname / IP',
|
||||
'hostForm.placeholder.hostname': '192.168.1.1',
|
||||
'hostForm.field.port': '埠',
|
||||
'hostForm.field.username': '使用者名稱',
|
||||
'hostForm.field.osType': '作業系統型別',
|
||||
'hostForm.placeholder.selectOs': '選擇作業系統',
|
||||
'hostForm.field.group': '分組',
|
||||
'hostForm.placeholder.group': '例如:AWS、DigitalOcean',
|
||||
'hostForm.field.tags': '標籤',
|
||||
'hostForm.placeholder.addTag': '新增標籤…',
|
||||
'hostForm.auth.method': '認證方式',
|
||||
'hostForm.auth.password': '密碼',
|
||||
'hostForm.auth.sshKey': 'SSH金鑰',
|
||||
'hostForm.auth.selectKey': '選擇 SSH金鑰',
|
||||
'hostForm.auth.noKeys': '暫無金鑰',
|
||||
'hostForm.auth.noKeysHint': '鑰匙串中未找到 SSH金鑰,請先建立一個。',
|
||||
'hostForm.saveHost': '儲存主機',
|
||||
|
||||
// Connection logs
|
||||
'logs.table.date': '日期',
|
||||
'logs.table.user': '使用者',
|
||||
'logs.table.host': '主機',
|
||||
'logs.table.saved': '收藏',
|
||||
'logs.empty.title': '暫無連線日誌',
|
||||
'logs.empty.desc': '當你連線主機或開啟本機終端後,這裡會顯示連線歷史。',
|
||||
'logs.loadMore': '載入更多 ({count} 條)',
|
||||
'logs.ongoing': '進行中',
|
||||
'logs.localTerminal': '本機終端',
|
||||
'logs.action.save': '收藏',
|
||||
'logs.action.unsave': '取消收藏',
|
||||
'logs.action.delete': '刪除',
|
||||
|
||||
// Log view
|
||||
'logView.customizeAppearance': '自訂外觀',
|
||||
'logView.appearance': '外觀',
|
||||
'logView.readOnly': '只讀',
|
||||
'logView.export': '匯出',
|
||||
|
||||
// Terminal toolbar / search / context menu / auth
|
||||
'terminal.toolbar.openSftp': '開啟 SFTP',
|
||||
'terminal.toolbar.availableAfterConnect': '連線後可用',
|
||||
'terminal.toolbar.sendYmodem': 'YMODEM 傳送',
|
||||
'terminal.toolbar.receiveYmodem': 'YMODEM 接收',
|
||||
'terminal.toolbar.sftp': 'SFTP',
|
||||
'terminal.toolbar.more': '更多操作',
|
||||
'terminal.toolbar.scripts': '指令碼',
|
||||
'terminal.toolbar.history': '指令歷史',
|
||||
'terminal.toolbar.library': '庫',
|
||||
'terminal.toolbar.noSnippets': '暫無程式碼片段',
|
||||
'terminal.toolbar.terminalSettings': '終端設定',
|
||||
'terminal.toolbar.searchTerminal': '搜尋終端',
|
||||
'terminal.toolbar.search': '搜尋',
|
||||
'terminal.toolbar.startSessionLog': '開始工作階段日誌',
|
||||
'terminal.toolbar.stopSessionLog': '停止工作階段日誌',
|
||||
'terminal.toolbar.broadcast': '廣播',
|
||||
'terminal.toolbar.broadcastEnable': '啟用廣播模式',
|
||||
'terminal.toolbar.broadcastDisable': '關閉廣播模式',
|
||||
'terminal.toolbar.composeBar': '撰寫欄',
|
||||
'terminal.composeBar.placeholder': '在此輸入指令,按回車傳送...',
|
||||
'terminal.composeBar.send': '傳送',
|
||||
'terminal.composeBar.close': '關閉撰寫欄',
|
||||
'terminal.composeBar.broadcasting': '正在廣播到所有工作階段',
|
||||
'terminal.composeBar.resize': '拖拽調整撰寫欄高度',
|
||||
'terminal.composeBar.manageSnippets': '管理快捷程式碼片段',
|
||||
'terminal.composeBar.searchSnippets': '搜尋程式碼片段...',
|
||||
'terminal.composeBar.noPinnedSnippets': '點選 + 固定常用程式碼片段',
|
||||
'terminal.composeBar.noMatchingSnippets': '沒有相符的程式碼片段',
|
||||
'terminal.composeBar.pinnedCount': '已固定 {count} 個',
|
||||
'terminal.composeBar.unpinSnippet': '從快捷欄移除 {label}',
|
||||
'terminal.composeBar.snippetClickHint': '單擊插入 · Shift+單擊直接傳送',
|
||||
'terminal.toolbar.focus': '聚焦',
|
||||
'terminal.toolbar.focusMode': '聚焦模式',
|
||||
'terminal.toolbar.detach': '移出到獨立標籤',
|
||||
'terminal.toolbar.encoding': '終端編碼',
|
||||
'terminal.toolbar.encoding.utf8': 'UTF-8',
|
||||
'terminal.toolbar.encoding.gb18030': 'GB18030',
|
||||
'terminal.toolbar.closeSession': '關閉工作階段',
|
||||
'terminal.toolbar.hostHighlight.title': '主機關鍵字標示',
|
||||
'terminal.toolbar.hostHighlight.noRules': '此主機未定義自訂標示規則',
|
||||
'terminal.toolbar.hostHighlight.addRule': '新增新規則',
|
||||
'terminal.toolbar.hostHighlight.labelPlaceholder': '標籤(例如:錯誤)',
|
||||
'terminal.toolbar.hostHighlight.patternPlaceholder': '正規表示式(例如:\\bfailed\\b)',
|
||||
'terminal.toolbar.hostHighlight.invalidPattern': '無效的正規表示式',
|
||||
'terminal.toolbar.hostHighlight.clearAll': '清除全部',
|
||||
'terminal.toolbar.hostHighlight.changeColor': '更改標示顏色',
|
||||
'terminal.toolbar.hostHighlight.selectColor': '選擇新規則的顏色',
|
||||
'terminal.statusbar.copyHostname.label': '複製主機地址',
|
||||
'terminal.statusbar.copyHostname.tooltip': '複製主機地址({hostname})',
|
||||
'terminal.statusbar.copyHostname.toast': '已複製主機地址:{hostname}',
|
||||
'terminal.statusbar.copyHostname.error': '複製主機地址失敗',
|
||||
'terminal.serverStats.cpu': 'CPU 使用率',
|
||||
'terminal.serverStats.cpuCores': 'CPU 核心使用率',
|
||||
'terminal.serverStats.memory': '記憶體使用',
|
||||
'terminal.serverStats.memoryDetails': '記憶體詳情',
|
||||
'terminal.serverStats.memUsed': '已用',
|
||||
'terminal.serverStats.memBuffers': '緩衝區',
|
||||
'terminal.serverStats.memCached': '快取',
|
||||
'terminal.serverStats.memFree': '空閒',
|
||||
'terminal.serverStats.swap': '交換空間',
|
||||
'terminal.serverStats.swapUsed': '已用交換',
|
||||
'terminal.serverStats.swapFree': '空閒交換',
|
||||
'terminal.serverStats.swapTotal': '總計',
|
||||
'terminal.serverStats.topProcesses': '記憶體佔用前十程序',
|
||||
'terminal.serverStats.disk': '磁碟使用(根分割槽)',
|
||||
'terminal.serverStats.diskDetails': '已掛載磁碟',
|
||||
'terminal.serverStats.network': '網路速度',
|
||||
'terminal.serverStats.latency': '延遲',
|
||||
'terminal.serverStats.networkDetails': '網路介面',
|
||||
'terminal.serverStats.noData': '暫無資料',
|
||||
'terminal.dragDrop.localTitle': '拖放以插入路徑',
|
||||
'terminal.dragDrop.localMessage': '檔案路徑將被插入到終端',
|
||||
'terminal.dragDrop.remoteTitle': '拖放以上傳檔案',
|
||||
'terminal.dragDrop.remoteZmodemMessage': '檔案將透過 ZMODEM(PTY)上傳',
|
||||
'terminal.dragDrop.remoteSftpMessage': '檔案將透過 SFTP 上傳',
|
||||
'terminal.dragDrop.noFiles': '沒有可上傳的檔案',
|
||||
'terminal.dragDrop.notConnected': '無法拖放檔案 - 終端未連線',
|
||||
'terminal.dragDrop.errorTitle': '拖放錯誤',
|
||||
'terminal.dragDrop.errorMessage': '處理拖放檔案失敗',
|
||||
'terminal.search.placeholder': '搜尋…',
|
||||
'terminal.search.noResults': '無結果',
|
||||
'terminal.search.prevMatch': '上一個相符 (Shift+Enter)',
|
||||
'terminal.search.nextMatch': '下一個相符 (Enter)',
|
||||
'terminal.menu.copy': '複製',
|
||||
'terminal.menu.paste': '貼上',
|
||||
'terminal.menu.addSelectionToAI': '新增到對話',
|
||||
'terminal.menu.pasteSelection': '貼上選取文字',
|
||||
'terminal.menu.selectAll': '全選',
|
||||
'terminal.menu.reconnect': '重新連線',
|
||||
'terminal.menu.sendYmodem': 'YMODEM 傳送',
|
||||
'terminal.menu.receiveYmodem': 'YMODEM 接收',
|
||||
'terminal.menu.splitHorizontal': '水平分割',
|
||||
'terminal.menu.splitVertical': '垂直分割',
|
||||
'terminal.menu.clearBuffer': '清空緩衝區',
|
||||
'terminal.menu.closeTerminal': '關閉終端',
|
||||
'terminal.menu.rename': '重新命名',
|
||||
'terminal.menu.detach': '從工作區移出',
|
||||
'terminal.menu.detachSession': '移出 {name}',
|
||||
'terminal.ymodem.selectFile': '選擇要傳送的檔案',
|
||||
'terminal.ymodem.allFiles': '所有檔案',
|
||||
'terminal.ymodem.started': '正在透過 YMODEM 傳送 {fileName}',
|
||||
'terminal.ymodem.complete': 'YMODEM 已傳送 {fileName}',
|
||||
'terminal.ymodem.failed': 'YMODEM 傳送失敗',
|
||||
'terminal.ymodem.selectReceiveDirectory': '選擇接收檔案儲存位置',
|
||||
'terminal.ymodem.receiveStarted': '正在透過 YMODEM 接收...',
|
||||
'terminal.ymodem.receiveComplete': 'YMODEM 已接收 {fileName}',
|
||||
'terminal.ymodem.receiveCompleteMultiple': 'YMODEM 已接收 {count} 個檔案',
|
||||
'terminal.ymodem.receiveEmpty': '沒有接收到 YMODEM 檔案',
|
||||
'terminal.ymodem.receiveFailed': 'YMODEM 接收失敗',
|
||||
'terminal.ymodem.unavailable': 'YMODEM 目前不可用',
|
||||
'terminal.selection.addToAI': '新增到對話',
|
||||
'terminal.selection.addToAIDesc': '將選取的終端輸出作為附件加入 AI 草稿',
|
||||
'terminal.auth.password': '密碼',
|
||||
'terminal.auth.sshKey': 'SSH Key',
|
||||
'terminal.auth.username': '使用者名稱',
|
||||
'terminal.auth.username.placeholder': 'root',
|
||||
'terminal.auth.passwordLabel': '密碼',
|
||||
'terminal.auth.password.placeholder': '輸入密碼',
|
||||
'terminal.auth.passphrase': '密碼短語',
|
||||
'terminal.auth.passphrase.placeholder': '可選:所選私鑰的密碼短語',
|
||||
'terminal.auth.certificate': '憑證',
|
||||
'terminal.auth.selectKey': '選擇金鑰',
|
||||
'terminal.auth.retryMessage': '驗證失敗。請檢查憑證後再試一次。',
|
||||
'terminal.auth.retryLog': '驗證失敗,請再試一次。',
|
||||
'terminal.auth.noKeysHint': '暫無金鑰,請先在鑰匙串中新增。',
|
||||
'terminal.auth.continueSave': '繼續並儲存',
|
||||
'terminal.auth.credentialsUnavailable': '目前裝置無法解密已儲存憑證,請重新輸入並再次儲存。',
|
||||
'terminal.auth.jumpCredentialsUnavailable': '某個跳板機的已儲存憑證無法在目前裝置解密,請到主機設定中重新填寫。',
|
||||
'terminal.auth.proxyCredentialsUnavailable': '代理憑證無法在目前裝置解密,請到主機設定中重新填寫代理密碼。',
|
||||
'terminal.auth.keyUnavailableFallbackPassword': '已儲存 SSH 金鑰在目前裝置不可用,改用密碼認證。',
|
||||
'terminal.connectionErrorTitle': '連線錯誤',
|
||||
'terminal.progress.timeoutIn': '將在 {seconds}s 後超時',
|
||||
'terminal.progress.waitingForUserInput': '等待使用者輸入',
|
||||
'terminal.progress.disconnected': '已斷開',
|
||||
'terminal.progress.cancelling': '正在取消...',
|
||||
'terminal.progress.startOver': '重新開始',
|
||||
'terminal.connection.dismissDisconnectedDialog': '關閉斷連提示',
|
||||
'terminal.connection.chainOf': 'Chain {current} / {total}',
|
||||
'terminal.connection.showLogs': '顯示日誌',
|
||||
'terminal.connection.hideLogs': '隱藏日誌',
|
||||
'terminal.connection.protocol.ssh': 'SSH',
|
||||
'terminal.connection.protocol.telnet': 'Telnet',
|
||||
'terminal.connection.protocol.mosh': 'Mosh',
|
||||
'terminal.connection.protocol.serial': '序列埠',
|
||||
'terminal.connection.protocol.local': '本機終端',
|
||||
'terminal.hostKey.unknownTitle': '確認主機指紋',
|
||||
'terminal.hostKey.changedTitle': '主機指紋已變化',
|
||||
'terminal.hostKey.unknownDescription': '尚未確認 {host} 的真實性。',
|
||||
'terminal.hostKey.changedDescription': '{host} 的已儲存指紋與目前伺服器不一致。',
|
||||
'terminal.hostKey.fingerprintLabel': '{keyType} 指紋為 SHA256:',
|
||||
'terminal.hostKey.savedFingerprintLabel': '已儲存的指紋',
|
||||
'terminal.hostKey.unknownHint': '如果這個指紋屬於你預期連線的伺服器,可以記住它。',
|
||||
'terminal.hostKey.changedHint': '只有在你確認這台主機確實變更過時才繼續。',
|
||||
'terminal.hostKey.addAndContinue': '記住並繼續',
|
||||
'terminal.hostKey.updateAndContinue': '更新並繼續',
|
||||
'terminal.themeModal.title': 'Terminal 外觀',
|
||||
'terminal.themeModal.tab.theme': '主題',
|
||||
'terminal.themeModal.tab.font': '字型',
|
||||
'terminal.themeModal.tab.custom': '自訂',
|
||||
'terminal.themeModal.globalTheme': '全域主題',
|
||||
'terminal.themeModal.globalFont': '全域字型',
|
||||
'terminal.themeModal.fontSize': '字型大小',
|
||||
'terminal.themeModal.fontWeight': '字型粗細',
|
||||
'terminal.themeModal.livePreview': '即時預覽',
|
||||
'terminal.themeModal.themeType': '{type} 主題',
|
||||
'terminal.hiddenTheme.title': '目前隱藏主題',
|
||||
'terminal.hiddenTheme.desc': '這個主題已從手動選擇列表中隱藏;當你選擇其他可見主題後,它會被替換。',
|
||||
'topTabs.toggleTheme.systemExitTitle': '目前正在跟隨系統主題',
|
||||
'topTabs.toggleTheme.systemExitMessage': '請到設定裡選擇固定的淺色或深色主題。',
|
||||
'topTabs.toggleTheme.openSettings': '開啟設定',
|
||||
|
||||
// Custom Themes
|
||||
'terminal.customTheme.section': '自訂主題',
|
||||
'terminal.customTheme.yourThemes': '我的主題',
|
||||
'terminal.customTheme.new': '新建主題',
|
||||
'terminal.customTheme.newDesc': '克隆目前主題並自訂',
|
||||
'terminal.customTheme.newTitle': '新建自訂主題',
|
||||
'terminal.customTheme.editTitle': '編輯主題',
|
||||
'terminal.customTheme.import': '匯入 .itermcolors',
|
||||
'terminal.customTheme.importDesc': '從 iTerm2 配色方案檔案匯入',
|
||||
'terminal.customTheme.importError': '無法解析所選檔案,請確保它是有效的 .itermcolors XML 檔案。',
|
||||
'terminal.customTheme.delete': '刪除主題',
|
||||
'terminal.customTheme.confirmDelete': '確認刪除',
|
||||
'terminal.customTheme.name': '名稱',
|
||||
'terminal.customTheme.namePlaceholder': '我的自訂主題',
|
||||
'terminal.customTheme.type': '型別',
|
||||
'terminal.customTheme.group.general': '通用',
|
||||
'terminal.customTheme.group.normal': '標準色',
|
||||
'terminal.customTheme.group.bright': '標示色',
|
||||
'terminal.customTheme.color.background': '背景',
|
||||
'terminal.customTheme.color.foreground': '前景',
|
||||
'terminal.customTheme.color.cursor': '游標',
|
||||
'terminal.customTheme.color.selection': '選區',
|
||||
'terminal.customTheme.color.black': '黑色',
|
||||
'terminal.customTheme.color.red': '紅色',
|
||||
'terminal.customTheme.color.green': '綠色',
|
||||
'terminal.customTheme.color.yellow': '黃色',
|
||||
'terminal.customTheme.color.blue': '藍色',
|
||||
'terminal.customTheme.color.magenta': '品紅',
|
||||
'terminal.customTheme.color.cyan': '青色',
|
||||
'terminal.customTheme.color.white': '白色',
|
||||
'terminal.customTheme.color.brightBlack': '亮黑',
|
||||
'terminal.customTheme.color.brightRed': '亮紅',
|
||||
'terminal.customTheme.color.brightGreen': '亮綠',
|
||||
'terminal.customTheme.color.brightYellow': '亮黃',
|
||||
'terminal.customTheme.color.brightBlue': '亮藍',
|
||||
'terminal.customTheme.color.brightMagenta': '亮品紅',
|
||||
'terminal.customTheme.color.brightCyan': '亮青色',
|
||||
'terminal.customTheme.color.brightWhite': '亮白',
|
||||
|
||||
'cloudSync.gate.title': '端到端加密同步',
|
||||
'cloudSync.gate.desc':
|
||||
'資料會在本機加密後再同步,雲端不會看到明文。設定主金鑰以啟用安全同步。',
|
||||
'cloudSync.gate.masterKey': '主金鑰',
|
||||
'cloudSync.gate.confirmMasterKey': '確認主金鑰',
|
||||
'cloudSync.gate.placeholder': '輸入一個強密碼',
|
||||
'cloudSync.gate.confirmPlaceholder': '再次輸入密碼',
|
||||
'cloudSync.gate.mismatch': '兩次輸入的密碼不一致',
|
||||
'cloudSync.gate.warning':
|
||||
'我已瞭解:如果忘記主金鑰,資料無法恢復,且沒有密碼重置功能。',
|
||||
'cloudSync.gate.enableVault': '啟用加密 Vault',
|
||||
'cloudSync.gate.enabledToast': '已啟用加密 Vault',
|
||||
'cloudSync.gate.setupFailed': '設定主金鑰失敗',
|
||||
'cloudSync.passwordStrength.tooShort': '太短',
|
||||
'cloudSync.passwordStrength.weak': '弱',
|
||||
'cloudSync.passwordStrength.moderate': '一般',
|
||||
'cloudSync.passwordStrength.strong': '強',
|
||||
'cloudSync.passwordStrength.veryStrong': '非常強',
|
||||
'cloudSync.provider.notConnected': '未連線',
|
||||
'cloudSync.provider.sync': '同步',
|
||||
'cloudSync.provider.connect': '連線',
|
||||
'cloudSync.provider.connecting': '連線中...',
|
||||
'cloudSync.provider.webdav': 'WebDAV',
|
||||
'cloudSync.provider.webdav.desc': '連線到自建 WebDAV 端點',
|
||||
'cloudSync.provider.s3': 'S3 相容儲存',
|
||||
'cloudSync.provider.s3.desc': '連線到 S3 相容物件儲存',
|
||||
'cloudSync.provider.comingSoon': '即將支援',
|
||||
'cloudSync.webdav.title': 'WebDAV 設定',
|
||||
'cloudSync.webdav.desc': '設定 WebDAV 端點用於加密同步。',
|
||||
'cloudSync.webdav.endpoint': '端點地址',
|
||||
'cloudSync.webdav.authType': '認證方式',
|
||||
'cloudSync.webdav.auth.basic': 'Basic',
|
||||
'cloudSync.webdav.auth.digest': 'Digest',
|
||||
'cloudSync.webdav.auth.token': 'Token',
|
||||
'cloudSync.webdav.username': '使用者名稱',
|
||||
'cloudSync.webdav.password': '密碼',
|
||||
'cloudSync.webdav.token': 'Token',
|
||||
'cloudSync.webdav.showSecret': '顯示金鑰',
|
||||
'cloudSync.webdav.allowInsecure': '允許不安全的連線(忽略憑證錯誤)',
|
||||
'cloudSync.webdav.validation.endpoint': '請輸入有效的 WebDAV 端點。',
|
||||
'cloudSync.webdav.validation.credentials': '請輸入使用者名稱和密碼。',
|
||||
'cloudSync.webdav.validation.token': '請輸入 Token。',
|
||||
'cloudSync.s3.title': 'S3 設定',
|
||||
'cloudSync.s3.desc': '連線到 S3 相容物件儲存以進行加密同步。',
|
||||
'cloudSync.s3.endpoint': '端點地址',
|
||||
'cloudSync.s3.region': 'Region',
|
||||
'cloudSync.s3.bucket': 'Bucket',
|
||||
'cloudSync.s3.accessKeyId': 'Access Key ID',
|
||||
'cloudSync.s3.secretAccessKey': 'Secret Access Key',
|
||||
'cloudSync.s3.sessionToken': 'Session Token(可選)',
|
||||
'cloudSync.s3.prefix': 'Key 字首(可選)',
|
||||
'cloudSync.s3.forcePathStyle': '強制使用 path-style URL(適用於 MinIO/R2 等)',
|
||||
'cloudSync.s3.allowInsecure': '允許不安全的連線(忽略憑證錯誤)',
|
||||
'cloudSync.s3.showSecret': '顯示金鑰',
|
||||
'cloudSync.s3.validation.required': '端點、Region、Bucket、Access Key 與 Secret 必填。',
|
||||
'cloudSync.smb.title': 'SMB 設定',
|
||||
'cloudSync.smb.desc': '連線到 SMB/CIFS 檔案共享以進行加密同步。',
|
||||
'cloudSync.smb.share': '共享路徑',
|
||||
'cloudSync.smb.username': '使用者名稱',
|
||||
'cloudSync.smb.password': '密碼',
|
||||
'cloudSync.smb.domain': '域(可選)',
|
||||
'cloudSync.smb.domainPlaceholder': '例如:WORKGROUP',
|
||||
'cloudSync.smb.port': '埠(可選)',
|
||||
'cloudSync.smb.showSecret': '顯示密碼',
|
||||
'cloudSync.smb.validation.share': '共享路徑必填。',
|
||||
'cloudSync.smb.validation.port': '埠必須是 1 到 65535 之間的數字。',
|
||||
'cloudSync.connect.smb.success': 'SMB 已連線',
|
||||
'cloudSync.connect.smb.failedTitle': 'SMB 連線失敗',
|
||||
'cloudSync.provider.smb': 'SMB 共享',
|
||||
'cloudSync.connect.webdav.success': 'WebDAV 已連線',
|
||||
'cloudSync.connect.webdav.failedTitle': 'WebDAV 連線失敗',
|
||||
'cloudSync.connect.s3.success': 'S3 已連線',
|
||||
'cloudSync.connect.s3.failedTitle': 'S3 連線失敗',
|
||||
'cloudSync.lastSync.never': '從未',
|
||||
'cloudSync.lastSync.justNow': '剛剛',
|
||||
'cloudSync.lastSync.minutesAgo': '{minutes} 分鐘前',
|
||||
'cloudSync.changeKey': '更改 Key',
|
||||
'cloudSync.providers.title': '雲服務',
|
||||
'cloudSync.syncAll': '同步所有已連線的服務',
|
||||
'cloudSync.autoSync.title': '自動同步',
|
||||
'cloudSync.autoSync.desc': '發生變更時自動同步',
|
||||
'cloudSync.strategy.title': '同步策略',
|
||||
'cloudSync.strategy.desc': '當本機和雲端都發生變化時,選擇如何處理。',
|
||||
'cloudSync.strategy.smartMerge': '智慧合併(推薦)',
|
||||
'cloudSync.strategy.smartMergeDesc': '儘量保留兩邊的變化;如果無法安全判斷,會再讓你手動選擇。',
|
||||
'cloudSync.strategy.preferCloud': '雲端優先',
|
||||
'cloudSync.strategy.preferCloudDesc': '兩邊都有變化時,下載雲端版本,並替換本機變化。',
|
||||
'cloudSync.strategy.preferLocal': '本機優先',
|
||||
'cloudSync.strategy.preferLocalDesc': '兩邊都有變化時,上傳本機版本,並替換雲端變化。',
|
||||
'cloudSync.status.title': '同步狀態',
|
||||
'cloudSync.status.localVersion': '本機版本',
|
||||
'cloudSync.status.remoteVersion': '遠端版本',
|
||||
'cloudSync.history.title': '同步歷史',
|
||||
'cloudSync.history.upload': '上傳',
|
||||
'cloudSync.history.download': '下載',
|
||||
'cloudSync.history.resolved': '已解決',
|
||||
'cloudSync.history.error': '錯誤',
|
||||
'cloudSync.localBackups.title': '本機備份歷史',
|
||||
'cloudSync.localBackups.desc': 'Netcatty 會在版本變化前,以及恢復主機庫前,自動留下一份本機恢復點。',
|
||||
'cloudSync.localBackups.retentionTitle': '備份保留數量',
|
||||
'cloudSync.localBackups.retentionDesc': '設定 Netcatty 最多保留多少份本機備份。',
|
||||
'cloudSync.localBackups.maxCount': '最多保留',
|
||||
'cloudSync.localBackups.maxSaved': '已儲存保留數量:{count}',
|
||||
'cloudSync.localBackups.maxInvalid': '請輸入 1 到 100 之間的數字。',
|
||||
'cloudSync.localBackups.empty': '還沒有本機備份。',
|
||||
'cloudSync.localBackups.reason.appVersionChange': '版本變化前',
|
||||
'cloudSync.localBackups.reason.beforeRestore': '恢復前',
|
||||
'cloudSync.localBackups.versionChange': '{from} -> {to}',
|
||||
'cloudSync.localBackups.counts': '{hosts} 台主機,{keys} 個金鑰,{snippets} 個程式碼片段,{notes} 條筆記',
|
||||
'cloudSync.localBackups.restore': '恢復',
|
||||
'cloudSync.localBackups.restoreSuccess': '已恢復本機備份。',
|
||||
'cloudSync.localBackups.restoreFailedTitle': '恢復失敗',
|
||||
'cloudSync.localBackups.restoreMissing': '找不到這份備份。',
|
||||
'cloudSync.localBackups.protectiveBackupFailed': '無法建立保護性備份,已中止恢復以避免覆蓋目前資料。請先解決底層問題(例如鑰匙串存取)後重試。詳情:{message}',
|
||||
'cloudSync.localBackups.restoreConfirmTitle': '確認恢復此備份?',
|
||||
'cloudSync.localBackups.restoreConfirmDesc': '目前的主機、金鑰、程式碼片段與設定將被替換為此備份中的內容。系統會先自動建立一個保護性快照,便於復原。',
|
||||
'cloudSync.localBackups.restoreConfirmButton': '恢復',
|
||||
'cloudSync.localBackups.restoreConfirmCancel': '取消',
|
||||
'cloudSync.localBackups.unavailableTitle': '無法使用本機備份',
|
||||
'cloudSync.localBackups.unavailableDesc': '目前平台未提供受支援的安全金鑰庫,Netcatty 無法安全地寫入本機備份。請在支援系統鑰匙串的環境中執行,或改用雲同步保留恢復點。',
|
||||
'cloudSync.localBackups.lockedTitle': '需要主金鑰',
|
||||
'cloudSync.localBackups.lockedDesc': '請先設定或解鎖主金鑰再恢復備份,以確保恢復後的憑證仍保持加密。',
|
||||
'cloudSync.revisionHistory.viewButton': '歷史版本',
|
||||
'cloudSync.revisionHistory.title': '主機庫版本歷史',
|
||||
'cloudSync.revisionHistory.description': '瀏覽並恢復 Gist 修訂歷史中的舊版主機庫資料。',
|
||||
'cloudSync.revisionHistory.empty': '未找到修訂記錄。',
|
||||
'cloudSync.revisionHistory.current': '目前版本',
|
||||
'cloudSync.revisionHistory.revision': '修訂',
|
||||
'cloudSync.revisionHistory.revisionPreview': '修訂內容',
|
||||
'cloudSync.revisionHistory.device': '裝置',
|
||||
'cloudSync.revisionHistory.hosts': '主機',
|
||||
'cloudSync.revisionHistory.keys': '金鑰',
|
||||
'cloudSync.revisionHistory.snippets': '程式碼片段',
|
||||
'cloudSync.revisionHistory.notes': '筆記',
|
||||
'cloudSync.revisionHistory.identities': '身份',
|
||||
'cloudSync.revisionHistory.restoreButton': '恢復此版本',
|
||||
'cloudSync.revisionHistory.restored': '已從選取的修訂恢復主機庫資料。',
|
||||
'cloudSync.revisionHistory.revisionNotFound': '修訂未找到或不包含主機庫資料。',
|
||||
'cloudSync.revisionHistory.decryptFailed': '無法解密此修訂。可能是使用了不同的主金鑰加密的。',
|
||||
'cloudSync.changeKey.title': '更改主金鑰',
|
||||
'cloudSync.changeKey.current': '目前主金鑰',
|
||||
'cloudSync.changeKey.new': '新的主金鑰',
|
||||
'cloudSync.changeKey.confirmNew': '確認新的主金鑰',
|
||||
'cloudSync.changeKey.currentPlaceholder': '輸入目前主金鑰',
|
||||
'cloudSync.changeKey.newPlaceholder': '輸入新的主金鑰',
|
||||
'cloudSync.changeKey.confirmPlaceholder': '再次輸入新的主金鑰',
|
||||
'cloudSync.changeKey.fillAll': '請填寫所有欄位',
|
||||
'cloudSync.changeKey.minLength': '新的主金鑰至少 8 個字元',
|
||||
'cloudSync.changeKey.notMatch': '兩次輸入的主金鑰不一致',
|
||||
'cloudSync.changeKey.incorrectCurrent': '目前主金鑰不正確',
|
||||
'cloudSync.changeKey.failed': '更改主金鑰失敗',
|
||||
'cloudSync.changeKey.desc': '這將重新加密 Vault,請務必記住新的主金鑰。',
|
||||
'cloudSync.changeKey.showKeys': '顯示主金鑰',
|
||||
'cloudSync.changeKey.updatedToast': '主金鑰已更新',
|
||||
'cloudSync.changeKey.updateButton': '更新主金鑰',
|
||||
'cloudSync.unlock.title': '輸入主金鑰',
|
||||
'cloudSync.unlock.masterKey': '主金鑰',
|
||||
'cloudSync.unlock.desc': '僅需輸入一次主金鑰以啟用加密同步,之後會透過系統 Keychain 安全儲存。',
|
||||
'cloudSync.unlock.placeholder': '輸入你的主金鑰',
|
||||
'cloudSync.unlock.empty': '請輸入主金鑰',
|
||||
'cloudSync.unlock.incorrect': '主金鑰不正確',
|
||||
'cloudSync.unlock.failed': '解鎖 Vault 失敗',
|
||||
'cloudSync.unlock.showKey': '顯示主金鑰',
|
||||
'cloudSync.unlock.notNow': '暫不',
|
||||
'cloudSync.unlock.readyToast': 'Vault 已就緒',
|
||||
'cloudSync.unlock.unlockButton': '解鎖',
|
||||
'cloudSync.header.vaultReady': 'Vault 已就緒',
|
||||
'cloudSync.header.preparingVault': '正在準備 Vault...',
|
||||
'cloudSync.header.providersConnected': '已連線 {count} 個 provider',
|
||||
'cloudSync.githubFlow.title': '連線到 GitHub',
|
||||
'cloudSync.githubFlow.desc': '複製下面的 code,並在 GitHub 頁面輸入以授權 Netcatty。',
|
||||
'cloudSync.githubFlow.copyCode': '複製 code',
|
||||
'cloudSync.githubFlow.copied': '已複製',
|
||||
'cloudSync.githubFlow.openGitHub': '開啟 GitHub',
|
||||
'cloudSync.githubFlow.waiting': '等待授權...',
|
||||
'cloudSync.conflict.title': '偵測到版本衝突',
|
||||
'cloudSync.conflict.desc': '選擇保留哪個版本',
|
||||
'cloudSync.conflict.local': '本機',
|
||||
'cloudSync.conflict.cloud': '雲端',
|
||||
'cloudSync.conflict.detailsTitle': '發生變化的資料',
|
||||
'cloudSync.conflict.detailsCounts': '本機 {local} · 雲端 {cloud} · 衝突 {conflicts}',
|
||||
'cloudSync.conflict.entity.hosts': '主機',
|
||||
'cloudSync.conflict.entity.keys': '金鑰',
|
||||
'cloudSync.conflict.entity.identities': '身份',
|
||||
'cloudSync.conflict.entity.proxyProfiles': '代理設定',
|
||||
'cloudSync.conflict.entity.snippets': '片段',
|
||||
'cloudSync.conflict.entity.notes': '筆記',
|
||||
'cloudSync.conflict.entity.noteGroups': '筆記分組',
|
||||
'cloudSync.conflict.entity.customGroups': '分組',
|
||||
'cloudSync.conflict.entity.snippetPackages': '片段包',
|
||||
'cloudSync.conflict.entity.portForwardingRules': '埠轉發',
|
||||
'cloudSync.conflict.entity.groupConfigs': '分組設定',
|
||||
'cloudSync.conflict.entity.settings': '設定',
|
||||
'cloudSync.conflict.keepLocal': '覆蓋雲端(保留本機)',
|
||||
'cloudSync.conflict.useCloud': '下載雲端(覆蓋本機)',
|
||||
'cloudSync.connect.browserContinue': '請在瀏覽器中完成授權',
|
||||
'cloudSync.connect.browserCancelled': '已取消上一個瀏覽器授權流程',
|
||||
'cloudSync.connect.github.success': 'GitHub 已連線',
|
||||
'cloudSync.connect.github.failedTitle': 'GitHub 連線失敗',
|
||||
'cloudSync.connect.github.timeout': '連線 GitHub 超時,請檢查網路或代理設定。',
|
||||
'cloudSync.connect.github.networkError': '無法存取 GitHub,請檢查網路或代理設定。',
|
||||
'cloudSync.connect.google.failedTitle': 'Google 連線失敗',
|
||||
'cloudSync.connect.onedrive.failedTitle': 'OneDrive 連線失敗',
|
||||
'cloudSync.sync.success': '已同步到 {provider}',
|
||||
'cloudSync.sync.failed': '同步失敗',
|
||||
'cloudSync.sync.failedTitle': '同步失敗',
|
||||
'cloudSync.sync.errorTitle': '同步錯誤',
|
||||
'cloudSync.resolve.downloaded': '已下載雲端資料',
|
||||
'cloudSync.resolve.uploaded': '已上傳本機資料',
|
||||
'cloudSync.resolve.failedTitle': '衝突處理失敗',
|
||||
'cloudSync.clearLocal.title': '清空本機資料',
|
||||
'cloudSync.clearLocal.desc': '重置本機版本和同步歷史。下次同步將從雲端下載。',
|
||||
'cloudSync.clearLocal.button': '清空',
|
||||
'cloudSync.clearLocal.dialog.title': '清空本機 Vault 資料?',
|
||||
'cloudSync.clearLocal.dialog.desc': '這將重置本機版本為 0 並清除同步歷史。下次同步時會從雲端下載資料,替換本機資料。',
|
||||
'cloudSync.clearLocal.dialog.cancel': '取消',
|
||||
'cloudSync.clearLocal.dialog.confirm': '確認清空',
|
||||
'cloudSync.clearLocal.toast.title': '本機資料已清空',
|
||||
'cloudSync.clearLocal.toast.desc': '本機版本已重置為 0。同步以從雲端下載資料。',
|
||||
|
||||
// Common (additional)
|
||||
'common.searchPlaceholder': '搜尋...',
|
||||
'common.import': '匯入',
|
||||
'common.generate': '生成',
|
||||
'common.delete': '刪除',
|
||||
'common.edit': '編輯',
|
||||
'sftp.context.openWithDefault': '系統預設程式開啟',
|
||||
'common.clear': '清除',
|
||||
'common.optional': '可選',
|
||||
'common.selectPlaceholder': '請選擇...',
|
||||
'common.error': '錯誤',
|
||||
'common.validation': '驗證',
|
||||
'common.saveChanges': '儲存修改',
|
||||
'common.advanced': '高階',
|
||||
'common.selectAHostPlaceholder': '選擇主機...',
|
||||
|
||||
// Actions
|
||||
'action.duplicate': '複製',
|
||||
'action.open': '開啟',
|
||||
'action.copy': '複製',
|
||||
'action.run': '執行',
|
||||
'action.start': '啟動',
|
||||
'action.stop': '停止',
|
||||
|
||||
// Port Forwarding (form)
|
||||
'pf.form.labelPlaceholder': '規則標籤',
|
||||
'pf.form.intermediateHost': '中轉主機 *',
|
||||
'pf.form.createRule': '建立規則',
|
||||
'pf.form.openWizard': '打開向導',
|
||||
'pf.form.openWizardTitle': '開啟埠轉發精靈',
|
||||
'pf.action.newForwarding': '新建轉發',
|
||||
'pf.view.grid': '網格',
|
||||
'pf.view.list': '列表',
|
||||
'pf.rule.summary.local': '本機 {bindAddress}:{localPort} -> SSH 隧道 -> 遠端 {remoteHost}:{remotePort}',
|
||||
'pf.rule.summary.remote': '遠端 {bindAddress}:{localPort} -> SSH 隧道 -> 本機 {remoteHost}:{remotePort}',
|
||||
'pf.rule.summary.dynamic': 'SOCKS 監聽於 {bindAddress}:{localPort}',
|
||||
'pf.tooltip.relayHost': '中轉主機',
|
||||
'pf.tooltip.hostLabel': '主機',
|
||||
'pf.tooltip.hostAddress': '地址',
|
||||
'pf.tooltip.noHost': '未設定中轉主機',
|
||||
'pf.tooltip.localDesc': '本機埠轉發:透過 SSH 隧道存取遠端服務',
|
||||
'pf.tooltip.remoteDesc': '遠端埠轉發:將本機服務公開給遠端主機',
|
||||
'pf.tooltip.dynamicDesc': '動態 SOCKS 代理:透過 SSH 隧道轉發流量',
|
||||
'pf.deleteActive.title': '刪除正在執行的埠轉發?',
|
||||
'pf.deleteActive.desc': '埠轉發規則 "{label}" 目前正在執行。刪除前將先關閉轉發連線。',
|
||||
'pf.deleteActive.confirm': '關閉並刪除',
|
||||
'pf.form.autoStart': '自動啟動',
|
||||
'pf.form.autoStartDesc': '應用程式啟動時自動開啟此規則',
|
||||
|
||||
// SFTP (pane + conflict)
|
||||
'sftp.pane.local': '本機',
|
||||
'sftp.pane.remote': '遠端',
|
||||
'sftp.pane.selectHost': '選擇主機',
|
||||
'sftp.pane.selectHostToStart': '先選擇一個主機',
|
||||
'sftp.pane.chooseFilesystem': '選擇要瀏覽的本機或遠端檔案系統',
|
||||
'sftp.tabs.addTab': '新建標籤頁',
|
||||
'sftp.tabs.closeTab': '關閉標籤頁',
|
||||
'sftp.tabs.newTab': '新標籤頁',
|
||||
'sftp.tabs.copyDefaultPath': '複製標籤頁(預設路徑)',
|
||||
'sftp.tabs.copyCurrentPath': '複製並跳轉到目前路徑',
|
||||
'sftp.conflict.title': '檔案衝突',
|
||||
'sftp.conflict.desc': '目標位置已存在同名檔案',
|
||||
'sftp.conflict.alreadyExistsSuffix': '已存在',
|
||||
'sftp.conflict.existingFile': '已有檔案',
|
||||
'sftp.conflict.newFile': '新檔案',
|
||||
'sftp.conflict.size': '大小:',
|
||||
'sftp.conflict.modified': '修改時間:',
|
||||
'sftp.conflict.applyToAll': '將此操作套用到剩餘的 {count} 個衝突',
|
||||
'sftp.conflict.action.stop': '停止',
|
||||
'sftp.conflict.action.skip': '跳過',
|
||||
'sftp.conflict.action.keepBoth': '保留兩者',
|
||||
'sftp.conflict.action.duplicate': '建立副本',
|
||||
'sftp.conflict.action.merge': '合併',
|
||||
'sftp.conflict.action.replace': '替換',
|
||||
|
||||
// SFTP Upload Phases
|
||||
'sftp.upload.phase.compressing': '正在壓縮',
|
||||
'sftp.upload.phase.uploading': '正在上傳',
|
||||
'sftp.upload.phase.extracting': '正在解壓',
|
||||
'sftp.upload.phase.compressed': '壓縮傳輸',
|
||||
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import en, { type Messages } from './locales/en';
|
||||
import zhCN from './locales/zh-CN';
|
||||
import zhTW from './locales/zh-TW';
|
||||
import ru from './locales/ru';
|
||||
|
||||
// Keep keys stable; add new locales by adding another import and map entry.
|
||||
@@ -9,5 +10,6 @@ export const MESSAGES_BY_LOCALE: Record<string, Messages> = {
|
||||
en,
|
||||
ru,
|
||||
'zh-CN': zhCN,
|
||||
'zh-TW': zhTW,
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface LocalVaultBackupPreview {
|
||||
hostCount: number;
|
||||
keyCount: number;
|
||||
snippetCount: number;
|
||||
noteCount: number;
|
||||
identityCount: number;
|
||||
portForwardingRuleCount: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
test("active tab changes notify chrome theme before react subscribers", () => {
|
||||
const storeSource = readFileSync(new URL("./activeTabStore.ts", import.meta.url), "utf8");
|
||||
const syncSource = readFileSync(new URL("./activeChromeThemeSync.ts", import.meta.url), "utf8");
|
||||
|
||||
const setActiveTabIdBody = storeSource.match(/setActiveTabId = \(id: string\) => \{[\s\S]*?\n {2}\};/)?.[0] ?? "";
|
||||
assert.match(setActiveTabIdBody, /this\.syncListeners\.forEach\(\(listener\) => listener\(id\)\)/);
|
||||
assert.match(setActiveTabIdBody, /this\.scheduleNotify\(\)/);
|
||||
assert.ok(
|
||||
setActiveTabIdBody.indexOf("syncListeners.forEach") < setActiveTabIdBody.indexOf("scheduleNotify"),
|
||||
"sync chrome theme listeners must run before deferred react notify",
|
||||
);
|
||||
assert.match(syncSource, /activeTabStore\.subscribeSync\(notifyActiveChromeThemeForTab\)/);
|
||||
assert.match(syncSource, /isActiveChromeThemeResolvable/);
|
||||
assert.match(syncSource, /clearTopTabsChromeThemeVars/);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { isActiveChromeThemeResolvable, resolveActiveChromeTheme } from '../app/activeChromeTheme';
|
||||
import { clearTopTabsChromeThemeVars } from '../app/topTabsChromeTheme';
|
||||
import type { TerminalAppearanceHostScope, ResolvedAppearance } from '../../domain/terminalAppearanceRuntime';
|
||||
import type { Host, TerminalSession, TerminalTheme, Workspace } from '../../types';
|
||||
import { activeTabStore } from './activeTabStore';
|
||||
import type { EditorTab } from './editorTabStore';
|
||||
import type { LogView } from './logViewState';
|
||||
import { syncActiveChromeTheme } from './useActiveChromeTheme';
|
||||
|
||||
export type ActiveChromeThemeDeps = {
|
||||
accentMode: 'theme' | 'custom';
|
||||
applyAppTheme: () => void;
|
||||
currentTerminalTheme: TerminalTheme;
|
||||
customAccent: string;
|
||||
editorTabs: readonly EditorTab[];
|
||||
followAppTerminalTheme: boolean;
|
||||
hostById: Map<string, Host>;
|
||||
logViews: readonly LogView[];
|
||||
resolveSessionAppearance?: (hostScope: TerminalAppearanceHostScope) => ResolvedAppearance;
|
||||
sessionById: Map<string, TerminalSession>;
|
||||
themeById: Map<string, TerminalTheme>;
|
||||
workspaceById: Map<string, Workspace>;
|
||||
};
|
||||
|
||||
let depsRef: ActiveChromeThemeDeps | null = null;
|
||||
|
||||
export function updateActiveChromeThemeDeps(deps: ActiveChromeThemeDeps): void {
|
||||
depsRef = deps;
|
||||
}
|
||||
|
||||
export function notifyActiveChromeThemeForTab(activeTabId: string): void {
|
||||
if (!depsRef || typeof document === 'undefined') return;
|
||||
if (activeTabId === 'vault' || activeTabId === 'sftp') {
|
||||
clearTopTabsChromeThemeVars();
|
||||
}
|
||||
if (!isActiveChromeThemeResolvable({ ...depsRef, activeTabId })) return;
|
||||
const activeTheme = resolveActiveChromeTheme({ ...depsRef, activeTabId });
|
||||
syncActiveChromeTheme(activeTheme, depsRef.applyAppTheme);
|
||||
}
|
||||
|
||||
activeTabStore.subscribeSync(notifyActiveChromeThemeForTab);
|
||||
@@ -0,0 +1,54 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { activeTabStore, fromEditorTabId, isEditorTabId, toEditorTabId } from './activeTabStore';
|
||||
import { terminalLayoutSuppressStore } from './terminalLayoutSuppressStore';
|
||||
|
||||
test('editor tab helpers round trip ids', () => {
|
||||
assert.equal(toEditorTabId('file-1'), 'editor:file-1');
|
||||
assert.equal(fromEditorTabId('editor:file-1'), 'file-1');
|
||||
});
|
||||
|
||||
test('editor tab helper detects editor top-tab ids', () => {
|
||||
assert.equal(isEditorTabId('editor:file-1'), true);
|
||||
assert.equal(isEditorTabId('session-1'), false);
|
||||
});
|
||||
|
||||
test('active tab changes do not start terminal layout suppression', async () => {
|
||||
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||
(globalThis as { window?: unknown }).window ??= globalThis;
|
||||
|
||||
while (terminalLayoutSuppressStore.getActive()) {
|
||||
terminalLayoutSuppressStore.end();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
let activeNotifyCount = 0;
|
||||
let suppressNotifyCount = 0;
|
||||
const unsubscribeActiveTab = activeTabStore.subscribe(() => {
|
||||
activeNotifyCount += 1;
|
||||
});
|
||||
const unsubscribeSuppress = terminalLayoutSuppressStore.subscribe(() => {
|
||||
suppressNotifyCount += 1;
|
||||
});
|
||||
|
||||
try {
|
||||
activeTabStore.setActiveTabId(`no-suppress-test-${Date.now()}`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
assert.equal(activeNotifyCount, 1);
|
||||
assert.equal(suppressNotifyCount, 0);
|
||||
assert.equal(terminalLayoutSuppressStore.getActive(), false);
|
||||
} finally {
|
||||
unsubscribeActiveTab();
|
||||
unsubscribeSuppress();
|
||||
if (previousWindow === undefined) {
|
||||
delete (globalThis as { window?: unknown }).window;
|
||||
} else {
|
||||
(globalThis as { window?: unknown }).window = previousWindow;
|
||||
}
|
||||
while (terminalLayoutSuppressStore.getActive()) {
|
||||
terminalLayoutSuppressStore.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useSyncExternalStore } from 'react';
|
||||
|
||||
// Simple store for active tab that allows fine-grained subscriptions
|
||||
type Listener = () => void;
|
||||
type SyncListener = (activeTabId: string) => void;
|
||||
|
||||
// ----- Editor tab id helpers -----
|
||||
export const EDITOR_PREFIX = 'editor:';
|
||||
@@ -18,20 +19,29 @@ export const fromEditorTabId = (tabId: string): string => tabId.slice(EDITOR_PRE
|
||||
class ActiveTabStore {
|
||||
private activeTabId: string = 'vault';
|
||||
private listeners = new Set<Listener>();
|
||||
private pendingNotify = false;
|
||||
private syncListeners = new Set<SyncListener>();
|
||||
private notifyRafId: number | null = null;
|
||||
|
||||
getActiveTabId = () => this.activeTabId;
|
||||
|
||||
private scheduleNotify = () => {
|
||||
if (this.notifyRafId !== null) return;
|
||||
const schedule = typeof requestAnimationFrame === 'function'
|
||||
? requestAnimationFrame
|
||||
: (cb: () => void) => window.setTimeout(cb, 0) as unknown as number;
|
||||
this.notifyRafId = schedule(() => {
|
||||
this.notifyRafId = null;
|
||||
this.listeners.forEach((listener) => listener());
|
||||
});
|
||||
};
|
||||
|
||||
setActiveTabId = (id: string) => {
|
||||
if (this.activeTabId !== id) {
|
||||
this.activeTabId = id;
|
||||
// Defer listener notification to avoid "setState during render" if called from a render phase
|
||||
if (this.pendingNotify) return;
|
||||
this.pendingNotify = true;
|
||||
Promise.resolve().then(() => {
|
||||
this.pendingNotify = false;
|
||||
this.listeners.forEach(listener => listener());
|
||||
});
|
||||
this.syncListeners.forEach((listener) => listener(id));
|
||||
// Coalesce rapid tab switches into one notification per frame and avoid
|
||||
// "setState during render" if called from a render phase.
|
||||
this.scheduleNotify();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,6 +49,11 @@ class ActiveTabStore {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
};
|
||||
|
||||
subscribeSync = (listener: SyncListener) => {
|
||||
this.syncListeners.add(listener);
|
||||
return () => this.syncListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
export const activeTabStore = new ActiveTabStore();
|
||||
@@ -47,19 +62,15 @@ export const activeTabStore = new ActiveTabStore();
|
||||
export const useActiveTabId = () => {
|
||||
return useSyncExternalStore(
|
||||
activeTabStore.subscribe,
|
||||
activeTabStore.getActiveTabId
|
||||
activeTabStore.getActiveTabId,
|
||||
activeTabStore.getActiveTabId,
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to get setter - never causes re-render
|
||||
export const useSetActiveTabId = () => {
|
||||
return activeTabStore.setActiveTabId;
|
||||
};
|
||||
|
||||
// Check if a specific tab is active - only re-renders when this specific tab's active state changes
|
||||
export const useIsTabActive = (tabId: string) => {
|
||||
const getSnapshot = useCallback(() => activeTabStore.getActiveTabId() === tabId, [tabId]);
|
||||
return useSyncExternalStore(activeTabStore.subscribe, getSnapshot);
|
||||
return useSyncExternalStore(activeTabStore.subscribe, getSnapshot, getSnapshot);
|
||||
};
|
||||
|
||||
// Stable snapshot functions - defined once outside components
|
||||
@@ -70,7 +81,8 @@ const getIsSftpActive = () => activeTabStore.getActiveTabId() === 'sftp';
|
||||
export const useIsVaultActive = () => {
|
||||
return useSyncExternalStore(
|
||||
activeTabStore.subscribe,
|
||||
getIsVaultActive
|
||||
getIsVaultActive,
|
||||
getIsVaultActive,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -78,7 +90,8 @@ export const useIsVaultActive = () => {
|
||||
export const useIsSftpActive = () => {
|
||||
return useSyncExternalStore(
|
||||
activeTabStore.subscribe,
|
||||
getIsSftpActive
|
||||
getIsSftpActive,
|
||||
getIsSftpActive,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -86,17 +99,5 @@ export const useIsSftpActive = () => {
|
||||
export const useIsEditorTabActive = (tabId: string): boolean => {
|
||||
const editorTopId = toEditorTabId(tabId);
|
||||
const getSnapshot = useCallback(() => activeTabStore.getActiveTabId() === editorTopId, [editorTopId]);
|
||||
return useSyncExternalStore(activeTabStore.subscribe, getSnapshot);
|
||||
};
|
||||
|
||||
// Check if terminal layer should be visible
|
||||
// Editor tabs are NOT terminal tabs, so exclude them from the visibility condition.
|
||||
export const useIsTerminalLayerVisible = (draggingSessionId: string | null) => {
|
||||
const getSnapshot = useCallback(() => {
|
||||
const activeTabId = activeTabStore.getActiveTabId();
|
||||
const isTerminalTab = activeTabId !== 'vault' && activeTabId !== 'sftp' && !isEditorTabId(activeTabId);
|
||||
return isTerminalTab || !!draggingSessionId;
|
||||
}, [draggingSessionId]);
|
||||
|
||||
return useSyncExternalStore(activeTabStore.subscribe, getSnapshot);
|
||||
return useSyncExternalStore(activeTabStore.subscribe, getSnapshot, getSnapshot);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ensureDraftForScopeState,
|
||||
getDraftMutationVersionState,
|
||||
getDraftUploadGenerationState,
|
||||
pruneStaleSessionPanelViews,
|
||||
pruneTerminalScopeState,
|
||||
pruneTerminalTransientState,
|
||||
resolvePanelView,
|
||||
@@ -89,6 +90,39 @@ test("setSessionView records target session id", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("pruneStaleSessionPanelViews resets session views that no longer exist", () => {
|
||||
const panelViewByScope = {
|
||||
"terminal:1": { mode: "session", sessionId: "deleted-session" },
|
||||
"workspace:2": { mode: "session", sessionId: "session-keep" },
|
||||
"terminal:3": { mode: "draft" },
|
||||
} satisfies Record<string, { mode: "draft" } | { mode: "session"; sessionId: string }>;
|
||||
|
||||
const next = pruneStaleSessionPanelViews(
|
||||
panelViewByScope,
|
||||
new Set(["session-keep"]),
|
||||
);
|
||||
|
||||
assert.deepEqual(next, {
|
||||
"terminal:1": { mode: "draft" },
|
||||
"workspace:2": { mode: "session", sessionId: "session-keep" },
|
||||
"terminal:3": { mode: "draft" },
|
||||
});
|
||||
});
|
||||
|
||||
test("pruneStaleSessionPanelViews returns the original ref when nothing is stale", () => {
|
||||
const panelViewByScope = {
|
||||
"terminal:1": { mode: "session", sessionId: "session-keep" },
|
||||
"terminal:2": { mode: "draft" },
|
||||
} satisfies Record<string, { mode: "draft" } | { mode: "session"; sessionId: string }>;
|
||||
|
||||
const next = pruneStaleSessionPanelViews(
|
||||
panelViewByScope,
|
||||
new Set(["session-keep"]),
|
||||
);
|
||||
|
||||
assert.equal(next, panelViewByScope);
|
||||
});
|
||||
|
||||
test("clearScopeDraftState removes both the draft and current panel view", () => {
|
||||
const draftsByScope = {
|
||||
"terminal:1": createEmptyDraft("agent-alpha"),
|
||||
|
||||
@@ -115,6 +115,25 @@ export function setSessionView(
|
||||
};
|
||||
}
|
||||
|
||||
export function pruneStaleSessionPanelViews(
|
||||
panelViewByScope: PanelViewByScope,
|
||||
validSessionIds: Set<string>,
|
||||
): PanelViewByScope {
|
||||
let next = panelViewByScope;
|
||||
|
||||
for (const [scopeKey, panelView] of Object.entries(panelViewByScope)) {
|
||||
if (panelView?.mode !== 'session' || validSessionIds.has(panelView.sessionId)) {
|
||||
continue;
|
||||
}
|
||||
const updated = setDraftView(next, scopeKey);
|
||||
if (updated !== next) {
|
||||
next = updated;
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
export function updateDraftForScope(
|
||||
draftsByScope: DraftsByScope,
|
||||
scopeKey: string,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
export function removeProviderReferences(
|
||||
removedProviderId: string,
|
||||
agentProviderMap: Record<string, string>,
|
||||
agentModelMap: Record<string, string>,
|
||||
): {
|
||||
agentProviderMap: Record<string, string>;
|
||||
agentModelMap: Record<string, string>;
|
||||
providerMapChanged: boolean;
|
||||
modelMapChanged: boolean;
|
||||
} {
|
||||
let providerMapChanged = false;
|
||||
let modelMapChanged = false;
|
||||
const orphanedAgents = new Set<string>();
|
||||
const nextAgentProviderMap: Record<string, string> = {};
|
||||
|
||||
for (const [agentId, providerId] of Object.entries(agentProviderMap)) {
|
||||
if (providerId === removedProviderId) {
|
||||
providerMapChanged = true;
|
||||
orphanedAgents.add(agentId);
|
||||
} else {
|
||||
nextAgentProviderMap[agentId] = providerId;
|
||||
}
|
||||
}
|
||||
|
||||
const nextAgentModelMap: Record<string, string> = { ...agentModelMap };
|
||||
for (const agentId of orphanedAgents) {
|
||||
if (agentId in nextAgentModelMap) {
|
||||
delete nextAgentModelMap[agentId];
|
||||
modelMapChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
agentProviderMap: providerMapChanged ? nextAgentProviderMap : agentProviderMap,
|
||||
agentModelMap: modelMapChanged ? nextAgentModelMap : agentModelMap,
|
||||
providerMapChanged,
|
||||
modelMapChanged,
|
||||
};
|
||||
}
|
||||
@@ -65,7 +65,7 @@ test("pruneInactiveScopedTransientState removes closed workspace and terminal sc
|
||||
});
|
||||
});
|
||||
|
||||
test("pruneInactiveScopedSessions preserves restorable terminal ACP ids across reconnects", () => {
|
||||
test("pruneInactiveScopedSessions reports inactive targets without deleting persisted history", () => {
|
||||
const sessions = [
|
||||
createSession("terminal-restorable", {
|
||||
type: "terminal",
|
||||
@@ -98,10 +98,32 @@ test("pruneInactiveScopedSessions preserves restorable terminal ACP ids across r
|
||||
"terminal-local",
|
||||
"workspace-closed",
|
||||
]);
|
||||
assert.deepEqual(next.sessions, [
|
||||
sessions[0],
|
||||
sessions[3],
|
||||
assert.equal(next.sessions, sessions);
|
||||
});
|
||||
|
||||
test("pruneInactiveScopedSessions preserves inactive workspace and local terminal history after restart", () => {
|
||||
const sessions = [
|
||||
createSession("terminal-local", {
|
||||
type: "terminal",
|
||||
targetId: "closed-local",
|
||||
hostIds: ["local-shell"],
|
||||
}, "ext-local"),
|
||||
createSession("workspace-closed", {
|
||||
type: "workspace",
|
||||
targetId: "closed-workspace",
|
||||
}, "ext-workspace"),
|
||||
];
|
||||
|
||||
const next = pruneInactiveScopedSessions(
|
||||
sessions,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
assert.deepEqual(next.orphanedSessionIds, [
|
||||
"terminal-local",
|
||||
"workspace-closed",
|
||||
]);
|
||||
assert.equal(next.sessions, sessions);
|
||||
});
|
||||
|
||||
test("pruneInactiveScopedSessions preserves original sessions when orphaned restorable chats are already detached", () => {
|
||||
@@ -131,7 +153,7 @@ test("pruneInactiveScopedSessions treats sessions displayed elsewhere as in-use,
|
||||
// terminal-restorable's original scope (terminal-closed-A) is gone, but
|
||||
// the user resumed it into terminal-open-B from history. The session's
|
||||
// externalSessionId must be preserved and it must not appear in the
|
||||
// orphaned list, otherwise the active chat loses ACP continuity.
|
||||
// orphaned list, otherwise the active chat loses external agent continuity.
|
||||
const resumedElsewhere = createSession("terminal-restorable", {
|
||||
type: "terminal",
|
||||
targetId: "terminal-closed-A",
|
||||
|
||||
@@ -90,12 +90,6 @@ export function pruneInactiveScopedTransientState(
|
||||
};
|
||||
}
|
||||
|
||||
function isRestorableTerminalSession(session: AISession): boolean {
|
||||
return session.scope.type === "terminal"
|
||||
&& !!session.scope.hostIds?.length
|
||||
&& session.scope.hostIds.some((id) => !id.startsWith("local-") && !id.startsWith("serial-"));
|
||||
}
|
||||
|
||||
export function pruneInactiveScopedSessions(
|
||||
sessions: AISession[],
|
||||
activeTargetIds: Set<string>,
|
||||
@@ -123,23 +117,8 @@ export function pruneInactiveScopedSessions(
|
||||
};
|
||||
}
|
||||
|
||||
const orphanedSessionIdSet = new Set(orphanedSessionIds);
|
||||
let sessionsChanged = false;
|
||||
|
||||
const nextSessions = sessions.flatMap((session) => {
|
||||
if (!orphanedSessionIdSet.has(session.id)) {
|
||||
return [session];
|
||||
}
|
||||
|
||||
if (!isRestorableTerminalSession(session)) {
|
||||
sessionsChanged = true;
|
||||
return [];
|
||||
}
|
||||
return [session];
|
||||
});
|
||||
|
||||
return {
|
||||
sessions: sessionsChanged ? nextSessions : sessions,
|
||||
sessions,
|
||||
orphanedSessionIds,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Same-window AI-state-changed event plumbing.
|
||||
*
|
||||
* `localStorage` writes only emit `storage` events in *other* windows; the
|
||||
* window doing the write never gets notified. That's a problem for code
|
||||
* that mutates AI storage outside of `useAIState`'s setters (e.g. sync
|
||||
* apply): without a manual nudge, mounted components keep showing stale
|
||||
* AI state until reload.
|
||||
*
|
||||
* Both the dispatcher and `useAIState`'s listener live here so non-React
|
||||
* call sites (sync, IPC handlers, etc.) can fire the event without
|
||||
* pulling in the hook.
|
||||
*/
|
||||
|
||||
export const AI_STATE_CHANGED_EVENT = 'netcatty:ai-state-changed';
|
||||
|
||||
export function emitAIStateChanged(key: string): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.dispatchEvent(new CustomEvent<{ key: string }>(AI_STATE_CHANGED_EVENT, { detail: { key } }));
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { localStorageAdapter } from '../../infrastructure/persistence/localStorageAdapter';
|
||||
import {
|
||||
STORAGE_KEY_AI_ACTIVE_SESSION_MAP,
|
||||
STORAGE_KEY_AI_SESSIONS,
|
||||
} from '../../infrastructure/config/storageKeys';
|
||||
import type {
|
||||
AIDraft,
|
||||
AIPanelView,
|
||||
AISession,
|
||||
AIPermissionMode,
|
||||
AIToolIntegrationMode,
|
||||
} from '../../infrastructure/ai/types';
|
||||
import {
|
||||
bumpDraftMutationVersionState,
|
||||
bumpDraftUploadGenerationState,
|
||||
getDraftUploadGenerationState,
|
||||
} from './aiDraftState';
|
||||
import {
|
||||
pruneInactiveScopedSessions,
|
||||
pruneInactiveScopedTransientState,
|
||||
} from './aiScopeCleanup';
|
||||
import { emitAIStateChanged } from './aiStateEvents';
|
||||
|
||||
/** Typed accessor for the Electron IPC bridge exposed on `window.netcatty`. */
|
||||
export interface AIBridge {
|
||||
aiSdkAgentCleanup?: (chatSessionId: string) => Promise<{ ok: boolean }>;
|
||||
aiMcpSetPermissionMode?: (mode: AIPermissionMode) => Promise<unknown> | unknown;
|
||||
aiMcpSetToolIntegrationMode?: (mode: AIToolIntegrationMode) => Promise<unknown> | unknown;
|
||||
aiMcpSetCommandBlocklist?: (blocklist: string[]) => Promise<unknown> | unknown;
|
||||
aiMcpSetCommandTimeout?: (timeout: number) => Promise<unknown> | unknown;
|
||||
aiMcpSetMaxIterations?: (maxIterations: number) => Promise<unknown> | unknown;
|
||||
}
|
||||
|
||||
export function getAIBridge() {
|
||||
return (window as unknown as { netcatty?: AIBridge }).netcatty;
|
||||
}
|
||||
|
||||
|
||||
export const AI_STATE_CHANGED_DRAFTS_BY_SCOPE = 'netcatty:ai-drafts-by-scope';
|
||||
export const AI_STATE_CHANGED_PANEL_VIEW_BY_SCOPE = 'netcatty:ai-panel-view-by-scope';
|
||||
|
||||
export type DraftsByScope = Partial<Record<string, AIDraft>>;
|
||||
export type PanelViewByScope = Partial<Record<string, AIPanelView>>;
|
||||
|
||||
export function cleanupSdkAgentSessions(sessionIds: string[]) {
|
||||
const bridge = getAIBridge();
|
||||
if (!bridge?.aiSdkAgentCleanup || sessionIds.length === 0) return;
|
||||
for (const sessionId of sessionIds) {
|
||||
void bridge.aiSdkAgentCleanup(sessionId).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function isScopeKeyActive(scopeKey: string, activeTargetIds: Set<string>) {
|
||||
const separatorIndex = scopeKey.indexOf(':');
|
||||
if (separatorIndex === -1) return true;
|
||||
|
||||
const targetId = scopeKey.slice(separatorIndex + 1);
|
||||
if (!targetId) return true;
|
||||
|
||||
return activeTargetIds.has(targetId);
|
||||
}
|
||||
|
||||
export function cleanupOrphanedAISessions(activeTargetIds: Set<string>) {
|
||||
const currentSessions = latestAISessionsSnapshot
|
||||
?? localStorageAdapter.read<AISession[]>(STORAGE_KEY_AI_SESSIONS)
|
||||
?? [];
|
||||
|
||||
// Sessions shown by a still-live scope must be protected from cleanup
|
||||
// even when their own `scope.targetId` points at a closed terminal —
|
||||
// history can be resumed into a different terminal and we must not
|
||||
// delete it outright while it's actively being used.
|
||||
const preCleanupActiveSessionMap = latestAIActiveSessionMapSnapshot
|
||||
?? localStorageAdapter.read<Record<string, string | null>>(STORAGE_KEY_AI_ACTIVE_SESSION_MAP)
|
||||
?? {};
|
||||
const activeSessionIds = new Set<string>();
|
||||
for (const [scopeKey, sessionId] of Object.entries(preCleanupActiveSessionMap)) {
|
||||
if (!sessionId) continue;
|
||||
if (!isScopeKeyActive(scopeKey, activeTargetIds)) continue;
|
||||
activeSessionIds.add(sessionId);
|
||||
}
|
||||
|
||||
const nextSessionCleanup = pruneInactiveScopedSessions(
|
||||
currentSessions,
|
||||
activeTargetIds,
|
||||
activeSessionIds,
|
||||
);
|
||||
|
||||
if (nextSessionCleanup.orphanedSessionIds.length > 0) {
|
||||
cleanupSdkAgentSessions(nextSessionCleanup.orphanedSessionIds);
|
||||
}
|
||||
|
||||
if (nextSessionCleanup.sessions !== currentSessions) {
|
||||
setLatestAISessionsSnapshot(nextSessionCleanup.sessions);
|
||||
localStorageAdapter.write(
|
||||
STORAGE_KEY_AI_SESSIONS,
|
||||
pruneSessionsForStorage(nextSessionCleanup.sessions),
|
||||
);
|
||||
emitAIStateChanged(STORAGE_KEY_AI_SESSIONS);
|
||||
}
|
||||
|
||||
const activeSessionIdMap = preCleanupActiveSessionMap;
|
||||
let activeSessionMapChanged = false;
|
||||
const nextActiveSessionIdMap = { ...activeSessionIdMap };
|
||||
|
||||
for (const scopeKey of Object.keys(activeSessionIdMap)) {
|
||||
if (isScopeKeyActive(scopeKey, activeTargetIds)) continue;
|
||||
delete nextActiveSessionIdMap[scopeKey];
|
||||
activeSessionMapChanged = true;
|
||||
}
|
||||
|
||||
if (activeSessionMapChanged) {
|
||||
setLatestAIActiveSessionMapSnapshot(nextActiveSessionIdMap);
|
||||
localStorageAdapter.write(STORAGE_KEY_AI_ACTIVE_SESSION_MAP, nextActiveSessionIdMap);
|
||||
emitAIStateChanged(STORAGE_KEY_AI_ACTIVE_SESSION_MAP);
|
||||
}
|
||||
|
||||
const currentActiveSessionIdMap = activeSessionMapChanged
|
||||
? nextActiveSessionIdMap
|
||||
: activeSessionIdMap;
|
||||
const currentDraftsByScope = latestAIDraftsByScopeSnapshot ?? {};
|
||||
const currentPanelViewByScope = latestAIPanelViewByScopeSnapshot ?? {};
|
||||
const prunedScopedTransientState = pruneInactiveScopedTransientState(
|
||||
currentActiveSessionIdMap,
|
||||
currentDraftsByScope,
|
||||
currentPanelViewByScope,
|
||||
activeTargetIds,
|
||||
);
|
||||
|
||||
if (prunedScopedTransientState.activeSessionIdMap !== currentActiveSessionIdMap) {
|
||||
setLatestAIActiveSessionMapSnapshot(prunedScopedTransientState.activeSessionIdMap);
|
||||
localStorageAdapter.write(
|
||||
STORAGE_KEY_AI_ACTIVE_SESSION_MAP,
|
||||
prunedScopedTransientState.activeSessionIdMap,
|
||||
);
|
||||
emitAIStateChanged(STORAGE_KEY_AI_ACTIVE_SESSION_MAP);
|
||||
}
|
||||
|
||||
if (prunedScopedTransientState.draftsByScope !== currentDraftsByScope) {
|
||||
for (const scopeKey of Object.keys(currentDraftsByScope)) {
|
||||
if (scopeKey in prunedScopedTransientState.draftsByScope) continue;
|
||||
bumpDraftMutationVersion(scopeKey);
|
||||
bumpDraftUploadGeneration(scopeKey);
|
||||
}
|
||||
setLatestAIDraftsByScopeSnapshot(prunedScopedTransientState.draftsByScope);
|
||||
emitAIStateChanged(AI_STATE_CHANGED_DRAFTS_BY_SCOPE);
|
||||
}
|
||||
|
||||
if (prunedScopedTransientState.panelViewByScope !== currentPanelViewByScope) {
|
||||
for (const scopeKey of Object.keys(currentPanelViewByScope)) {
|
||||
if (scopeKey in prunedScopedTransientState.panelViewByScope) continue;
|
||||
bumpDraftMutationVersion(scopeKey);
|
||||
}
|
||||
setLatestAIPanelViewByScopeSnapshot(prunedScopedTransientState.panelViewByScope);
|
||||
emitAIStateChanged(AI_STATE_CHANGED_PANEL_VIEW_BY_SCOPE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Maximum number of sessions to keep in localStorage. */
|
||||
const MAX_STORED_SESSIONS = 50;
|
||||
/** Maximum number of messages per session when persisting to localStorage. */
|
||||
const MAX_SESSION_MESSAGES = 200;
|
||||
|
||||
/**
|
||||
* Prune sessions before writing to localStorage to prevent hitting the
|
||||
* ~5-10 MB storage quota. Only affects what is persisted — the in-memory
|
||||
* state retains all messages until the session is reloaded.
|
||||
*
|
||||
* - Keeps only the MAX_STORED_SESSIONS most-recently-updated sessions.
|
||||
* - Trims each session's messages to the last MAX_SESSION_MESSAGES.
|
||||
*/
|
||||
export function pruneSessionsForStorage(sessions: AISession[]): AISession[] {
|
||||
// Sort by updatedAt descending so we keep the newest
|
||||
const sorted = [...sessions].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
const limited = sorted.slice(0, MAX_STORED_SESSIONS);
|
||||
return limited.map(s => {
|
||||
if (s.messages.length > MAX_SESSION_MESSAGES) {
|
||||
return { ...s, messages: s.messages.slice(-MAX_SESSION_MESSAGES) };
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
export let latestAISessionsSnapshot: AISession[] | null = null;
|
||||
export let latestAIActiveSessionMapSnapshot: Record<string, string | null> | null = null;
|
||||
export let latestAIDraftsByScopeSnapshot: DraftsByScope | null = null;
|
||||
export let latestAIPanelViewByScopeSnapshot: PanelViewByScope | null = null;
|
||||
let latestAIDraftMutationVersionByScopeSnapshot: Record<string, number> = {};
|
||||
let latestAIDraftUploadGenerationByScopeSnapshot: Record<string, number> = {};
|
||||
|
||||
export function setLatestAISessionsSnapshot(sessions: AISession[]) {
|
||||
latestAISessionsSnapshot = sessions;
|
||||
}
|
||||
|
||||
export function setLatestAIActiveSessionMapSnapshot(activeSessionIdMap: Record<string, string | null>) {
|
||||
latestAIActiveSessionMapSnapshot = activeSessionIdMap;
|
||||
}
|
||||
|
||||
export function prewarmAIStateStorageSnapshots() {
|
||||
try {
|
||||
if (latestAISessionsSnapshot === null) {
|
||||
latestAISessionsSnapshot =
|
||||
localStorageAdapter.read<AISession[]>(STORAGE_KEY_AI_SESSIONS) ?? [];
|
||||
}
|
||||
if (latestAIActiveSessionMapSnapshot === null) {
|
||||
latestAIActiveSessionMapSnapshot =
|
||||
localStorageAdapter.read<Record<string, string | null>>(STORAGE_KEY_AI_ACTIVE_SESSION_MAP) ?? {};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[AIState] Failed to prewarm AI state storage snapshots:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export function setLatestAIDraftsByScopeSnapshot(draftsByScope: DraftsByScope) {
|
||||
latestAIDraftsByScopeSnapshot = draftsByScope;
|
||||
}
|
||||
|
||||
export function setLatestAIPanelViewByScopeSnapshot(panelViewByScope: PanelViewByScope) {
|
||||
latestAIPanelViewByScopeSnapshot = panelViewByScope;
|
||||
}
|
||||
|
||||
export function bumpDraftMutationVersion(scopeKey: string) {
|
||||
latestAIDraftMutationVersionByScopeSnapshot = bumpDraftMutationVersionState(
|
||||
latestAIDraftMutationVersionByScopeSnapshot,
|
||||
scopeKey,
|
||||
);
|
||||
}
|
||||
|
||||
export function getDraftUploadGeneration(scopeKey: string) {
|
||||
return getDraftUploadGenerationState(
|
||||
latestAIDraftUploadGenerationByScopeSnapshot,
|
||||
scopeKey,
|
||||
);
|
||||
}
|
||||
|
||||
export function bumpDraftUploadGeneration(scopeKey: string) {
|
||||
latestAIDraftUploadGenerationByScopeSnapshot = bumpDraftUploadGenerationState(
|
||||
latestAIDraftUploadGenerationByScopeSnapshot,
|
||||
scopeKey,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { resolveAutoSyncHashDecision } from "./autoSyncHashDecision.ts";
|
||||
|
||||
test("remote-applied data is skipped only when the current hash still matches it", () => {
|
||||
assert.equal(
|
||||
resolveAutoSyncHashDecision({
|
||||
currentHash: "remote-applied",
|
||||
lastSyncedHash: "old-local",
|
||||
appliedSkipHash: "remote-applied",
|
||||
}),
|
||||
"skip-applied",
|
||||
);
|
||||
});
|
||||
|
||||
test("user edits after remote apply still sync even when they match the old baseline", () => {
|
||||
assert.equal(
|
||||
resolveAutoSyncHashDecision({
|
||||
currentHash: "old-local",
|
||||
lastSyncedHash: "old-local",
|
||||
appliedSkipHash: "remote-applied",
|
||||
}),
|
||||
"sync",
|
||||
);
|
||||
});
|
||||
|
||||
test("unchanged data is skipped only when there is no pending remote-apply hash", () => {
|
||||
assert.equal(
|
||||
resolveAutoSyncHashDecision({
|
||||
currentHash: "same",
|
||||
lastSyncedHash: "same",
|
||||
appliedSkipHash: null,
|
||||
}),
|
||||
"unchanged",
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
export type AutoSyncHashDecision = 'skip-applied' | 'unchanged' | 'sync';
|
||||
|
||||
export function resolveAutoSyncHashDecision({
|
||||
currentHash,
|
||||
lastSyncedHash,
|
||||
appliedSkipHash,
|
||||
}: {
|
||||
currentHash: string;
|
||||
lastSyncedHash: string;
|
||||
appliedSkipHash: string | null;
|
||||
}): AutoSyncHashDecision {
|
||||
if (appliedSkipHash !== null) {
|
||||
return currentHash === appliedSkipHash ? 'skip-applied' : 'sync';
|
||||
}
|
||||
return currentHash === lastSyncedHash ? 'unchanged' : 'sync';
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import {
|
||||
getRuntimeRemoteCheckIntervalMs,
|
||||
shouldRunRuntimeRemoteCheck,
|
||||
} from './autoSyncRemoteSchedule';
|
||||
|
||||
test("runtime remote checks wait for the startup check to finish", () => {
|
||||
assert.equal(
|
||||
shouldRunRuntimeRemoteCheck({
|
||||
hasAnyConnectedProvider: true,
|
||||
autoSyncEnabled: true,
|
||||
isUnlocked: true,
|
||||
startupRemoteCheckDone: false,
|
||||
isSyncing: false,
|
||||
isSyncRunning: false,
|
||||
remoteCheckInFlight: false,
|
||||
now: 10_000,
|
||||
lastRemoteCheckAt: null,
|
||||
minIntervalMs: 30_000,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("runtime remote checks run immediately after startup gate opens", () => {
|
||||
assert.equal(
|
||||
shouldRunRuntimeRemoteCheck({
|
||||
hasAnyConnectedProvider: true,
|
||||
autoSyncEnabled: true,
|
||||
isUnlocked: true,
|
||||
startupRemoteCheckDone: true,
|
||||
isSyncing: false,
|
||||
isSyncRunning: false,
|
||||
remoteCheckInFlight: false,
|
||||
now: 10_000,
|
||||
lastRemoteCheckAt: null,
|
||||
minIntervalMs: 30_000,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("runtime remote checks respect the minimum interval", () => {
|
||||
const common = {
|
||||
hasAnyConnectedProvider: true,
|
||||
autoSyncEnabled: true,
|
||||
isUnlocked: true,
|
||||
startupRemoteCheckDone: true,
|
||||
isSyncing: false,
|
||||
isSyncRunning: false,
|
||||
remoteCheckInFlight: false,
|
||||
minIntervalMs: 30_000,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
shouldRunRuntimeRemoteCheck({
|
||||
...common,
|
||||
now: 35_000,
|
||||
lastRemoteCheckAt: 10_000,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
shouldRunRuntimeRemoteCheck({
|
||||
...common,
|
||||
now: 40_000,
|
||||
lastRemoteCheckAt: 10_000,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("forced runtime remote checks bypass only the interval gate", () => {
|
||||
const common = {
|
||||
hasAnyConnectedProvider: true,
|
||||
autoSyncEnabled: true,
|
||||
isUnlocked: true,
|
||||
startupRemoteCheckDone: true,
|
||||
isSyncing: false,
|
||||
isSyncRunning: false,
|
||||
remoteCheckInFlight: false,
|
||||
minIntervalMs: 30_000,
|
||||
force: true,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
shouldRunRuntimeRemoteCheck({
|
||||
...common,
|
||||
now: 35_000,
|
||||
lastRemoteCheckAt: 10_000,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldRunRuntimeRemoteCheck({
|
||||
...common,
|
||||
isSyncing: true,
|
||||
now: 35_000,
|
||||
lastRemoteCheckAt: 10_000,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("configured auto-sync intervals map to bounded remote recheck intervals", () => {
|
||||
assert.equal(getRuntimeRemoteCheckIntervalMs(1), 30_000);
|
||||
assert.equal(getRuntimeRemoteCheckIntervalMs(10), 300_000);
|
||||
assert.equal(getRuntimeRemoteCheckIntervalMs(120), 300_000);
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
const MIN_RUNTIME_REMOTE_CHECK_MS = 30_000;
|
||||
const MAX_RUNTIME_REMOTE_CHECK_MS = 5 * 60_000;
|
||||
|
||||
export function getRuntimeRemoteCheckIntervalMs(autoSyncIntervalMinutes: number): number {
|
||||
const configuredMs = Math.max(1, Number(autoSyncIntervalMinutes) || 1) * 60_000;
|
||||
return Math.max(
|
||||
MIN_RUNTIME_REMOTE_CHECK_MS,
|
||||
Math.min(MAX_RUNTIME_REMOTE_CHECK_MS, Math.floor(configuredMs / 2)),
|
||||
);
|
||||
}
|
||||
|
||||
export interface RuntimeRemoteCheckInput {
|
||||
hasAnyConnectedProvider: boolean;
|
||||
autoSyncEnabled: boolean;
|
||||
isUnlocked: boolean;
|
||||
startupRemoteCheckDone: boolean;
|
||||
isSyncing: boolean;
|
||||
isSyncRunning: boolean;
|
||||
remoteCheckInFlight: boolean;
|
||||
force?: boolean;
|
||||
now: number;
|
||||
lastRemoteCheckAt: number | null;
|
||||
minIntervalMs: number;
|
||||
}
|
||||
|
||||
export function shouldRunRuntimeRemoteCheck(input: RuntimeRemoteCheckInput): boolean {
|
||||
if (!input.hasAnyConnectedProvider) return false;
|
||||
if (!input.autoSyncEnabled) return false;
|
||||
if (!input.isUnlocked) return false;
|
||||
if (!input.startupRemoteCheckDone) return false;
|
||||
if (input.isSyncing || input.isSyncRunning || input.remoteCheckInFlight) return false;
|
||||
if (input.force === true) return true;
|
||||
if (input.lastRemoteCheckAt == null) return true;
|
||||
return input.now - input.lastRemoteCheckAt >= input.minIntervalMs;
|
||||
}
|
||||
@@ -142,14 +142,6 @@ export const customThemeStore = new CustomThemeStore();
|
||||
|
||||
// ============== Hooks ==============
|
||||
|
||||
/** Get all themes (built-in + custom) */
|
||||
export const useAllThemes = (): TerminalTheme[] => {
|
||||
return useSyncExternalStore(
|
||||
customThemeStore.subscribe,
|
||||
customThemeStore.getAllThemes
|
||||
);
|
||||
};
|
||||
|
||||
/** Get custom themes only */
|
||||
export const useCustomThemes = (): TerminalTheme[] => {
|
||||
return useSyncExternalStore(
|
||||
@@ -158,12 +150,6 @@ export const useCustomThemes = (): TerminalTheme[] => {
|
||||
);
|
||||
};
|
||||
|
||||
/** Get theme by ID (built-in or custom) with fallback */
|
||||
export const useThemeById = (id: string): TerminalTheme => {
|
||||
const allThemes = useAllThemes();
|
||||
return allThemes.find(t => t.id === id) || TERMINAL_THEMES[0];
|
||||
};
|
||||
|
||||
/** Theme mutation actions */
|
||||
export const useCustomThemeActions = () => {
|
||||
const addTheme = useCallback((theme: TerminalTheme) => {
|
||||
|
||||
@@ -238,9 +238,9 @@ export const editorTabStore = new EditorTabStore();
|
||||
const getTabsSnapshot = () => editorTabStore.getTabs();
|
||||
|
||||
export const useEditorTabs = (): readonly EditorTab[] =>
|
||||
useSyncExternalStore(editorTabStore.subscribe, getTabsSnapshot);
|
||||
useSyncExternalStore(editorTabStore.subscribe, getTabsSnapshot, getTabsSnapshot);
|
||||
|
||||
export const useEditorTab = (id: EditorTabId): EditorTab | undefined => {
|
||||
const getSnapshot = useCallback(() => editorTabStore.getTab(id), [id]);
|
||||
return useSyncExternalStore(editorTabStore.subscribe, getSnapshot);
|
||||
return useSyncExternalStore(editorTabStore.subscribe, getSnapshot, getSnapshot);
|
||||
};
|
||||
@@ -134,24 +134,6 @@ export const useAvailableFonts = (): TerminalFont[] => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get font loading state
|
||||
*/
|
||||
export const useFontsLoading = (): boolean => {
|
||||
return useSyncExternalStore(
|
||||
fontStore.subscribe,
|
||||
fontStore.getIsLoading
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get font by ID with fallback - useful for components that need a specific font
|
||||
*/
|
||||
export const useFontById = (fontId: string): TerminalFont => {
|
||||
const fonts = useAvailableFonts();
|
||||
return fonts.find(f => f.id === fontId) || fonts[0] || TERMINAL_FONTS[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize fonts eagerly (call at app startup)
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
class HostTreeInlineGroupDeleteStore {
|
||||
private targetPath: string | null = null;
|
||||
private listeners = new Set<Listener>();
|
||||
|
||||
getTargetPath = () => this.targetPath;
|
||||
|
||||
open = (groupPath: string) => {
|
||||
this.targetPath = groupPath;
|
||||
this.listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
close = () => {
|
||||
if (!this.targetPath) return;
|
||||
this.targetPath = null;
|
||||
this.listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
subscribe = (listener: Listener) => {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
export const hostTreeInlineGroupDeleteStore = new HostTreeInlineGroupDeleteStore();
|
||||
|
||||
export const useHostTreeInlineGroupDeleteTarget = () => {
|
||||
return useSyncExternalStore(
|
||||
hostTreeInlineGroupDeleteStore.subscribe,
|
||||
hostTreeInlineGroupDeleteStore.getTargetPath,
|
||||
hostTreeInlineGroupDeleteStore.getTargetPath,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
export type HostTreeInlineGroupEdit = {
|
||||
groupPath: string;
|
||||
initialName: string;
|
||||
isNew: boolean;
|
||||
shouldScrollIntoView?: boolean;
|
||||
};
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
class HostTreeInlineGroupEditStore {
|
||||
private edit: HostTreeInlineGroupEdit | null = null;
|
||||
private listeners = new Set<Listener>();
|
||||
|
||||
getEdit = () => this.edit;
|
||||
|
||||
startEdit = (edit: HostTreeInlineGroupEdit) => {
|
||||
this.edit = {
|
||||
...edit,
|
||||
shouldScrollIntoView: edit.isNew ? true : edit.shouldScrollIntoView,
|
||||
};
|
||||
this.listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
markScrollHandled = () => {
|
||||
if (!this.edit?.shouldScrollIntoView) return;
|
||||
this.edit = { ...this.edit, shouldScrollIntoView: false };
|
||||
this.listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
if (!this.edit) return;
|
||||
this.edit = null;
|
||||
this.listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
subscribe = (listener: Listener) => {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
export const hostTreeInlineGroupEditStore = new HostTreeInlineGroupEditStore();
|
||||
|
||||
export const useHostTreeInlineGroupEdit = () => {
|
||||
return useSyncExternalStore(
|
||||
hostTreeInlineGroupEditStore.subscribe,
|
||||
hostTreeInlineGroupEditStore.getEdit,
|
||||
hostTreeInlineGroupEditStore.getEdit,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
export type HostTreeInlineHostEdit = {
|
||||
hostId: string;
|
||||
initialName: string;
|
||||
};
|
||||
|
||||
type Listener = () => void;
|
||||
|
||||
class HostTreeInlineHostEditStore {
|
||||
private edit: HostTreeInlineHostEdit | null = null;
|
||||
private listeners = new Set<Listener>();
|
||||
|
||||
getEdit = () => this.edit;
|
||||
|
||||
startEdit = (edit: HostTreeInlineHostEdit) => {
|
||||
this.edit = edit;
|
||||
this.listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
if (!this.edit) return;
|
||||
this.edit = null;
|
||||
this.listeners.forEach((listener) => listener());
|
||||
};
|
||||
|
||||
subscribe = (listener: Listener) => {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
export const hostTreeInlineHostEditStore = new HostTreeInlineHostEditStore();
|
||||
|
||||
export const useHostTreeInlineHostEdit = () => {
|
||||
return useSyncExternalStore(
|
||||
hostTreeInlineHostEditStore.subscribe,
|
||||
hostTreeInlineHostEditStore.getEdit,
|
||||
hostTreeInlineHostEditStore.getEdit,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ConnectionLog } from "../../domain/models";
|
||||
|
||||
export interface LogView {
|
||||
id: string;
|
||||
connectionLogId: string;
|
||||
log: ConnectionLog;
|
||||
}
|
||||
|
||||
export const getLogViewTabId = (log: Pick<ConnectionLog, "id">): string => `log-${log.id}`;
|
||||
|
||||
export const addLogView = (views: LogView[], log: ConnectionLog): LogView[] => {
|
||||
if (views.some((view) => view.connectionLogId === log.id)) return views;
|
||||
return [
|
||||
...views,
|
||||
{
|
||||
id: getLogViewTabId(log),
|
||||
connectionLogId: log.id,
|
||||
log,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const removeLogView = (views: LogView[], logViewId: string): LogView[] =>
|
||||
views.filter((view) => view.id !== logViewId);
|
||||
Loaded 100 of 1739 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user