Compare commits
616 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2650e5cf7c | ||
|
|
da1fa3e8ed | ||
|
|
e256314d4f | ||
|
|
f8f7d5955f | ||
|
|
fa6f68fec3 | ||
|
|
0705e8a65a | ||
|
|
9d9bbd3821 | ||
|
|
86a312ea6b | ||
|
|
e4771e795b | ||
|
|
1504830142 | ||
|
|
7e99bc7aad | ||
|
|
abca8cb26d | ||
|
|
d032088415 | ||
|
|
80fb26ad48 | ||
|
|
017db589d4 | ||
|
|
6e1b842bba | ||
|
|
0c04ea3a33 | ||
|
|
c21ac1aea6 | ||
|
|
864e0651b1 | ||
|
|
65d328eb38 | ||
|
|
731d360323 | ||
|
|
c4ccdba268 | ||
|
|
4f00492e49 | ||
|
|
abcf2baad6 | ||
|
|
49a7698993 | ||
|
|
8d2548acaf | ||
|
|
251deb5886 | ||
|
|
7a15bdeadc | ||
|
|
1e59d57764 | ||
|
|
12b3768598 | ||
|
|
3abe5b98d0 | ||
|
|
ad004105c3 | ||
|
|
f70266197e | ||
|
|
cc31a8004a | ||
|
|
fa14851596 | ||
|
|
d56c46e944 | ||
|
|
9f8bcf1fe1 | ||
|
|
e50387a796 | ||
|
|
3d2eac8772 | ||
|
|
343f529cac | ||
|
|
3bfa12b61c | ||
|
|
79bd776ef9 | ||
|
|
222428ad47 | ||
|
|
4b3853dd22 | ||
|
|
9dd191902c | ||
|
|
3f524ad674 | ||
|
|
7e6376fcb7 | ||
|
|
6f35013faf | ||
|
|
e71acdaaa9 | ||
|
|
fd7c663282 | ||
|
|
89b2bbe9ac | ||
|
|
7eb64a463b | ||
|
|
8971a28abc | ||
|
|
2ff989429f | ||
|
|
24369e2581 | ||
|
|
2bb8a5182c | ||
|
|
629bf9461a | ||
|
|
a56fb6c8d6 | ||
|
|
efb3529c92 | ||
|
|
a372a4173c | ||
|
|
5e46832548 | ||
|
|
91869c42e1 | ||
|
|
d421748bed | ||
|
|
7e5cd7e5a6 | ||
|
|
2b910b2c47 | ||
|
|
814ce4ca11 | ||
|
|
1e63fd1e19 | ||
|
|
4b19902e5c | ||
|
|
fd014a1d34 | ||
|
|
fd91bcf603 | ||
|
|
61a4a8f920 | ||
|
|
ed4275a18b | ||
|
|
7481d65e1e | ||
|
|
0c49cf1af9 | ||
|
|
7f04000739 | ||
|
|
e3da9824b6 | ||
|
|
34370345cd | ||
|
|
6c1a4e851c | ||
|
|
766e3ce7f9 | ||
|
|
30c2cca2e1 | ||
|
|
af3241b773 | ||
|
|
eca0a63273 | ||
|
|
f15cdb38d6 | ||
|
|
5b8577aaa7 | ||
|
|
0a4a2b46c1 | ||
|
|
53d441b3f5 | ||
|
|
d4c346e40a | ||
|
|
2dcacf71e4 | ||
|
|
9ded6446a7 | ||
|
|
0a6f4a9f02 | ||
|
|
635e588bcc | ||
|
|
7343db78a5 | ||
|
|
ccd457c992 | ||
|
|
97676f114e | ||
|
|
e83c1eb017 | ||
|
|
e417c0106a | ||
|
|
3c09840d35 | ||
|
|
7361a94f8c | ||
|
|
a9bffe3913 | ||
|
|
5322555eba | ||
|
|
172dce2867 | ||
|
|
5735fee36e | ||
|
|
91642d8784 | ||
|
|
9d8f3f4211 | ||
|
|
66d39da80a | ||
|
|
fbd4a31a9c | ||
|
|
3d7e03ddaf | ||
|
|
1f213bf257 | ||
|
|
b38f079611 | ||
|
|
21e639cacd | ||
|
|
bdaf665b7c | ||
|
|
61a515c1d2 | ||
|
|
1b646df908 | ||
|
|
5550f939b2 | ||
|
|
b34fb5a600 | ||
|
|
c0dce5c0b1 | ||
|
|
d56bd2920f | ||
|
|
48ad100a64 | ||
|
|
ef07a172a9 | ||
|
|
f492d47719 | ||
|
|
ac8c07deb4 | ||
|
|
ca48ab639e | ||
|
|
7c5232c1a1 | ||
|
|
4fac7fdfe1 | ||
|
|
f7fc9560d5 | ||
|
|
f7ba744e7f | ||
|
|
315164f142 | ||
|
|
429cab5223 | ||
|
|
deecbc874b | ||
|
|
504f4cafa0 | ||
|
|
6d327d17af | ||
|
|
74290eb52b | ||
|
|
60b9653fd3 | ||
|
|
53e32d3031 | ||
|
|
ed279cf8a1 | ||
|
|
296b898bda | ||
|
|
2325155b1e | ||
|
|
b6ff4aae6a | ||
|
|
f04002fdb8 | ||
|
|
b8cb254f56 | ||
|
|
3983477904 | ||
|
|
f011f5ae38 | ||
|
|
18ecab18df | ||
|
|
793c481221 | ||
|
|
4fee3688ea | ||
|
|
b9693436bb | ||
|
|
6b18d8f934 | ||
|
|
65c325de9a | ||
|
|
8da5aaf259 | ||
|
|
00e8fdd3e6 | ||
|
|
b2eea5d0d7 | ||
|
|
9a8e24f590 | ||
|
|
32c6d45cb0 | ||
|
|
74ce6f2f1f | ||
|
|
573865cf10 | ||
|
|
56d4733e2a | ||
|
|
a8965a01e3 | ||
|
|
beef51ef38 | ||
|
|
b5b3ee8709 | ||
|
|
4f1e01dde0 | ||
|
|
d42ff51de5 | ||
|
|
c39861b7b7 | ||
|
|
d39b9fd73e | ||
|
|
ecab4ab634 | ||
|
|
5e67e15842 | ||
|
|
2af1a8b72c | ||
|
|
2510ed0ebb | ||
|
|
6827985289 | ||
|
|
a095a2c01c | ||
|
|
2033ff6777 | ||
|
|
0c22288833 | ||
|
|
0576150067 | ||
|
|
9cdcf616f7 | ||
|
|
2de10364f3 | ||
|
|
562559a1b0 | ||
|
|
0f0b7313bb | ||
|
|
412fc87d1e | ||
|
|
0a1abab475 | ||
|
|
b63ef8c1aa | ||
|
|
c38a3d439d | ||
|
|
dd395e668f | ||
|
|
8a5ef441f9 | ||
|
|
d9acbe865f | ||
|
|
dc35fef873 | ||
|
|
4e9293ae0f | ||
|
|
36973b8693 | ||
|
|
0ab734d1a5 | ||
|
|
bfce9b525a | ||
|
|
f19b6ef02f | ||
|
|
1992908a85 | ||
|
|
a6bcafa8f6 | ||
|
|
eabcb06eeb | ||
|
|
21dcbfa4c4 | ||
|
|
b6c074a242 | ||
|
|
f6d095d533 | ||
|
|
7a36251d3f | ||
|
|
3fda4d0da9 | ||
|
|
2fa8917d5e | ||
|
|
67149af64b | ||
|
|
0104cb9f29 | ||
|
|
1afe976777 | ||
|
|
d9b4399c57 | ||
|
|
ac7b3b9824 | ||
|
|
359206630d | ||
|
|
96dfee90ab | ||
|
|
9ace600fce | ||
|
|
549b945d0f | ||
|
|
4528a79c87 | ||
|
|
951ce985b5 | ||
|
|
001f04a9ee | ||
|
|
3844d2eb75 | ||
|
|
e593221e02 | ||
|
|
8cc3801dc5 | ||
|
|
251e57ec61 | ||
|
|
769a4f00aa | ||
|
|
9bafc937d5 | ||
|
|
2d0ea09e06 | ||
|
|
aeec5e361c | ||
|
|
71b2d62c9f | ||
|
|
40b3072e5f | ||
|
|
cae9338274 | ||
|
|
4c2781b3b6 | ||
|
|
b2b5bef9f5 | ||
|
|
df8c96569a | ||
|
|
e562f0b851 | ||
|
|
7b2b48f0d1 | ||
|
|
c353c88db8 | ||
|
|
171dbb7509 | ||
|
|
65e8fabe7d | ||
|
|
389f0b6f82 | ||
|
|
039566ded5 | ||
|
|
d18b31692b | ||
|
|
c993c15c92 | ||
|
|
3c5ffc045f | ||
|
|
261bb7aa6f | ||
|
|
96a7a41759 | ||
|
|
d563521eb1 | ||
|
|
a08c42db8b | ||
|
|
8e026238ae | ||
|
|
7412b3a5c8 | ||
|
|
b60b770ed6 | ||
|
|
20c4d6f6eb | ||
|
|
2437c75d75 | ||
|
|
867c2209b1 | ||
|
|
fffa448425 | ||
|
|
b1142b88f1 | ||
|
|
6d95e7debc | ||
|
|
6bafcb0ec0 | ||
|
|
4935abcf33 | ||
|
|
14f74b76bb | ||
|
|
6b9a1a49bb | ||
|
|
533a0e2d5b | ||
|
|
393f1a29d5 | ||
|
|
1dabbfc4de | ||
|
|
7665f8c260 | ||
|
|
eef5e25a00 | ||
|
|
261f29c185 | ||
|
|
2a46939aa5 | ||
|
|
779c9fc850 | ||
|
|
a20a06320d | ||
|
|
943a9e86f0 | ||
|
|
563242c5f1 | ||
|
|
d39a016d5f | ||
|
|
fa87d87011 | ||
|
|
7dc847fca2 | ||
|
|
343edcdbad | ||
|
|
b631703aa6 | ||
|
|
6dd6b73c2f | ||
|
|
2b496bda31 | ||
|
|
17c8d198c3 | ||
|
|
86f8d9694d | ||
|
|
3948cb74ca | ||
|
|
4ebced1e71 | ||
|
|
d4e58fc925 | ||
|
|
2bfb27f346 | ||
|
|
c4fba1c905 | ||
|
|
4a5e6c2a23 | ||
|
|
5fb7157f57 | ||
|
|
a8c38d2a00 | ||
|
|
cbc82fff64 | ||
|
|
5c44ba1da8 | ||
|
|
fd2f0e513b | ||
|
|
7fd2a0f187 | ||
|
|
fd355eeeab | ||
|
|
c93e370370 | ||
|
|
57d218a17f | ||
|
|
ffc43a67f2 | ||
|
|
7deb5b885a | ||
|
|
5d5e23482f | ||
|
|
36c1e40d64 | ||
|
|
c6f4fe2b7b | ||
|
|
1a23627193 | ||
|
|
d73b2377bf | ||
|
|
4559ab7ec2 | ||
|
|
1d9679e516 | ||
|
|
4c30f6b012 | ||
|
|
511210939f | ||
|
|
d2addf58cb | ||
|
|
f7db410235 | ||
|
|
dd18b04cea | ||
|
|
58d8009e91 | ||
|
|
663407b95d | ||
|
|
f78901603e | ||
|
|
5e1101baeb | ||
|
|
2ba2441900 | ||
|
|
4446ae3dbd | ||
|
|
72668f0386 | ||
|
|
a9b858ec6f | ||
|
|
e1f902c203 | ||
|
|
be6e34ba52 | ||
|
|
39b3b00117 | ||
|
|
763019f0c5 | ||
|
|
d743271be8 | ||
|
|
992dad26aa | ||
|
|
9bd0e67474 | ||
|
|
5767a4afb2 | ||
|
|
9e4c510684 | ||
|
|
16607fb069 | ||
|
|
e047a06432 | ||
|
|
9e09fd898a | ||
|
|
799c32a871 | ||
|
|
483f33b5c9 | ||
|
|
d444fd4fba | ||
|
|
0aae93ba2e | ||
|
|
9608bea3bf | ||
|
|
a038a1ecdc | ||
|
|
c82cdd7f8f | ||
|
|
0c6d5c3c61 | ||
|
|
900426f359 | ||
|
|
1d760fc93a | ||
|
|
61571e0f61 | ||
|
|
c9eb423c89 | ||
|
|
45b294a121 | ||
|
|
3a3f1fabe1 | ||
|
|
03177a09b3 | ||
|
|
cae391f62b | ||
|
|
2a5e9db079 | ||
|
|
650d6e8b41 | ||
|
|
1daf134b31 | ||
|
|
e1dfa35c6c | ||
|
|
73f80692d3 | ||
|
|
42c7dae495 | ||
|
|
b2a1309caa | ||
|
|
94bf5f9580 | ||
|
|
704ebdc9d7 | ||
|
|
165da4e559 | ||
|
|
d3e3b484bf | ||
|
|
192f8faa5b | ||
|
|
579d5cb0a3 | ||
|
|
07fca5b9af | ||
|
|
866a63ab6c | ||
|
|
97b4935bc4 | ||
|
|
30129abef3 | ||
|
|
24d904b32c | ||
|
|
5f0ce57ead | ||
|
|
51f58d095a | ||
|
|
d22e3838c4 | ||
|
|
adbb421b7b | ||
|
|
eaa47af269 | ||
|
|
a6cb5544f8 | ||
|
|
9e91faa660 | ||
|
|
8636fadc72 | ||
|
|
0621957592 | ||
|
|
8ec06b0c84 | ||
|
|
d47f8d7ee9 | ||
|
|
24f8959525 | ||
|
|
983740578b | ||
|
|
b5f79ed7cd | ||
|
|
bbb0e79d4e | ||
|
|
471dc05897 | ||
|
|
7a772d2459 | ||
|
|
1d92421960 | ||
|
|
aeaaf429d7 | ||
|
|
84432e98ae | ||
|
|
77c6102de7 | ||
|
|
ab5dd82169 | ||
|
|
23e7b69dc5 | ||
|
|
3dc8f393f2 | ||
|
|
8a2144f263 | ||
|
|
c1c59caa10 | ||
|
|
d27ebd90b6 | ||
|
|
467745c1e9 | ||
|
|
537378a038 | ||
|
|
4b5ed30e5b | ||
|
|
52b7f6a225 | ||
|
|
f31675d8a2 | ||
|
|
dd46a8450c | ||
|
|
b0843f7d66 | ||
|
|
daadc0195c | ||
|
|
298dec6957 | ||
|
|
bf39d85dfa | ||
|
|
30a9de25a8 | ||
|
|
af1ecf0bd4 | ||
|
|
fe55a2cd3c | ||
|
|
5a33d4e57e | ||
|
|
7f46a9023c | ||
|
|
dfd943b621 | ||
|
|
7007d0d922 | ||
|
|
601678500d | ||
|
|
9bfb504381 | ||
|
|
8a03b0cf15 | ||
|
|
11ba89de0a | ||
|
|
bac7f62eea | ||
|
|
eef90ea02b | ||
|
|
0650df534a | ||
|
|
9ef8c8b823 | ||
|
|
d7e08da0b2 | ||
|
|
ef361e0798 | ||
|
|
6855332092 | ||
|
|
121d523e02 | ||
|
|
42a375c4c7 | ||
|
|
a1dd705d97 | ||
|
|
71f90b36ca | ||
|
|
37facdc3c1 | ||
|
|
66b4f547ff | ||
|
|
d27b9c7f2d | ||
|
|
278ff9c6bc | ||
|
|
d6fe1ce9d7 | ||
|
|
0bfa5256b8 | ||
|
|
72ccfc8aec | ||
|
|
d117c5dc10 | ||
|
|
9312783f44 | ||
|
|
e5b16ebfd3 | ||
|
|
5d1d65c2d3 | ||
|
|
9ca1309cec | ||
|
|
a03afc05f5 | ||
|
|
0198963584 | ||
|
|
58e745d967 | ||
|
|
377e347d68 | ||
|
|
bac0704d3d | ||
|
|
d2ff46edf6 | ||
|
|
f908372b4e | ||
|
|
5d44ff4913 | ||
|
|
4c9aa66048 | ||
|
|
b6a09b99ab | ||
|
|
3a0dcb1a52 | ||
|
|
5015503b4c | ||
|
|
16423feea4 | ||
|
|
9703514698 | ||
|
|
de7a97fb76 | ||
|
|
319aaf8132 | ||
|
|
74bc58ba91 | ||
|
|
d622db0d7c | ||
|
|
de1ddf2362 | ||
|
|
32c0fc860b | ||
|
|
1938f432dd | ||
|
|
a5cfb0ca1d | ||
|
|
a172234fb0 | ||
|
|
63f989b31a | ||
|
|
2ae5d01d5c | ||
|
|
130f1deed1 | ||
|
|
5880d85b48 | ||
|
|
9455670e80 | ||
|
|
e369321c66 | ||
|
|
efc51b0d46 | ||
|
|
d6f3b23b88 | ||
|
|
0a4fa7b9f8 | ||
|
|
2b3e4a8d25 | ||
|
|
bf3a16f96d | ||
|
|
b416e72820 | ||
|
|
ca84bdb227 | ||
|
|
148a4e97a6 | ||
|
|
a13493ebc2 | ||
|
|
ce4ac79e5f | ||
|
|
8f76ea49e7 | ||
|
|
923d3293cd | ||
|
|
7379ff8d15 | ||
|
|
18ebec350d | ||
|
|
3b0cbc53aa | ||
|
|
f00e8ffa4d | ||
|
|
d6f7aad1c3 | ||
|
|
092ea6e836 | ||
|
|
d565e2464a | ||
|
|
2f5d875c47 | ||
|
|
fdb2ddc5f7 | ||
|
|
7a12c5315a | ||
|
|
60d788288d | ||
|
|
dc3c510d57 | ||
|
|
ec6a49f01e | ||
|
|
2b9bfbc20d | ||
|
|
06a51df834 | ||
|
|
6fa183dc56 | ||
|
|
b3cb4049ed | ||
|
|
602b51b1f5 | ||
|
|
a83039577c | ||
|
|
1c77a289a6 | ||
|
|
6278b9124d | ||
|
|
f94cafdbcc | ||
|
|
e13da2caba | ||
|
|
d833fa8dfd | ||
|
|
c921cc59b9 | ||
|
|
7a2c594324 | ||
|
|
0eeb9c2fbf | ||
|
|
ac921cd5a0 | ||
|
|
3ea14c1687 | ||
|
|
6e927473b9 | ||
|
|
1d9e9c1b7d | ||
|
|
54a6189b0c | ||
|
|
85aa9f39c2 | ||
|
|
bda83ce76e | ||
|
|
9ee4c20250 | ||
|
|
fbc70e43e3 | ||
|
|
bc4b4a2171 | ||
|
|
96f9bf6f6f | ||
|
|
9f0986536a | ||
|
|
f668aa7acd | ||
|
|
fc50f4784a | ||
|
|
1fa58cad31 | ||
|
|
469e62557c | ||
|
|
75830aaea7 | ||
|
|
61ef5df559 | ||
|
|
14b5ba9c4c | ||
|
|
9e9c56a3b4 | ||
|
|
3a79f55614 | ||
|
|
af25ee5c11 | ||
|
|
6dd581d5e2 | ||
|
|
746ec019c4 | ||
|
|
2b70f28b0b | ||
|
|
45127646e8 | ||
|
|
83e9c1dd97 | ||
|
|
2eabb7d5ac | ||
|
|
9d4c596b4b | ||
|
|
cc38ab6c45 | ||
|
|
586fa09c7b | ||
|
|
0c45bc5ea8 | ||
|
|
9d9c0633f0 | ||
|
|
47f9635b10 | ||
|
|
68088f5e17 | ||
|
|
77a37cc6df | ||
|
|
420e59bf81 | ||
|
|
dbc5135301 | ||
|
|
8c7d6bb552 | ||
|
|
2b5c1952c0 | ||
|
|
85a82618e5 | ||
|
|
0280ac34c3 | ||
|
|
439900154b | ||
|
|
4a6e902684 | ||
|
|
71bbd2e54a | ||
|
|
3083d8e147 | ||
|
|
e74883e9c2 | ||
|
|
0816a9d167 | ||
|
|
4b3e91fa84 | ||
|
|
0973a0b60e | ||
|
|
de5f61126d | ||
|
|
0c20ca761f | ||
|
|
4bce56207e | ||
|
|
dca54e0033 | ||
|
|
309646bf1d | ||
|
|
18b9961b39 | ||
|
|
1e51ff17f2 | ||
|
|
63b5f707e2 | ||
|
|
30efb6ee7a | ||
|
|
61b017618a | ||
|
|
1e0397adc9 | ||
|
|
48b34bf95f | ||
|
|
d5fc69e210 | ||
|
|
59f9dd697f | ||
|
|
c9d72323f1 | ||
|
|
e87f7f3abe | ||
|
|
82ebbcb6d6 | ||
|
|
2db11070c5 | ||
|
|
5efd2517e7 | ||
|
|
c0ba654678 | ||
|
|
546a5a549b | ||
|
|
cbf02c34e3 | ||
|
|
74a7258f10 | ||
|
|
1006c044bc | ||
|
|
ef4ea719f3 | ||
|
|
8b34afe69f | ||
|
|
01292af298 | ||
|
|
cff8b2fe39 | ||
|
|
2cb20b5cc0 | ||
|
|
8f2aed18fe | ||
|
|
d85831cc9a | ||
|
|
55dc3a5556 | ||
|
|
591afe08bd | ||
|
|
748f2002ab | ||
|
|
d2d18a2384 | ||
|
|
35f4fa6aa7 | ||
|
|
66fc2d22ed | ||
|
|
16cf9ee1ed | ||
|
|
d9d97bf14c | ||
|
|
dc811bd3c7 | ||
|
|
b939d1849a | ||
|
|
beca31f55d | ||
|
|
c7df103950 | ||
|
|
4bf7972ad5 | ||
|
|
534eaed1ed | ||
|
|
7e014e7385 | ||
|
|
34adb2660b | ||
|
|
b6bc165cf0 | ||
|
|
bdd5ed7fc7 | ||
|
|
95d19417c3 | ||
|
|
30ebebdd71 | ||
|
|
e9c557776d | ||
|
|
535a43b698 | ||
|
|
59752ed4aa | ||
|
|
b3e7b8f3f1 | ||
|
|
c4e9365512 | ||
|
|
7d3972d3a8 | ||
|
|
52ca4306fd | ||
|
|
da368ee612 | ||
|
|
22c50e7765 | ||
|
|
7bc39dd1bc | ||
|
|
c80ead6116 | ||
|
|
67e76e4009 | ||
|
|
b213218a30 | ||
|
|
c629a1252c | ||
|
|
64d2481e93 | ||
|
|
e7d6a6add8 | ||
|
|
edc25f7da4 | ||
|
|
5bff84ace1 | ||
|
|
f8bfcba317 | ||
|
|
013a05201b | ||
|
|
433e811821 | ||
|
|
df4cfc0fbc |
18
.env
18
.env
@@ -2,28 +2,18 @@ GENERATE_SOURCEMAP=false
|
|||||||
|
|
||||||
REACT_APP_NAME=KISS Translator
|
REACT_APP_NAME=KISS Translator
|
||||||
REACT_APP_NAME_CN=简约翻译
|
REACT_APP_NAME_CN=简约翻译
|
||||||
REACT_APP_VERSION=1.7.12
|
REACT_APP_VERSION=2.0.12
|
||||||
|
|
||||||
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
REACT_APP_HOMEPAGE=https://github.com/fishjar/kiss-translator
|
||||||
|
|
||||||
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
|
REACT_APP_OPTIONSPAGE=https://fishjar.github.io/kiss-translator/options.html
|
||||||
REACT_APP_OPTIONSPAGE2=https://kiss-translator.rayjar.com/options
|
|
||||||
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
REACT_APP_OPTIONSPAGE_DEV=http://localhost:3000/options.html
|
||||||
|
|
||||||
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
REACT_APP_LOGOURL=https://fishjar.github.io/kiss-translator/images/logo192.png
|
||||||
REACT_APP_LOGOURL2=https://kiss-translator.rayjar.com/images/logo192.png
|
|
||||||
|
|
||||||
REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules.json
|
REACT_APP_RULESURL=https://fishjar.github.io/kiss-rules/kiss-rules_v2.json
|
||||||
REACT_APP_RULESURL_ON=https://fishjar.github.io/kiss-rules/kiss-rules-on.json
|
REACT_APP_RULESURL_ON=https://fishjar.github.io/kiss-rules/kiss-rules-on_v2.json
|
||||||
REACT_APP_RULESURL_OFF=https://fishjar.github.io/kiss-rules/kiss-rules-off.json
|
REACT_APP_RULESURL_OFF=https://fishjar.github.io/kiss-rules/kiss-rules-off_v2.json
|
||||||
|
|
||||||
REACT_APP_WEBFIXURL=https://fishjar.github.io/kiss-rules/kiss-webfix.json
|
|
||||||
|
|
||||||
REACT_APP_VERSIONFILE=https://fishjar.github.io/kiss-translator/version.txt
|
|
||||||
REACT_APP_VERSIONFILE2=https://kiss-translator.rayjar.com/version.txt
|
|
||||||
|
|
||||||
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
REACT_APP_USERSCRIPT_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator.user.js
|
||||||
REACT_APP_USERSCRIPT_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator.user.js
|
|
||||||
|
|
||||||
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
|
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL=https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js
|
||||||
REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2=https://kiss-translator.rayjar.com/kiss-translator-ios-safari.user.js
|
|
||||||
|
|||||||
37
.github/workflows/release.yml
vendored
37
.github/workflows/release.yml
vendored
@@ -7,28 +7,28 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 8.7.6
|
version: 9.14.4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "18.17.0"
|
node-version: 24
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install
|
- run: pnpm install
|
||||||
- run: pnpm build
|
- run: pnpm build+zip
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: build
|
path: build
|
||||||
deploy-web:
|
deploy-web:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: build
|
path: build
|
||||||
@@ -37,7 +37,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
folder: build/web
|
folder: build/web
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-22.04
|
needs: build
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
outputs:
|
outputs:
|
||||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||||
steps:
|
steps:
|
||||||
@@ -54,18 +55,14 @@ jobs:
|
|||||||
needs: [build, create-release]
|
needs: [build, create-release]
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
client: ["chrome", "edge", "firefox", "userscript"]
|
client: ["chrome", "edge", "firefox", "userscript", "thunderbird"]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: build
|
path: build
|
||||||
- name: Zip Release
|
|
||||||
run: |
|
|
||||||
cd build
|
|
||||||
zip -r ${{ matrix.client }}.zip ${{ matrix.client }}
|
|
||||||
- uses: actions/upload-release-asset@v1
|
- uses: actions/upload-release-asset@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
|
/.obsidian
|
||||||
.pnp.js
|
.pnp.js
|
||||||
.yarn
|
.yarn
|
||||||
|
|
||||||
|
|||||||
1
.pnpm-version
Normal file
1
.pnpm-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
9.14.4
|
||||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
build
|
||||||
|
public
|
||||||
|
package.json
|
||||||
24
.prettierrc
Normal file
24
.prettierrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"insertPragma": false,
|
||||||
|
"singleAttributePerLine": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 80,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": false,
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"experimentalTernaries": false,
|
||||||
|
"parser": "babel"
|
||||||
|
}
|
||||||
143
README.en.md
143
README.en.md
@@ -1,6 +1,8 @@
|
|||||||
# KISS Translator
|
# KISS Translator
|
||||||
|
|
||||||
A simple [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
|
||||||
|
|
||||||
|
A simple, open source [bilingual translation extension & Greasemonkey script](https://github.com/fishjar/kiss-translator).
|
||||||
|
|
||||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||||
|
|
||||||
@@ -9,29 +11,57 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
|
|||||||
- [x] Keep it simple, smart
|
- [x] Keep it simple, smart
|
||||||
- [x] Open source
|
- [x] Open source
|
||||||
- [x] Adapt to common browsers
|
- [x] Adapt to common browsers
|
||||||
- [x] Chrome/Edge/Firefox/Kiwi
|
- [x] Chrome/Edge
|
||||||
- [ ] Safari
|
- [x] Firefox
|
||||||
|
- [x] Kiwi (Android)
|
||||||
|
- [x] Orion (iOS)
|
||||||
|
- [x] Safari
|
||||||
|
- [x] Thunderbird
|
||||||
- [x] Supports multiple translation services
|
- [x] Supports multiple translation services
|
||||||
- [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent
|
- [x] Google/Microsoft
|
||||||
- [x] Custom translation interface
|
- [x] Tencent/Volcengine
|
||||||
|
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||||
|
- [x] DeepL/DeepLX/NiuTrans
|
||||||
|
- [x] AzureAI / CloudflareAI
|
||||||
|
- [x] Chrome built-in AI translation (BuiltinAI)
|
||||||
- [x] Covers common translation scenarios
|
- [x] Covers common translation scenarios
|
||||||
- [x] Web bilingual translation
|
- [x] Webpage bilingual translation
|
||||||
- [x] Input box translation
|
- [x] Input-box translation
|
||||||
- [x] Seletction translation
|
- Instantly translate text in input fields into other languages via shortcut keys
|
||||||
- [x] Favorite Words
|
- [x] Text selection translation
|
||||||
- [x] Mouseover translation
|
- [x] Open translation popup on any page, support multiple translation services for comparison
|
||||||
|
- [x] English dictionary lookup
|
||||||
|
- [x] Save vocabulary
|
||||||
|
- [x] Hover translation
|
||||||
- [x] YouTube subtitle translation
|
- [x] YouTube subtitle translation
|
||||||
|
- Support translating video subtitles with any translation service and display bilingually
|
||||||
|
- Built-in basic subtitle merging and sentence-splitting algorithm to improve translation quality
|
||||||
|
- Supports AI-powered sentence segmentation for even better translation
|
||||||
|
- Custom subtitle style
|
||||||
|
- [x] Supports diverse translation modes
|
||||||
|
- [x] Supports both automatic text recognition and manual rule modes
|
||||||
|
- Automatic text recognition mode allows most sites to be translated fully without writing rules
|
||||||
|
- Manual rule mode enables extreme optimization for specific sites
|
||||||
|
- [x] Custom translation styling
|
||||||
|
- [x] Supports rich-text translation and rendering, preserving links and other text styles where possible
|
||||||
|
- [x] Option to show only translation (hide original text)
|
||||||
|
- [x] Advanced translation API features
|
||||||
|
- [x] With custom API support, theoretically works with any translation service
|
||||||
|
- [x] Batch aggregation of translation requests
|
||||||
|
- [x] Supports AI conversation context memory to improve translation quality
|
||||||
|
- [x] Custom AI terminology dictionary
|
||||||
|
- [x] All APIs support hooks and custom parameters for advanced usage
|
||||||
- [x] Cross-client data synchronization
|
- [x] Cross-client data synchronization
|
||||||
- [x] KISS-Worker(cloudflare/docker)
|
- [x] KISS-Worker(cloudflare/docker)
|
||||||
- [x] WebDAV
|
- [x] WebDAV
|
||||||
- [x] Custom translation rules
|
- [x] Custom translation rules
|
||||||
- [x] Rule subscription/rule sharing
|
- [x] Rule subscription/rule sharing
|
||||||
- [x] Custom translation style
|
- [x] Customized terminology
|
||||||
- [x] Custom shortcut keys
|
- [x] Custom shortcut keys
|
||||||
- `Alt+Q` Toggle Translation
|
- `Alt+Q` Toggle Translation
|
||||||
- `Alt+C` Toggle Styles
|
- `Alt+C` Toggle Styles
|
||||||
- `Alt+K` Open Setting Popup
|
- `Alt+K` Open Setting Popup
|
||||||
- `Alt+B` Open Translate Popup
|
- `Alt+S` Open Translate Popup / Translate Selected Text
|
||||||
- `Alt+O` Open Options Page
|
- `Alt+O` Open Options Page
|
||||||
- `Alt+I` Input Box Translation
|
- `Alt+I` Input Box Translation
|
||||||
|
|
||||||
@@ -39,17 +69,22 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
|
|||||||
|
|
||||||
> Note: For the following reasons, it is recommended to use browser extensions first
|
> Note: For the following reasons, it is recommended to use browser extensions first
|
||||||
>
|
>
|
||||||
> - Browser extension can use local language recognition
|
> - Browser extensions have more complete functions (local language recognition, context menu, etc.)
|
||||||
> - Grease Monkey script will encounter more usage problems
|
> - Grease Monkey script will encounter more usage problems (cross domain issues, script conflicts, etc.)
|
||||||
|
|
||||||
- [x] Browser extension
|
- [x] Browser extension
|
||||||
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
- [x] Chrome [Installation address](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=en)
|
||||||
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
- [x] Kiwi (Android)
|
||||||
- [x] Firefox [Installation address](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
- [x] Orion (iOS)
|
||||||
|
- [x] Edge [Installation address](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=en)
|
||||||
|
- [x] Firefox [Installation address](https://addons.mozilla.org/en-US/firefox/addon/kiss-translator/)
|
||||||
- [ ] Safari
|
- [ ] Safari
|
||||||
|
- [ ] Safari (Mac)
|
||||||
|
- [ ] Safari (iOS)
|
||||||
|
- [x] Thunderbird [Download address](https://github.com/fishjar/kiss-translator/releases)
|
||||||
- [x] GreaseMonkey Script
|
- [x] GreaseMonkey Script
|
||||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||||
- Greasy Fork [Installation address](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||||
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [Installation link](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||||
|
|
||||||
## Associated Projects
|
## Associated Projects
|
||||||
@@ -61,22 +96,70 @@ A simple [bilingual translation extension & Greasemonkey script](https://github.
|
|||||||
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
- Community subscription rules: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||||
- Provides the latest and most complete list of subscription rules maintained by the community.
|
- Provides the latest and most complete list of subscription rules maintained by the community.
|
||||||
- Help with rules-related issues.
|
- Help with rules-related issues.
|
||||||
- Web page correction script: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
|
|
||||||
- Fixed scripts for some special sites.
|
## Frequently Asked Questions
|
||||||
- So that the translation software can get better display effect.
|
|
||||||
- Translation interface agent: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
### How to Set Keyboard Shortcuts
|
||||||
- If you encounter network problems when accessing a certain translation interface, this proxy service may help you.
|
|
||||||
- Deploy and manage by yourself.
|
Set this in the extension management page, for example:
|
||||||
- Minimalistic Dictionary Plugin: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
|
||||||
- A word-marking translation plug-in used with this project.
|
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||||
- Supports query of English words, sentences and Chinese characters.
|
- firefox [about:addons](about:addons)
|
||||||
- Supports history records and word collections.
|
|
||||||
|
### What is the priority order of rule settings?
|
||||||
|
|
||||||
|
Personal Rules > Subscription Rules > Global Rules
|
||||||
|
|
||||||
|
Among these, Global Rules have the lowest priority but are very important as they serve as the default rules.
|
||||||
|
|
||||||
|
### API (Ollama, etc.) Test Failure
|
||||||
|
|
||||||
|
Common reasons for API test failures include:
|
||||||
|
|
||||||
|
- Incorrect address:
|
||||||
|
- For example, `Ollama` has a native API address and an `Openai`-compatible address. This plugin currently supports the `Openai`-compatible address and does not support the `Ollama` native API address.
|
||||||
|
- Some AI models do not support batch translation:
|
||||||
|
- In this case, you can choose to disable batch translation or use a custom API.
|
||||||
|
- Alternatively, you can use a custom API. For details, please refer to: [Custom API Example Documentation](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||||
|
- Some AI models have inconsistent parameters:
|
||||||
|
- For example, the parameters of the `Gemini` native API are highly inconsistent. Some model versions do not support certain parameters, leading to errors.
|
||||||
|
- In this case, you can modify the request body using a `Hook`, or replace it with `Gemini2` (an OpenAI-compatible address).
|
||||||
|
- The server restricts cross-origin access, returning a 403 error:
|
||||||
|
- For example, `Ollama` requires adding the environment variable `OLLAMA_ORIGINS=*` when starting. See: https://github.com/fishjar/kiss-translator/issues/174
|
||||||
|
|
||||||
|
### Custom API doesn't work in Tampermonkey scripts
|
||||||
|
|
||||||
|
Tampermonkey scripts require adding domains to the whitelist; otherwise, requests cannot be sent.
|
||||||
|
|
||||||
|
### How to set up a hook function for a custom API
|
||||||
|
|
||||||
|
Custom APIs are very powerful and flexible, and can theoretically connect to any translation API.
|
||||||
|
|
||||||
|
Example reference: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||||
|
|
||||||
|
### How to directly access the Tampermonkey script settings page
|
||||||
|
|
||||||
|
Settings page address: https://fishjar.github.io/kiss-translator/options.html
|
||||||
|
|
||||||
|
## Future Plans
|
||||||
|
|
||||||
|
This is a side project with no strict timeline. Community contributions are welcome. The following are preliminary feature directions:
|
||||||
|
|
||||||
|
- [x] **Batch Text Requests**: Optimize request strategy to reduce translation API calls and improve performance.
|
||||||
|
- [x] **Enhanced Rich Text Translation**: Support accurate translation of complex page structures and rich text content.
|
||||||
|
- [x] **Advanced Custom/AI Interfaces**: Add support for context memory, multi-turn conversations, and other advanced AI features.
|
||||||
|
- [x] **Fallback English Dictionary**: When translation services fail, fall back to a local dictionary lookup.
|
||||||
|
- [x] **Improved YouTube Subtitle Support**: Enhance merging and translation experience for streaming subtitles, reducing sentence fragmentation.
|
||||||
|
- [ ] **Upgraded Rule Collaboration System**: Introduce more flexible rule sharing, version management, and community review processes.
|
||||||
|
|
||||||
|
If you're interested in any of these directions, feel free to discuss in [Issues](https://github.com/fishjar/kiss-translator/issues) or submit a PR!
|
||||||
|
|
||||||
## Development Guidelines
|
## Development Guidelines
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/fishjar/kiss-translator.git
|
git clone https://github.com/fishjar/kiss-translator.git
|
||||||
cd kiss-translator
|
cd kiss-translator
|
||||||
|
git checkout dev # Submit a PR suggestion to push to the dev branch
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
```
|
```
|
||||||
@@ -84,3 +167,7 @@ pnpm build
|
|||||||
## Discussion
|
## Discussion
|
||||||
|
|
||||||
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
- Join [Telegram Group](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||||
|
|
||||||
|
## Appreciate
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
173
README.ja.md
Normal file
173
README.ja.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# KISS Translator シンプル翻訳
|
||||||
|
|
||||||
|
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
|
||||||
|
|
||||||
|
シンプルでオープンソースの [バイリンガル対照翻訳拡張機能&ユーザースクリプト](https://github.com/fishjar/kiss-translator)です。
|
||||||
|
|
||||||
|
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||||
|
|
||||||
|
## 特徴
|
||||||
|
|
||||||
|
- [x] シンプルさを維持
|
||||||
|
- [x] オープンソース
|
||||||
|
- [x] 主要なブラウザに対応
|
||||||
|
- [x] Chrome/Edge
|
||||||
|
- [x] Firefox
|
||||||
|
- [x] Kiwi (Android)
|
||||||
|
- [x] Orion (iOS)
|
||||||
|
- [x] Safari
|
||||||
|
- [x] Thunderbird
|
||||||
|
- [x] 複数の翻訳サービスをサポート
|
||||||
|
- [x] Google/Microsoft
|
||||||
|
- [x] Tencent/Volcengine
|
||||||
|
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||||
|
- [x] DeepL/DeepLX/NiuTrans
|
||||||
|
- [x] AzureAI/CloudflareAI
|
||||||
|
- [x] Chromeブラウザ内蔵AI翻訳(BuiltinAI)
|
||||||
|
- [x] 一般的な翻訳シナリオをカバー
|
||||||
|
- [x] Webページのバイリンガル対照翻訳
|
||||||
|
- [x] 入力ボックス翻訳
|
||||||
|
- ショートカットキーで入力ボックス内のテキストを即座に他言語に翻訳
|
||||||
|
- [x] テキスト選択翻訳
|
||||||
|
- [x] 任意のページで翻訳ボックスを開き、複数の翻訳サービスで比較翻訳が可能
|
||||||
|
- [x] 英語辞書翻訳
|
||||||
|
- [x] 単語のブックマーク
|
||||||
|
- [x] マウスオーバー翻訳
|
||||||
|
- [x] YouTube 字幕翻訳
|
||||||
|
- 任意の翻訳サービスを使用してビデオ字幕を翻訳し、バイリンガル表示をサポート
|
||||||
|
- 基本的な字幕結合・改行アルゴリズムを内蔵し、翻訳品質を向上
|
||||||
|
- AIによる改行機能をサポートし、翻訳品質をさらに向上
|
||||||
|
- 字幕スタイルのカスタマイズ
|
||||||
|
- [x] 多様な翻訳効果をサポート
|
||||||
|
- [x] テキスト自動認識と手動ルールの2つのモードをサポート
|
||||||
|
- テキスト自動認識モードにより、ほとんどのWebサイトでルールを記述しなくても完全な翻訳が可能
|
||||||
|
- 手動ルールモードで、特定のWebサイトに合わせた最適な最適化が可能
|
||||||
|
- [x] 翻訳テキストスタイルのカスタマイズ
|
||||||
|
- [x] リッチテキストの翻訳と表示をサポートし、原文のリンクやその他のテキストスタイルを可能な限り保持
|
||||||
|
- [x] 翻訳文のみの表示(原文を非表示)をサポート
|
||||||
|
- [x] 翻訳APIの高度な機能
|
||||||
|
- [x] カスタムAPIにより、理論上あらゆる翻訳インターフェースをサポート
|
||||||
|
- [x] 翻訳テキストの統合バッチ送信
|
||||||
|
- [x] AIコンテキスト(会話メモリ)機能をサポートし、翻訳品質を向上
|
||||||
|
- [x] カスタムAI用語集
|
||||||
|
- [x] すべてのインターフェースがフックやカスタムパラメータなどの高度な機能をサポート
|
||||||
|
- [x] クライアント間のデータ同期
|
||||||
|
- [x] KISS-Worker(cloudflare/docker)
|
||||||
|
- [x] WebDAV
|
||||||
|
- [x] カスタム翻訳ルール
|
||||||
|
- [x] ルールの購読/ルール共有
|
||||||
|
- [x] カスタム専門用語
|
||||||
|
- [x] カスタムショートカットキー
|
||||||
|
- `Alt+Q` 翻訳をオン
|
||||||
|
- `Alt+C` スタイル切り替え
|
||||||
|
- `Alt+K` 設定ポップアップを開く
|
||||||
|
- `Alt+S` 翻訳ポップアップを開く/選択テキストを翻訳
|
||||||
|
- `Alt+O` 設定ページを開く
|
||||||
|
- `Alt+I` 入力ボックス翻訳
|
||||||
|
|
||||||
|
## インストール
|
||||||
|
|
||||||
|
> 注:以下の理由により、ブラウザ拡張機能の使用を優先することをお勧めします
|
||||||
|
>
|
||||||
|
> - ブラウザ拡張機能の方が機能が完全です(ローカル言語認識、右クリックメニューなど)
|
||||||
|
> - ユーザースクリプトはより多くの問題(クロスドメイン問題、スクリプトの競合など)に遭遇する可能性があります
|
||||||
|
|
||||||
|
- [x] ブラウザ拡張機能
|
||||||
|
- [x] Chrome [インストール](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=ja)
|
||||||
|
- [x] Kiwi (Android)
|
||||||
|
- [x] Orion (iOS)
|
||||||
|
- [x] Edge [インストール](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=ja)
|
||||||
|
- [x] Firefox [インストール](https://addons.mozilla.org/ja/firefox/addon/kiss-translator/)
|
||||||
|
- [ ] Safari
|
||||||
|
- [ ] Safari (Mac)
|
||||||
|
- [ ] Safari (iOS)
|
||||||
|
- [x] Thunderbird [ダウンロード](https://github.com/fishjar/kiss-translator/releases)
|
||||||
|
- [x] ユーザースクリプト
|
||||||
|
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [インストールリンク](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||||
|
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||||
|
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [インストールリンク](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||||
|
|
||||||
|
## 関連プロジェクト
|
||||||
|
|
||||||
|
- データ同期サービス: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||||
|
- 本プロジェクトのデータ同期サービスとして使用できます。
|
||||||
|
- 個人のプライベートなルールリストの共有にも使用できます。
|
||||||
|
- セルフホスト、セルフマネジメント、データはプライベート。
|
||||||
|
- コミュニティ購読ルール: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||||
|
- コミュニティによってメンテナンスされた、最新かつ最も完全な購読ルールリストを提供します。
|
||||||
|
- ルール関連の問題についての助けを求める。
|
||||||
|
|
||||||
|
## よくある質問(FAQ)
|
||||||
|
|
||||||
|
### ショートカットキーの設定方法
|
||||||
|
|
||||||
|
拡張機能の管理ページで設定します。例:
|
||||||
|
|
||||||
|
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||||
|
- firefox [about:addons](about:addons)
|
||||||
|
|
||||||
|
### ルール設定の優先順位は?
|
||||||
|
|
||||||
|
個人ルール > 購読ルール > グローバルルール
|
||||||
|
|
||||||
|
グローバルルールの優先順位は最も低いですが、フォールバックルールとして非常に重要です。
|
||||||
|
|
||||||
|
### API(Ollamaなど)のテストに失敗する
|
||||||
|
|
||||||
|
APIテストの失敗には、一般的に以下の原因が考えられます:
|
||||||
|
|
||||||
|
- アドレスが間違っている:
|
||||||
|
- 例えば `Ollama` にはネイティブAPIアドレスと `Openai` 互換のアドレスがありますが、本プラグインは現在、`Openai` 互換アドレスをサポートしており、`Ollama` ネイティブAPIアドレスはサポートしていません
|
||||||
|
- 一部のAIモデルが統合翻訳をサポートしていない:
|
||||||
|
- この場合、統合翻訳を無効にするか、カスタムAPIを使用して対応できます。
|
||||||
|
- または、カスタムAPIを使用して対応します。詳細は[カスタムAPIサンプルドキュメント](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)を参照してください
|
||||||
|
- 一部のAIモデルでパラメータが一致しない:
|
||||||
|
- 例えば `Gemini` のネイティブAPIはパラメータの不一致が大きく、一部のバージョンのモデルが特定のパラメータをサポートしていないためエラーが返されることがあります。
|
||||||
|
- この場合、`Hook` を使用してリクエスト `body` を変更するか、`Gemini2` (`Openai` 互換アドレス) に切り替えることができます
|
||||||
|
- サーバーのクロスドメイン制限によりアクセスが拒否され、403エラーが返される:
|
||||||
|
- 例えば `Ollama` を起動する際に、環境変数 `OLLAMA_ORIGINS=*` を追加する必要があります。参考:https://github.com/fishjar/kiss-translator/issues/174
|
||||||
|
|
||||||
|
### 入力したAPIがユーザースクリプトで使用できない
|
||||||
|
|
||||||
|
ユーザースクリプトは、リクエストを送信するためにドメインのホワイトリストを追加する必要があります。
|
||||||
|
|
||||||
|
### カスタムAPIのhook関数の設定方法
|
||||||
|
|
||||||
|
カスタムAPI機能は非常に強力で柔軟性があり、理論的にはどんな翻訳APIにも接続できます。
|
||||||
|
|
||||||
|
サンプル参照: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||||
|
|
||||||
|
### ユーザースクリプトの設定ページに直接アクセスする方法
|
||||||
|
|
||||||
|
設定ページアドレス: https://fishjar.github.io/kiss-translator/options.html
|
||||||
|
|
||||||
|
## 今後の計画
|
||||||
|
|
||||||
|
本プロジェクトは余暇に開発しており、厳密なタイムスケジュールはありません。コミュニティの共同構築を歓迎します。以下は初期段階の機能の方向性です:
|
||||||
|
|
||||||
|
- [x] **テキストの統合送信**:リクエスト戦略を最適化し、翻訳APIの呼び出し回数を減らし、パフォーマンスを向上させます。
|
||||||
|
- [x] **リッチテキスト翻訳の強化**:より複雑なページ構造やリッチテキストコンテンツの正確な翻訳をサポートします。
|
||||||
|
- [x] **カスタム/AI APIの強化**:コンテキストメモリ、複数ラウンドの対話など、高度なAI機能をサポートします。
|
||||||
|
- [x] **英語辞書のフォールバックメカニズム**:翻訳サービスが利用できない場合、他の辞書に切り替えるか、ローカル辞書での検索にフォールバックします。
|
||||||
|
- [x] **YouTube字幕サポートの最適化**:ストリーミング字幕の結合と翻訳体験を改善し、途切れを減らします。
|
||||||
|
- [ ] **ルール共同構築メカニズムのアップグレード**:より柔軟なルールの共有、バージョン管理、コミュニティレビュープロセスを導入します。
|
||||||
|
|
||||||
|
特定の方向に興味がある場合は、[Issues](https://github.com/fishjar/kiss-translator/issues) で議論したり、PRを送信したりすることを歓迎します!
|
||||||
|
|
||||||
|
## 開発ガイド
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone [https://github.com/fishjar/kiss-translator.git](https://github.com/fishjar/kiss-translator.git)
|
||||||
|
cd kiss-translator
|
||||||
|
git checkout dev # PRを送信する場合はdevブランチにプッシュすることをお勧めします
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## コミュニケーション
|
||||||
|
|
||||||
|
- [Telegram グループ](https://t.me/+RRCu_4oNwrM2NmFl)に参加
|
||||||
|
|
||||||
|
## 寄付
|
||||||
|
|
||||||
|

|
||||||
174
README.ko.md
Normal file
174
README.ko.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# KISS Translator 심플 번역
|
||||||
|
|
||||||
|
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
|
||||||
|
|
||||||
|
심플하고 오픈 소스인 [이중 언어 대조 번역 확장 프로그램 & 유저 스크립트](https://github.com/fishjar/kiss-translator)입니다.
|
||||||
|
|
||||||
|
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||||
|
|
||||||
|
## 특징
|
||||||
|
|
||||||
|
- [x] 심플함 유지
|
||||||
|
- [x] 오픈 소스
|
||||||
|
- [x] 주요 브라우저 지원
|
||||||
|
- [x] Chrome/Edge
|
||||||
|
- [x] Firefox
|
||||||
|
- [x] Kiwi (Android)
|
||||||
|
- [x] Orion (iOS)
|
||||||
|
- [x] Safari
|
||||||
|
- [x] Thunderbird
|
||||||
|
- [x] 다양한 번역 서비스 지원
|
||||||
|
- [x] Google/Microsoft
|
||||||
|
- [x] Tencent/Volcengine
|
||||||
|
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||||
|
- [x] DeepL/DeepLX/NiuTrans
|
||||||
|
- [x] AzureAI/CloudflareAI
|
||||||
|
- [x] Chrome 브라우저 내장 AI 번역(BuiltinAI)
|
||||||
|
- [x] 일반적인 번역 시나리오 지원
|
||||||
|
- [x] 웹페이지 이중 언어 대조 번역
|
||||||
|
- [x] 입력창 번역
|
||||||
|
- 단축키를 통해 입력창 내 텍스트를 즉시 다른 언어로 번역
|
||||||
|
- [x] 텍스트 선택 번역
|
||||||
|
- [x] 모든 페이지에서 번역창을 열어 여러 번역 서비스로 비교 번역 가능
|
||||||
|
- [x] 영어 사전 번역
|
||||||
|
- [x] 단어 즐겨찾기
|
||||||
|
- [x] 마우스오버 번역
|
||||||
|
- [x] YouTube 자막 번역
|
||||||
|
- 모든 번역 서비스를 사용하여 비디오 자막을 번역하고 이중 언어로 표시 지원
|
||||||
|
- 기본적인 자막 병합 및 줄 바꿈 알고리즘 내장으로 번역 품질 향상
|
||||||
|
- AI 줄 바꿈 기능 지원으로 번역 품질 추가 향상
|
||||||
|
- 사용자 정의 자막 스타일
|
||||||
|
- [x] 다양한 번역 효과 지원
|
||||||
|
- [x] 자동 텍스트 인식 및 수동 규칙 두 가지 모드 지원
|
||||||
|
- 자동 텍스트 인식 모드는 대부분의 웹사이트에서 규칙 작성 없이도 완벽한 번역 가능
|
||||||
|
- 수동 규칙 모드로 특정 웹사이트에 대한 최적의 최적화 가능
|
||||||
|
- [x] 번역문 스타일 사용자 정의
|
||||||
|
- [x] 리치 텍스트 번역 및 표시 지원, 원문의 링크 및 기타 텍스트 스타일 최대한 보존
|
||||||
|
- [x] 번역문만 표시 (원문 숨기기) 지원
|
||||||
|
- [x] 번역 인터페이스 고급 기능
|
||||||
|
- [x] 사용자 정의 인터페이스를 통해 이론상 모든 번역 인터페이스 지원
|
||||||
|
- [x] 번역 텍스트 일괄 통합 전송
|
||||||
|
- [x] AI 컨텍스트 (대화 기억) 기능 지원으로 번역 품질 향상
|
||||||
|
- [x] 사용자 정의 AI 용어 사전
|
||||||
|
- [x] 모든 인터페이스는 후크 및 사용자 정의 파라미터 등 고급 기능 지원
|
||||||
|
- [x] 클라이언트 간 데이터 동기화
|
||||||
|
- [x] KISS-Worker (cloudflare/docker)
|
||||||
|
- [x] WebDAV
|
||||||
|
- [x] 사용자 정의 번역 규칙
|
||||||
|
- [x] 규칙 구독 / 규칙 공유
|
||||||
|
- [x] 사용자 정의 전문 용어
|
||||||
|
- [x] 사용자 정의 단축키
|
||||||
|
- `Alt+Q` 번역 켜기
|
||||||
|
- `Alt+C` 스타일 전환
|
||||||
|
- `Alt+K` 설정 팝업 열기
|
||||||
|
- `Alt+S` 번역 팝업 열기 / 선택한 텍스트 번역
|
||||||
|
- `Alt+O` 설정 페이지 열기
|
||||||
|
- `Alt+I` 입력창 번역
|
||||||
|
|
||||||
|
## 설치
|
||||||
|
|
||||||
|
> 참고: 다음과 같은 이유로 브라우저 확장 프로그램 사용을 우선적으로 권장합니다.
|
||||||
|
>
|
||||||
|
> - 브라우저 확장 프로그램의 기능이 더 완전합니다 (로컬 언어 인식, 우클릭 메뉴 등).
|
||||||
|
> - 유저 스크립트는 사용상 더 많은 문제 (크로스 도메인 문제, 스크립트 충돌 등)를 겪을 수 있습니다.
|
||||||
|
|
||||||
|
- [x] 브라우저 확장 프로그램
|
||||||
|
- [x] Chrome [설치 주소](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=ko)
|
||||||
|
- [x] Kiwi (Android)
|
||||||
|
- [x] Orion (iOS)
|
||||||
|
- [x] Edge [설치 주소](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=ko)
|
||||||
|
- [x] Firefox [설치 주소](https://addons.mozilla.org/ko/firefox/addon/kiss-translator/)
|
||||||
|
- [ ] Safari
|
||||||
|
- [ ] Safari (Mac)
|
||||||
|
- [ ] Safari (iOS)
|
||||||
|
- [x] Thunderbird [다운로드 주소](https://github.com/fishjar/kiss-translator/releases)
|
||||||
|
- [x] 유저 스크립트
|
||||||
|
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [설치 링크](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||||
|
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||||
|
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [설치 링크](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||||
|
|
||||||
|
## 관련 프로젝트
|
||||||
|
|
||||||
|
- 데이터 동기화 서비스: [https://github.com/fishjar/kiss-worker](https://github.com/fishjar/kiss-worker)
|
||||||
|
- 본 프로젝트의 데이터 동기화 서비스로 사용할 수 있습니다.
|
||||||
|
- 개인의 비공개 규칙 목록을 공유하는 데에도 사용할 수 있습니다.
|
||||||
|
- 직접 배포, 직접 관리, 데이터 비공개.
|
||||||
|
- 커뮤니티 구독 규칙: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||||
|
- 커뮤니티에서 유지 관리하는 최신의 가장 완벽한 구독 규칙 목록을 제공합니다.
|
||||||
|
- 규칙 관련 문제에 대한 도움 요청.
|
||||||
|
|
||||||
|
## 자주 묻는 질문 (FAQ)
|
||||||
|
|
||||||
|
### 단축키는 어떻게 설정하나요?
|
||||||
|
|
||||||
|
플러그인 관리 페이지에서 설정합니다. 예:
|
||||||
|
|
||||||
|
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||||
|
- firefox [about:addons](about:addons)
|
||||||
|
|
||||||
|
### 규칙 설정의 우선순위는 어떻게 되나요?
|
||||||
|
|
||||||
|
개인 규칙 > 구독 규칙 > 전역 규칙
|
||||||
|
|
||||||
|
그중 전역 규칙은 우선순위가 가장 낮지만, 예비 규칙으로서 매우 중요합니다.
|
||||||
|
|
||||||
|
### 인터페이스 (Ollama 등) 테스트 실패
|
||||||
|
|
||||||
|
일반적으로 인터페이스 테스트 실패는 다음과 같은 몇 가지 원인이 있습니다:
|
||||||
|
|
||||||
|
- 주소를 잘못 입력한 경우:
|
||||||
|
- 예를 들어 `Ollama`는 네이티브 인터페이스 주소와 `Openai` 호환 주소가 있습니다. 본 플러그인은 현재 `Openai` 호환 주소를 통일되게 지원하며, `Ollama` 네이티브 인터페이스 주소는 지원하지 않습니다.
|
||||||
|
- 일부 AI 모델이 통합 번역을 지원하지 않는 경우:
|
||||||
|
- 이 경우 통합 번역을 비활성화하거나 사용자 정의 인터페이스 방식을 통해 사용할 수 있습니다.
|
||||||
|
- 또는 사용자 정의 인터페이스 방식을 통해 사용합니다. 자세한 내용은 [사용자 정의 인터페이스 예시 문서](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)를 참조하세요.
|
||||||
|
- 일부 AI 모델의 파라미터가 일치하지 않는 경우:
|
||||||
|
- 예를 들어 `Gemini` 네이티브 인터페이스 파라미터는 매우 불일치하며, 일부 버전의 모델은 특정 파라미터를 지원하지 않아 오류를 반환할 수 있습니다.
|
||||||
|
- 이 경우 `Hook`을 사용하여 요청 `body`를 수정하거나, `Gemini2` (`Openai` 호환 주소)로 변경할 수 있습니다.
|
||||||
|
- 서버의 크로스 도메인 접근 제한으로 403 오류가 반환되는 경우:
|
||||||
|
- 예를 들어 `Ollama` 시작 시 환경 변수 `OLLAMA_ORIGINS=*`를 추가해야 합니다. 참고: https://github.com/fishjar/kiss-translator/issues/174
|
||||||
|
|
||||||
|
### 입력한 인터페이스를 유저 스크립트에서 사용할 수 없습니다
|
||||||
|
|
||||||
|
유저 스크립트는 도메인 화이트리스트를 추가해야 요청을 보낼 수 있습니다.
|
||||||
|
|
||||||
|
### 사용자 정의 인터페이스의 hook 함수는 어떻게 설정하나요?
|
||||||
|
|
||||||
|
사용자 정의 인터페이스 기능은 매우 강력하고 유연하며, 이론적으로 어떤 번역 인터페이스든 연결할 수 있습니다.
|
||||||
|
|
||||||
|
예시 참고: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||||
|
|
||||||
|
### 유저 스크립트 설정 페이지로 바로 이동하는 방법
|
||||||
|
|
||||||
|
설정 페이지 주소: https://fishjar.github.io/kiss-translator/options.html
|
||||||
|
|
||||||
|
## 향후 계획
|
||||||
|
|
||||||
|
본 프로젝트는 여가 시간에 개발되며, 엄격한 시간표는 없습니다. 커뮤니티의 공동 구축을 환영합니다. 다음은 초기 구상 중인 기능 방향입니다:
|
||||||
|
|
||||||
|
- [x] **텍스트 통합 전송**: 요청 전략을 최적화하여 번역 인터페이스 호출 횟수를 줄이고 성능을 향상시킵니다.
|
||||||
|
- [x] **리치 텍스트 번역 강화**: 더 복잡한 페이지 구조와 리치 텍스트 콘텐츠의 정확한 번역을 지원합니다.
|
||||||
|
- [x] **사용자 정의/AI 인터페이스 강화**: 컨텍스트 기억, 다중 턴 대화 등 고급 AI 기능을 지원합니다.
|
||||||
|
- [x] **영어 사전 예비 메커니즘**: 번역 서비스가 실패할 경우 다른 사전으로 전환하거나 로컬 사전 조회로 대체합니다.
|
||||||
|
- [x] **YouTube 자막 지원 최적화**: 스트리밍 자막의 병합 및 번역 경험을 개선하고, 끊김을 줄입니다.
|
||||||
|
- [ ] **규칙 공동 구축 메커니즘 업그레이드**: 더 유연한 규칙 공유, 버전 관리 및 커뮤니티 검토 프로세스를 도입합니다.
|
||||||
|
|
||||||
|
특정 방향에 관심이 있다면, [Issues](https://github.com/fishjar/kiss-translator/issues)에서 토론하거나 PR을 제출해 주세요!
|
||||||
|
|
||||||
|
## 개발 가이드
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone [https://github.com/fishjar/kiss-translator.git](https://github.com/fishjar/kiss-translator.git)
|
||||||
|
cd kiss-translator
|
||||||
|
git checkout dev # PR 제출 시 dev 브랜치로 푸시하는 것을 권장합니다
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 커뮤니티
|
||||||
|
|
||||||
|
- [Telegram 그룹](https://t.me/+RRCu_4oNwrM2NmFl) 가입
|
||||||
|
|
||||||
|
## 후원
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
129
README.md
129
README.md
@@ -1,6 +1,8 @@
|
|||||||
# 简约翻译
|
# KISS Translator 简约翻译
|
||||||
|
|
||||||
一个简约的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
[English](README.en.md) | [中文](README.md) | [日本語](README.ja.md) | [한국어](README.ko.md)
|
||||||
|
|
||||||
|
一个简约、开源的 [双语对照翻译扩展 & 油猴脚本](https://github.com/fishjar/kiss-translator)。
|
||||||
|
|
||||||
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
[kiss-translator.webm](https://github.com/fishjar/kiss-translator/assets/1157624/f7ba8a5c-e4a8-4d5a-823a-5c5c67a0a47f)
|
||||||
|
|
||||||
@@ -9,29 +11,57 @@
|
|||||||
- [x] 保持简约
|
- [x] 保持简约
|
||||||
- [x] 开放源代码
|
- [x] 开放源代码
|
||||||
- [x] 适配常见浏览器
|
- [x] 适配常见浏览器
|
||||||
- [x] Chrome/Edge/Firefox/Kiwi
|
- [x] Chrome/Edge
|
||||||
- [ ] Safari
|
- [x] Firefox
|
||||||
|
- [x] Kiwi (Android)
|
||||||
|
- [x] Orion (iOS)
|
||||||
|
- [x] Safari
|
||||||
|
- [x] Thunderbird
|
||||||
- [x] 支持多种翻译服务
|
- [x] 支持多种翻译服务
|
||||||
- [x] Google/Microsoft/DeepL/OpenAI/CloudflareAI/Baidu/Tencent
|
- [x] Google/Microsoft
|
||||||
- [x] 自定义翻译接口
|
- [x] Tencent/Volcengine
|
||||||
|
- [x] OpenAI/Gemini/Claude/Ollama/DeepSeek/OpenRouter
|
||||||
|
- [x] DeepL/DeepLX/NiuTrans
|
||||||
|
- [x] AzureAI/CloudflareAI
|
||||||
|
- [x] Chrome浏览器内置AI翻译(BuiltinAI)
|
||||||
- [x] 覆盖常见翻译场景
|
- [x] 覆盖常见翻译场景
|
||||||
- [x] 网页双语对照翻译
|
- [x] 网页双语对照翻译
|
||||||
- [x] 输入框翻译
|
- [x] 输入框翻译
|
||||||
|
- 通过快捷键立即将输入框内文本翻译成其他语言
|
||||||
- [x] 划词翻译
|
- [x] 划词翻译
|
||||||
|
- [x] 任意页面打开翻译框,可用多种翻译服务对比翻译
|
||||||
|
- [x] 英文词典翻译
|
||||||
- [x] 收藏词汇
|
- [x] 收藏词汇
|
||||||
- [x] 鼠标悬停翻译
|
- [x] 鼠标悬停翻译
|
||||||
- [x] YouTube 字幕翻译
|
- [x] YouTube 字幕翻译
|
||||||
|
- 支持任意翻译服务对视频字幕进行翻译并双语显示
|
||||||
|
- 内置基础的字幕合并与断句算法,提升翻译效果
|
||||||
|
- 支持AI断句功能,可进一步提升翻译质量
|
||||||
|
- 自定义字幕样式
|
||||||
|
- [x] 支持多样翻译效果
|
||||||
|
- [x] 支持自动识别文本与手动规则两种模式
|
||||||
|
- 自动识别文本模式使得绝大部分网站无需编写规则也能翻译完整
|
||||||
|
- 手动规则模式,可以针对特定网站极致优化
|
||||||
|
- [x] 自定义译文样式
|
||||||
|
- [x] 支持富文本翻译及显示,能够尽量保留原文中的链接及其他文本样式
|
||||||
|
- [x] 支持仅显示译文(隐藏原文)
|
||||||
|
- [x] 翻译接口高级功能
|
||||||
|
- [x] 通过自定义接口,理论上支持任何翻译接口
|
||||||
|
- [x] 聚合批量发送翻译文本
|
||||||
|
- [x] 支持AI上下文会话记忆功能,提升翻译效果
|
||||||
|
- [x] 自定义AI术语词典
|
||||||
|
- [x] 所有接口均支持Hook和自定义参数等高级功能
|
||||||
- [x] 跨客户端数据同步
|
- [x] 跨客户端数据同步
|
||||||
- [x] KISS-Worker(cloudflare/docker)
|
- [x] KISS-Worker(cloudflare/docker)
|
||||||
- [x] WebDAV
|
- [x] WebDAV
|
||||||
- [x] 自定义翻译规则
|
- [x] 自定义翻译规则
|
||||||
- [x] 规则订阅/规则分享
|
- [x] 规则订阅/规则分享
|
||||||
- [x] 自定义译文样式
|
- [x] 自定义专业术语
|
||||||
- [x] 自定义快捷键
|
- [x] 自定义快捷键
|
||||||
- `Alt+Q` 开启翻译
|
- `Alt+Q` 开启翻译
|
||||||
- `Alt+C` 切换样式
|
- `Alt+C` 切换样式
|
||||||
- `Alt+K` 打开设置弹窗
|
- `Alt+K` 打开设置弹窗
|
||||||
- `Alt+B` 打开翻译弹窗
|
- `Alt+S` 打开翻译弹窗/翻译选中文字
|
||||||
- `Alt+O` 打开设置页面
|
- `Alt+O` 打开设置页面
|
||||||
- `Alt+I` 输入框翻译
|
- `Alt+I` 输入框翻译
|
||||||
|
|
||||||
@@ -39,17 +69,22 @@
|
|||||||
|
|
||||||
> 注:基于以下原因,建议优先使用浏览器扩展
|
> 注:基于以下原因,建议优先使用浏览器扩展
|
||||||
>
|
>
|
||||||
> - 浏览器扩展可以使用本地的语言识别
|
> - 浏览器扩展的功能更完整(本地语言识别、右键菜单等)
|
||||||
> - 油猴脚本会遇到更多使用上的问题
|
> - 油猴脚本会遇到更多使用上的问题(跨域问题、脚本冲突等)
|
||||||
|
|
||||||
- [x] 浏览器扩展
|
- [x] 浏览器扩展
|
||||||
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
- [x] Chrome [安装地址](https://chrome.google.com/webstore/detail/kiss-translator/bdiifdefkgmcblbcghdlonllpjhhjgof?hl=zh-CN)
|
||||||
|
- [x] Kiwi (Android)
|
||||||
|
- [x] Orion (iOS)
|
||||||
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
- [x] Edge [安装地址](https://microsoftedge.microsoft.com/addons/detail/%E7%AE%80%E7%BA%A6%E7%BF%BB%E8%AF%91/jemckldkclkinpjighnoilpbldbdmmlh?hl=zh-CN)
|
||||||
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
- [x] Firefox [安装地址](https://addons.mozilla.org/zh-CN/firefox/addon/kiss-translator/)
|
||||||
- [ ] Safari
|
- [ ] Safari
|
||||||
|
- [ ] Safari (Mac)
|
||||||
|
- [ ] Safari (iOS)
|
||||||
|
- [x] Thunderbird [下载地址](https://github.com/fishjar/kiss-translator/releases)
|
||||||
- [x] 油猴脚本
|
- [x] 油猴脚本
|
||||||
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
- [x] Chrome/Edge/Firefox ([Tampermonkey](https://www.tampermonkey.net/)/[Violentmonkey](https://violentmonkey.github.io/)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator.user.js)
|
||||||
- Greasy Fork [安装地址](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
- [Greasy Fork](https://greasyfork.org/zh-CN/scripts/472840-kiss-translator)
|
||||||
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
- [x] iOS Safari ([Userscripts Safari](https://github.com/quoid/userscripts)) [安装链接](https://fishjar.github.io/kiss-translator/kiss-translator-ios-safari.user.js)
|
||||||
|
|
||||||
## 关联项目
|
## 关联项目
|
||||||
@@ -61,22 +96,70 @@
|
|||||||
- 社区订阅规则: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
- 社区订阅规则: [https://github.com/fishjar/kiss-rules](https://github.com/fishjar/kiss-rules)
|
||||||
- 提供社区维护的,最新最全的订阅规则列表。
|
- 提供社区维护的,最新最全的订阅规则列表。
|
||||||
- 求助规则相关的问题。
|
- 求助规则相关的问题。
|
||||||
- 网页修正脚本: [https://github.com/fishjar/kiss-webfixer](https://github.com/fishjar/kiss-webfixer)
|
|
||||||
- 针对一些特殊网站的修正脚本。
|
## 常见问题
|
||||||
- 以便翻译软件得到更好的展示效果。
|
|
||||||
- 翻译接口代理: [https://github.com/fishjar/kiss-proxy](https://github.com/fishjar/kiss-proxy)
|
### 如何设置快捷键
|
||||||
- 如果访问某个翻译接口遇到网络问题,这个代理服务也许可以帮你到你。
|
|
||||||
- 自己部署,自己管理。
|
在插件管理那里设置,例如:
|
||||||
- 简约词典插件: [https://github.com/fishjar/kiss-dictionary](https://github.com/fishjar/kiss-dictionary)
|
|
||||||
- 搭配本项目一起使用的划词翻译插件。
|
- chrome [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
|
||||||
- 支持英文单词、句子、汉字的查询。
|
- firefox [about:addons](about:addons)
|
||||||
- 支持历史记录、单词收藏。
|
|
||||||
|
### 规则设置的优先级是如何的
|
||||||
|
|
||||||
|
个人规则 > 订阅规则 > 全局规则
|
||||||
|
|
||||||
|
其中全局规则优先级最低,但非常重要,相当于兜底规则。
|
||||||
|
|
||||||
|
### 接口(Ollama等)测试失败
|
||||||
|
|
||||||
|
一般接口测试失败常见有以下几种原因:
|
||||||
|
|
||||||
|
- 地址填错了:
|
||||||
|
- 比如 `Ollama` 有原生接口地址和 `Openai` 兼容的地址,本插件目前统一支持 `Openai` 兼容的地址,不支持 `Ollama` 原生接口地址
|
||||||
|
- 某些AI模型不支持聚合翻译:
|
||||||
|
- 此种情况可以选择禁用聚合翻译或通过自定义接口的方式来使用。
|
||||||
|
- 或通过自定义接口的方式来使用,详情参考: [自定义接口示例文档](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||||
|
- 某些AI模型的参数不一致:
|
||||||
|
- 比如 `Gemini` 原生接口参数非常不一致,部分版本的模型不支持某些参数会导致返回错误。
|
||||||
|
- 此种情况可以通过 `Hook` 修改请求 `body` ,或者更换为 `Gemini2` (`Openai` 兼容的地址)
|
||||||
|
- 服务器跨域限制访问,返回403错误:
|
||||||
|
- 比如 `Ollama` 启动时须添加环境变量 `OLLAMA_ORIGINS=*`, 参考:https://github.com/fishjar/kiss-translator/issues/174
|
||||||
|
|
||||||
|
### 填写的接口在油猴脚本不能使用
|
||||||
|
|
||||||
|
油猴脚本需要增加域名白名单,否则不能发出请求。
|
||||||
|
|
||||||
|
### 如何设置自定义接口的hook函数
|
||||||
|
|
||||||
|
自定义接口功能非常强大、灵活,理论可以接入任何翻译接口。
|
||||||
|
|
||||||
|
示例参考: [custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||||
|
|
||||||
|
### 如何直接进入油猴脚本设置页面
|
||||||
|
|
||||||
|
设置页面地址: https://fishjar.github.io/kiss-translator/options.html
|
||||||
|
|
||||||
|
## 未来规划
|
||||||
|
|
||||||
|
本项目为业余开发,无严格时间表,欢迎社区共建。以下为初步设想的功能方向:
|
||||||
|
|
||||||
|
- [x] **聚合发送文本**:优化请求策略,减少翻译接口调用次数,提升性能。
|
||||||
|
- [x] **增强富文本翻译**:支持更复杂的页面结构和富文本内容的准确翻译。
|
||||||
|
- [x] **强化自定义/AI 接口**:支持上下文记忆、多轮对话等高级 AI 功能。
|
||||||
|
- [x] **英文词典备灾机制**:当翻译服务失效时,可切换其他词典或 fallback 到本地词典查询。
|
||||||
|
- [x] **优化 YouTube 字幕支持**:改进流式字幕的合并与翻译体验,减少断句。
|
||||||
|
- [ ] **规则共建机制升级**:引入更灵活的规则分享、版本管理与社区评审流程。
|
||||||
|
|
||||||
|
如果你对某个方向感兴趣,欢迎在 [Issues](https://github.com/fishjar/kiss-translator/issues) 中讨论或提交 PR!
|
||||||
|
|
||||||
## 开发指引
|
## 开发指引
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/fishjar/kiss-translator.git
|
git clone https://github.com/fishjar/kiss-translator.git
|
||||||
cd kiss-translator
|
cd kiss-translator
|
||||||
|
git checkout dev # 提交PR建议推送到dev分支
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
```
|
```
|
||||||
@@ -84,3 +167,7 @@ pnpm build
|
|||||||
## 交流
|
## 交流
|
||||||
|
|
||||||
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
- 加入 [Telegram 群](https://t.me/+RRCu_4oNwrM2NmFl)
|
||||||
|
|
||||||
|
## 赞赏
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -32,6 +32,8 @@ const extWebpack = (config, env) => {
|
|||||||
options: paths.appSrc + "/options.js",
|
options: paths.appSrc + "/options.js",
|
||||||
background: paths.appSrc + "/background.js",
|
background: paths.appSrc + "/background.js",
|
||||||
content: paths.appSrc + "/content.js",
|
content: paths.appSrc + "/content.js",
|
||||||
|
"injector-subtitle": paths.appSrc + "/injector-subtitle.js",
|
||||||
|
"injector-shadowroot": paths.appSrc + "/injector-shadowroot.js",
|
||||||
};
|
};
|
||||||
|
|
||||||
config.output.filename = "[name].js";
|
config.output.filename = "[name].js";
|
||||||
@@ -85,31 +87,41 @@ const userscriptWebpack = (config, env) => {
|
|||||||
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
// @updateURL ${process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}
|
||||||
// @grant GM.xmlHttpRequest
|
// @grant GM.xmlHttpRequest
|
||||||
// @grant GM.registerMenuCommand
|
// @grant GM.registerMenuCommand
|
||||||
|
// @grant GM.unregisterMenuCommand
|
||||||
// @grant GM.setValue
|
// @grant GM.setValue
|
||||||
// @grant GM.getValue
|
// @grant GM.getValue
|
||||||
// @grant GM.deleteValue
|
// @grant GM.deleteValue
|
||||||
// @grant GM.info
|
// @grant GM.info
|
||||||
// @grant unsafeWindow
|
// @grant unsafeWindow
|
||||||
// @connect translate.googleapis.com
|
// @connect translate.googleapis.com
|
||||||
|
// @connect translate-pa.googleapis.com
|
||||||
|
// @connect generativelanguage.googleapis.com
|
||||||
// @connect api-edge.cognitive.microsofttranslator.com
|
// @connect api-edge.cognitive.microsofttranslator.com
|
||||||
// @connect edge.microsoft.com
|
// @connect edge.microsoft.com
|
||||||
|
// @connect bing.com
|
||||||
// @connect api-free.deepl.com
|
// @connect api-free.deepl.com
|
||||||
// @connect api.deepl.com
|
// @connect api.deepl.com
|
||||||
// @connect www2.deepl.com
|
// @connect www2.deepl.com
|
||||||
// @connect api.openai.com
|
// @connect api.openai.com
|
||||||
|
// @connect generativelanguage.googleapis.com
|
||||||
// @connect openai.azure.com
|
// @connect openai.azure.com
|
||||||
// @connect workers.dev
|
// @connect workers.dev
|
||||||
// @connect github.io
|
// @connect github.io
|
||||||
|
// @connect github.com
|
||||||
// @connect githubusercontent.com
|
// @connect githubusercontent.com
|
||||||
// @connect kiss-translator.rayjar.com
|
// @connect kiss-translator.rayjar.com
|
||||||
// @connect ghproxy.com
|
// @connect ghproxy.com
|
||||||
// @connect dav.jianguoyun.com
|
// @connect dav.jianguoyun.com
|
||||||
// @connect fanyi.baidu.com
|
// @connect fanyi.baidu.com
|
||||||
// @connect transmart.qq.com
|
// @connect transmart.qq.com
|
||||||
// @connect localhost:3000
|
// @connect niutrans.com
|
||||||
// @connect 127.0.0.1:3000
|
// @connect translate.volcengine.com
|
||||||
// @connect localhost:1188
|
// @connect dict.youdao.com
|
||||||
// @connect 127.0.0.1:1188
|
// @connect api.anthropic.com
|
||||||
|
// @connect api.cloudflare.com
|
||||||
|
// @connect openrouter.ai
|
||||||
|
// @connect localhost
|
||||||
|
// @connect 127.0.0.1
|
||||||
// @run-at document-end
|
// @run-at document-end
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
|
|||||||
348
custom-api.md
Normal file
348
custom-api.md
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
# 自定义接口示例(本文档已过期,新版不再适用)
|
||||||
|
|
||||||
|
V2版的示例请查看这里:[custom-api_v2.md](https://github.com/fishjar/kiss-translator/blob/master/custom-api_v2.md)
|
||||||
|
|
||||||
|
以下示例为网友提供,仅供学习参考。
|
||||||
|
|
||||||
|
## 本地运行 Seed-X-PPO-7B 量化模型
|
||||||
|
|
||||||
|
> 由网友 emptyghost6 提供,来源:https://linux.do/t/topic/828257
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
http://localhost:8000/v1/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => {
|
||||||
|
// 模型支持的语言代码到完整名称的映射
|
||||||
|
const langFullNameMap = {
|
||||||
|
ar: 'Arabic', fr: 'French', ms: 'Malay', ru: 'Russian',
|
||||||
|
cs: 'Czech', hr: 'Croatian', nb: 'Norwegian Bokmal', sv: 'Swedish',
|
||||||
|
da: 'Danish', hu: 'Hungarian', nl: 'Dutch', th: 'Thai',
|
||||||
|
de: 'German', id: 'Indonesian', no: 'Norwegian', tr: 'Turkish',
|
||||||
|
en: 'English', it: 'Italian', pl: 'Polish', uk: 'Ukrainian',
|
||||||
|
es: 'Spanish', ja: 'Japanese', pt: 'Portuguese', vi: 'Vietnamese',
|
||||||
|
fi: 'Finnish', ko: 'Korean', ro: 'Romanian', zh: 'Chinese'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将 Hook 系统的语言代码转换为模型 API 支持的代码
|
||||||
|
const getModelLangCode = (lang) => {
|
||||||
|
if (lang === 'zh-CN' || lang === 'zh-TW') return 'zh';
|
||||||
|
return lang;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceLangCode = getModelLangCode(from);
|
||||||
|
const targetLangCode = getModelLangCode(to);
|
||||||
|
|
||||||
|
const sourceLangName = langFullNameMap[sourceLangCode] || from;
|
||||||
|
const targetLangName = langFullNameMap[targetLangCode] || to;
|
||||||
|
|
||||||
|
const prompt = `Translate it to ${targetLangName}:\n${text} <${targetLangCode}>`;
|
||||||
|
|
||||||
|
// 构建请求体对象
|
||||||
|
const bodyObject = {
|
||||||
|
model: "./ByteDance-Seed/Seed-X-PPO-7B-AWQ-Int4",
|
||||||
|
prompt: prompt,
|
||||||
|
max_tokens: 2048,
|
||||||
|
temperature: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回最终的请求配置
|
||||||
|
return [url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
// 关键改动:将 JavaScript 对象转换为 JSON 字符串
|
||||||
|
body: JSON.stringify(bodyObject),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => {
|
||||||
|
// 检查返回是否有效
|
||||||
|
if (res && res.choices && res.choices.length > 0 && res.choices[0].text) {
|
||||||
|
|
||||||
|
// 提取译文并去除可能存在的前后空格
|
||||||
|
const translatedText = res.choices[0].text.trim();
|
||||||
|
|
||||||
|
// 比较原文与译文,相同为 true,否则为 false。
|
||||||
|
const areTextsIdentical = text.trim() === translatedText;
|
||||||
|
|
||||||
|
// 返回数组:[翻译后的文本, 是否与原文相同]
|
||||||
|
return [translatedText, areTextsIdentical];
|
||||||
|
}
|
||||||
|
// 如果响应格式不正确或没有结果,则抛出错误
|
||||||
|
throw new Error("Invalid API response format or no translation found.");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入 openrouter
|
||||||
|
|
||||||
|
> 由网友 Rick Sanchez 提供
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
https://openrouter.ai/api/v1/chat/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${key}`,
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"model": "deepseek/deepseek-chat-v3-0324:free", //可自定义你的模型
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": //可自定义你的提示词
|
||||||
|
`You are a professional ${to} native translator. Your task is to produce a fluent, natural, and culturally appropriate translation of the following text from ${from} to ${to}, fully conveying the meaning, tone, and nuance of the original.
|
||||||
|
|
||||||
|
## Translation Rules
|
||||||
|
1. Output only the final polished translation — no explanations, intermediate drafts, or notes.
|
||||||
|
2. Translate in a way that reads naturally to a native ${to} audience, adapting idioms, cultural references, and tone when necessary.
|
||||||
|
3. Preserve proper nouns, technical terms, brand names, and URLs exactly as in the original text unless a widely accepted ${to} equivalent exists.
|
||||||
|
4. Keep any formatting (Markdown, HTML tags, bullet points, numbering) intact and positioned naturally within the translation.
|
||||||
|
5. Adapt humor, metaphors, and figurative language to culturally relevant forms in ${to} while keeping the original intent.
|
||||||
|
6. Maintain the same level of formality or informality as the original.
|
||||||
|
|
||||||
|
Source Text: ${text}
|
||||||
|
|
||||||
|
Translated Text:`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [
|
||||||
|
res.choices?.[0]?.message?.content ?? "",
|
||||||
|
false
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入 gemini-2.5-flash, 关闭思考模式, 去审查
|
||||||
|
|
||||||
|
> 由网友 Rick Sanchez 提供
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
https://generativelanguage.googleapis.com/v1beta/models
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [`${url}/gemini-2.5-flash:generateContent?key=${key}`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
"generationConfig": {
|
||||||
|
"temperature": 0.8,
|
||||||
|
"thinkingConfig": {
|
||||||
|
"thinkingBudget": 0, //gemini-2.5-flash设为0关闭思考模式
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"safetySettings": [
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_HARASSMENT",
|
||||||
|
"threshold": "BLOCK_NONE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||||
|
"threshold": "BLOCK_NONE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||||
|
"threshold": "BLOCK_NONE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||||
|
"threshold": "BLOCK_NONE",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contents": [{
|
||||||
|
"parts": [{
|
||||||
|
"text": `自定义提示词`
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [
|
||||||
|
res.candidates?.[0]?.content?.parts?.[0]?.text ?? "",
|
||||||
|
false
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入 Qwen-MT
|
||||||
|
|
||||||
|
> 由网友 atom 提供
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => {
|
||||||
|
const mapLanguageCode = (lang) => ({
|
||||||
|
'zh-CN': 'zh',
|
||||||
|
'zh-TW': 'zh_tw',
|
||||||
|
})[lang] || lang;
|
||||||
|
|
||||||
|
const targetLang = mapLanguageCode(to);
|
||||||
|
|
||||||
|
return [
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${key}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"model": "qwen-mt-turbo",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": text
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"translation_options": {
|
||||||
|
"source_lang": "auto",
|
||||||
|
"target_lang": targetLang
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [res.choices?.[0]?.message?.content ?? "", false]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 接入 deepl 接口
|
||||||
|
|
||||||
|
> 来源: https://github.com/fishjar/kiss-translator/issues/101#issuecomment-2123786236
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
text,
|
||||||
|
target_lang: "ZH",
|
||||||
|
source_lang: "auto",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [res.data, "ZH" === res.source_lang]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入智谱AI大模型
|
||||||
|
|
||||||
|
> 来源: https://github.com/fishjar/kiss-translator/issues/205#issuecomment-2642422679
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
"method": "POST",
|
||||||
|
"headers": {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
"Authorization": key
|
||||||
|
},
|
||||||
|
"body": JSON.stringify({
|
||||||
|
"model": "glm-4-flash",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role":"system",
|
||||||
|
"content": "You are a professional, authentic machine translation engine. You only return the translated text, without any explanations."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": `Translate the following text into ${to}. If translation is unnecessary (e.g. proper nouns, codes, etc.), return the original text. NO explanations. NO notes:\n\n ${text} `
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 接入谷歌新接口
|
||||||
|
|
||||||
|
> 由网友 Bush2021 提供,来源:https://github.com/fishjar/kiss-translator/issues/225#issuecomment-2810950717
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```sh
|
||||||
|
https://translate-pa.googleapis.com/v1/translateHtml
|
||||||
|
```
|
||||||
|
|
||||||
|
KEY
|
||||||
|
|
||||||
|
```sh
|
||||||
|
AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(text, from, to, url, key) => [url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json+protobuf",
|
||||||
|
"X-Goog-API-Key": key
|
||||||
|
},
|
||||||
|
body: JSON.stringify([[[text], from || "auto", to], "wt_lib"])
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
(res, text, from, to) => [res?.[0]?.join(" ") || "Translation unavailable", to === res?.[1]?.[0]]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
387
custom-api_v2.md
Normal file
387
custom-api_v2.md
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
# 自定义接口说明及示例
|
||||||
|
|
||||||
|
## 默认接口规范
|
||||||
|
|
||||||
|
如果接口的请求数据和返回数据符合以下规范,
|
||||||
|
则无需填写 `Request Hook` 或 `Response Hook`。
|
||||||
|
|
||||||
|
|
||||||
|
### 非聚合翻译 (v2.0.9)
|
||||||
|
|
||||||
|
Request body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "hello", // 需要翻译的文本列表
|
||||||
|
"from":"auto", // 原文语言
|
||||||
|
"to": "zh-CN" // 目标语言
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "你好", // 译文
|
||||||
|
"src": "en" // 原文语言
|
||||||
|
}
|
||||||
|
|
||||||
|
// 或者
|
||||||
|
{
|
||||||
|
"text": "你好", // 译文
|
||||||
|
"from": "en" // 原文语言
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 聚合翻译
|
||||||
|
|
||||||
|
Request body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"texts": ["hello"], // 需要翻译的文本列表
|
||||||
|
"from":"auto", // 原文语言
|
||||||
|
"to": "zh-CN" // 目标语言
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"text": "你好", // 译文
|
||||||
|
"src": "en" // 原文语言
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
v2.0.4版后亦支持以下 Response 格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"translations": [ // 译文列表
|
||||||
|
{
|
||||||
|
"text": "你好", // 译文
|
||||||
|
"src": "en" // 原文语言
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prompt 相关
|
||||||
|
|
||||||
|
`Prompt` 可替换占位符:
|
||||||
|
|
||||||
|
```js
|
||||||
|
`{{from}}` // 原文语言名称
|
||||||
|
`{{to}}` // 目标语言名称
|
||||||
|
`{{fromLang}}` // 原文语言代码
|
||||||
|
`{{toLang}}` // 目标语言代码
|
||||||
|
`{{text}}` // 原文
|
||||||
|
`{{tone}}` // 风格
|
||||||
|
`{{title}}` // 页面标题
|
||||||
|
`{{description}}` // 页面描述
|
||||||
|
```
|
||||||
|
|
||||||
|
Hook 中 `Prompt` 类型说明:
|
||||||
|
|
||||||
|
```js
|
||||||
|
`systemPrompt` // 聚合翻译 System Prompt
|
||||||
|
`nobatchPrompt` // 非聚合翻译 System Prompt
|
||||||
|
`nobatchUserPrompt` // 非聚合翻译 User Prompt
|
||||||
|
`subtitlePrompt` // 字幕翻译 System Prompt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 谷歌翻译接口
|
||||||
|
|
||||||
|
> 此接口不支持聚合
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
async (args) => {
|
||||||
|
const url = args.url.replace("{{text}}", args.texts[0]);
|
||||||
|
const method = "GET";
|
||||||
|
return { url, method };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
async ({ res }) => {
|
||||||
|
return { translations: [[res?.sentences?.[0]?.trans || "", res?.src]] };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Ollama
|
||||||
|
|
||||||
|
> 此示例为支持聚合的模型类(要支持上下文,需进一步改动)
|
||||||
|
|
||||||
|
* 注意 ollama 启动参数需要添加环境变量 `OLLAMA_ORIGINS=*`
|
||||||
|
* 检查环境变量生效命令:`systemctl show ollama | grep OLLAMA_ORIGINS`
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:11434/v1/chat/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
async (args) => {
|
||||||
|
const url = args.url;
|
||||||
|
const method = "POST";
|
||||||
|
const headers = { "Content-type": "application/json" };
|
||||||
|
const body = {
|
||||||
|
model: "gemma3",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content:
|
||||||
|
'Act as a translation API. Output a single raw JSON object only. No extra text or fences.\n\nInput:\n{"targetLanguage":"<lang>","title":"<context>","description":"<context>","segments":[{"id":1,"text":"..."}],"glossary":{"sourceTerm":"targetTerm"},"tone":"<formal|casual>"}\n\nOutput:\n{"translations":[{"id":1,"text":"...","sourceLanguage":"<detected>"}]}\n\nRules:\n1. Use title/description for context only; do not output them.\n2. Keep id, order, and count of segments.\n3. Preserve whitespace, HTML entities, and all HTML-like tags (e.g., <i1>, <a1>). Translate inner text only.\n4. Highest priority: Follow \'glossary\'. Use value for translation; if value is "", keep the key.\n5. Do not translate: content in <code>, <pre>, text enclosed in backticks, or placeholders like {1}, {{1}}, [1], [[1]].\n6. Apply the specified tone to the translation.\n7. Detect sourceLanguage for each segment.\n8. Return empty or unchanged inputs as is.\n\nExample:\nInput: {"targetLanguage":"zh-CN","segments":[{"id":1,"text":"A <b>React</b> component."}],"glossary":{"component":"组件","React":""}}\nOutput: {"translations":[{"id":1,"text":"一个<b>React</b>组件","sourceLanguage":"en"}]}\n\nFail-safe: On any error, return {"translations":[]}.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: JSON.stringify({
|
||||||
|
targetLanguage: args.toLang,
|
||||||
|
segments: args.texts.map((text, id) => ({ id, text })),
|
||||||
|
title: "", // 可省略
|
||||||
|
description: "", // 可省略
|
||||||
|
glossary: {}, // 可省略
|
||||||
|
tone: "", // 可省略
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
max_tokens: 20480,
|
||||||
|
think: false,
|
||||||
|
stream: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { url, body, headers, method };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
v2.0.2 Request Hook 可以简化为:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async (args) => {
|
||||||
|
const url = args.url;
|
||||||
|
const method = "POST";
|
||||||
|
const headers = { "Content-type": "application/json" };
|
||||||
|
const body = {
|
||||||
|
model: "gemma3", // v2.0.2 版后此处可填 args.model
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: args.defaultSystemPrompt, // 或者 args.systemPrompt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: JSON.stringify({
|
||||||
|
targetLanguage: args.toLang,
|
||||||
|
segments: args.texts.map((text, id) => ({ id, text })),
|
||||||
|
title: "", // 可省略
|
||||||
|
description: "", // 可省略
|
||||||
|
glossary: {}, // 可省略
|
||||||
|
tone: "", // 可省略
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
max_tokens: 20480,
|
||||||
|
think: false,
|
||||||
|
stream: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { url, body, headers, method };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
async ({ res }) => {
|
||||||
|
const extractJson = (raw) => {
|
||||||
|
const jsonRegex = /({.*}|\[.*\])/s;
|
||||||
|
const match = raw.match(jsonRegex);
|
||||||
|
return match ? match[0] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseAIRes = (raw) => {
|
||||||
|
if (!raw) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonString = extractJson(raw);
|
||||||
|
if (!jsonString) return [];
|
||||||
|
|
||||||
|
const data = JSON.parse(jsonString);
|
||||||
|
if (Array.isArray(data.translations)) {
|
||||||
|
return data.translations.map((item) => [
|
||||||
|
item?.text ?? "",
|
||||||
|
item?.sourceLanguage ?? "",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("parseAIRes", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const translations = parseAIRes(res?.choices?.[0]?.message?.content);
|
||||||
|
|
||||||
|
return { translations };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
v2.0.2 版后内置`parseAIRes`函数,Response Hook 可以简化为:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async ({ res, parseAIRes }) => {
|
||||||
|
const translations = parseAIRes(res?.choices?.[0]?.message?.content);
|
||||||
|
return { translations };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 硅基流动
|
||||||
|
|
||||||
|
> 此示例为不支持聚合模型类,支持聚合的模型类参考上面 Ollama 的写法
|
||||||
|
|
||||||
|
URL
|
||||||
|
|
||||||
|
```
|
||||||
|
https://api.siliconflow.cn/v1/chat/completions
|
||||||
|
```
|
||||||
|
|
||||||
|
Request Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
async (args) => {
|
||||||
|
const url = args.url;
|
||||||
|
const method = "POST";
|
||||||
|
const headers = {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
Authorization: `Bearer ${args.key}`,
|
||||||
|
};
|
||||||
|
const body = {
|
||||||
|
model: "tencent/Hunyuan-MT-7B", // v2.0.2 版后此处可填 args.model
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content:
|
||||||
|
"You are a professional, authentic machine translation engine.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: `Translate the following source text to ${args.to}. Output translation directly without any additional text.\n\nSource Text: ${args.texts[0]}\n\nTranslated Text:`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
max_tokens: 20480,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { url, body, headers, method };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
v2.0.6 版后内置默认 prompt,Response Hook 可以简化为:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async (args) => {
|
||||||
|
const url = args.url;
|
||||||
|
const method = "POST";
|
||||||
|
const headers = {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
Authorization: `Bearer ${args.key}`,
|
||||||
|
};
|
||||||
|
const body = {
|
||||||
|
model: "tencent/Hunyuan-MT-7B", // 或 args.model
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: args.defaultNobatchPrompt, // 或 args.nobatchPrompt
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: args.defaultNobatchUserPrompt, // 或 args.nobatchUserPrompt
|
||||||
|
},
|
||||||
|
],
|
||||||
|
temperature: 0,
|
||||||
|
max_tokens: 20480,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { url, body, headers, method };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Response Hook
|
||||||
|
|
||||||
|
```js
|
||||||
|
async ({ res }) => {
|
||||||
|
return { translations: [[res?.choices?.[0]?.message?.content || ""]] };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 语言代码表及说明
|
||||||
|
|
||||||
|
Hook参数里面的语言含义说明:
|
||||||
|
|
||||||
|
- `toLang`, `fromLang` 是本插件支持的标准语言代码
|
||||||
|
- `to`, `from` 是转换后的适用于特定接口的语言代码
|
||||||
|
|
||||||
|
如果你的自定义接口与下面的标准语言代码不匹配,需要自行映射转换。
|
||||||
|
|
||||||
|
```
|
||||||
|
["en", "English - English"],
|
||||||
|
["zh-CN", "Simplified Chinese - 简体中文"],
|
||||||
|
["zh-TW", "Traditional Chinese - 繁體中文"],
|
||||||
|
["ar", "Arabic - العربية"],
|
||||||
|
["bg", "Bulgarian - Български"],
|
||||||
|
["ca", "Catalan - Català"],
|
||||||
|
["hr", "Croatian - Hrvatski"],
|
||||||
|
["cs", "Czech - Čeština"],
|
||||||
|
["da", "Danish - Dansk"],
|
||||||
|
["nl", "Dutch - Nederlands"],
|
||||||
|
["fa", "Persian - فارسی"],
|
||||||
|
["fi", "Finnish - Suomi"],
|
||||||
|
["fr", "French - Français"],
|
||||||
|
["de", "German - Deutsch"],
|
||||||
|
["el", "Greek - Ελληνικά"],
|
||||||
|
["hi", "Hindi - हिन्दी"],
|
||||||
|
["hu", "Hungarian - Magyar"],
|
||||||
|
["id", "Indonesian - Indonesia"],
|
||||||
|
["it", "Italian - Italiano"],
|
||||||
|
["ja", "Japanese - 日本語"],
|
||||||
|
["ko", "Korean - 한국어"],
|
||||||
|
["ms", "Malay - Melayu"],
|
||||||
|
["mt", "Maltese - Malti"],
|
||||||
|
["nb", "Norwegian - Norsk Bokmål"],
|
||||||
|
["pl", "Polish - Polski"],
|
||||||
|
["pt", "Portuguese - Português"],
|
||||||
|
["ro", "Romanian - Română"],
|
||||||
|
["ru", "Russian - Русский"],
|
||||||
|
["sk", "Slovak - Slovenčina"],
|
||||||
|
["sl", "Slovenian - Slovenščina"],
|
||||||
|
["es", "Spanish - Español"],
|
||||||
|
["sv", "Swedish - Svenska"],
|
||||||
|
["ta", "Tamil - தமிழ்"],
|
||||||
|
["te", "Telugu - తెలుగు"],
|
||||||
|
["th", "Thai - ไทย"],
|
||||||
|
["tr", "Turkish - Türkçe"],
|
||||||
|
["uk", "Ukrainian - Українська"],
|
||||||
|
["vi", "Vietnamese - Tiếng Việt"],
|
||||||
|
```
|
||||||
42
package.json
42
package.json
@@ -1,35 +1,44 @@
|
|||||||
{
|
{
|
||||||
"name": "kiss-translator",
|
"name": "kiss-translator",
|
||||||
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
"description": "A minimalist bilingual translation Extension & Greasemonkey Script",
|
||||||
"version": "1.7.12",
|
"version": "2.0.12",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/cache": "^11.11.0",
|
"@emotion/cache": "^11.11.0",
|
||||||
|
"@emotion/css": "^11.13.5",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.10.8",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/icons-material": "^5.11.11",
|
"@mui/icons-material": "^5.15.15",
|
||||||
"@mui/material": "^5.11.12",
|
"@mui/lab": "5.0.0-alpha.170",
|
||||||
|
"@mui/material": "^5.15.15",
|
||||||
"query-string": "^8.1.0",
|
"query-string": "^8.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.10.0",
|
"react-router-dom": "^6.16.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"sval": "^0.5.2",
|
||||||
"webdav": "^5.3.0",
|
"webdav": "^5.3.0",
|
||||||
"webextension-polyfill": "^0.10.0"
|
"webextension-polyfill": "^0.10.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "REACT_APP_CLIENT=web react-app-rewired start",
|
"start": "REACT_APP_CLIENT=web react-app-rewired start",
|
||||||
"start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start",
|
"start:userscript": "REACT_APP_CLIENT=userscript react-app-rewired start",
|
||||||
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build",
|
"build:chrome": "rm -rf build/chrome && BUILD_PATH=./build/chrome REACT_APP_CLIENT=chrome react-app-rewired build && rm build/chrome/content.html",
|
||||||
|
"build:safari-output": "rm -rf build/safari && BUILD_PATH=./build/safari REACT_APP_CLIENT=safari react-app-rewired build && rm build/safari/content.html",
|
||||||
|
"build:safari": "node src/scripts/build-safari.js",
|
||||||
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
|
"build:edge": "rm -rf build/edge && cp -r build/chrome build/edge",
|
||||||
"build:firefox": "rm -rf build/firefox && cp -r build/chrome build/firefox && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json",
|
"build:thunderbird": "rm -rf build/thunderbird && BUILD_PATH=./build/thunderbird REACT_APP_CLIENT=thunderbird react-app-rewired build && rm build/thunderbird/content.html && cp ./build/thunderbird/manifest.thunderbird.json ./build/thunderbird/manifest.json && rm build/*/manifest.thunderbird.json",
|
||||||
|
"build:firefox": "rm -rf build/firefox && BUILD_PATH=./build/firefox REACT_APP_CLIENT=firefox react-app-rewired build && rm build/firefox/content.html && cat ./build/firefox/manifest.firefox.json > ./build/firefox/manifest.json && rm build/*/manifest.firefox.json",
|
||||||
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
"build:web": "rm -rf build/web && BUILD_PATH=./build/web REACT_APP_CLIENT=userscript react-app-rewired build",
|
||||||
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
|
"build:userscript-ios": "file1=build/web/kiss-translator.user.js file2=build/web/kiss-translator-ios-safari.user.js && cp $file1 $file2 && sed -i 's|// @grant unsafeWindow|// @inject-into content|g' $file2",
|
||||||
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
|
"build:userscript": "rm -rf build/userscript && mkdir build/userscript && cp build/web/*.user.js build/userscript/",
|
||||||
"build:rules": "babel-node src/rules.js",
|
"build:rules": "babel-node src/rules.js",
|
||||||
"build": "pnpm build:chrome && pnpm build:edge && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
|
"build": "pnpm format && pnpm build:chrome && pnpm build:edge && pnpm build:thunderbird && pnpm build:firefox && pnpm build:web && pnpm build:userscript-ios && pnpm build:userscript && pnpm build:rules",
|
||||||
|
"zip": "cd build && rm -f *.zip && zip -r chrome.zip chrome && zip -r edge.zip edge && zip -r userscript.zip userscript && (cd firefox && zip -r ../firefox.zip .) && (cd thunderbird && zip -r ../thunderbird.zip .)",
|
||||||
|
"build+zip": "pnpm build && pnpm zip",
|
||||||
|
"format": "prettier --write \"**/*.{js,json,html}\"",
|
||||||
"test": "react-app-rewired test",
|
"test": "react-app-rewired test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
@@ -41,7 +50,10 @@
|
|||||||
"globals": {
|
"globals": {
|
||||||
"GM": true,
|
"GM": true,
|
||||||
"unsafeWindow": true,
|
"unsafeWindow": true,
|
||||||
"globalThis": true
|
"globalThis": true,
|
||||||
|
"messenger": true,
|
||||||
|
"LanguageDetector": true,
|
||||||
|
"Translator": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
@@ -57,10 +69,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.10",
|
"@babel/core": "^7.22.20",
|
||||||
"@babel/node": "^7.22.10",
|
"@babel/node": "^7.22.19",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-env": "^7.22.10",
|
"@babel/preset-env": "^7.22.20",
|
||||||
"react-app-rewired": "^2.2.1"
|
"dotenv": "^17.2.1",
|
||||||
|
"find-up": "^7.0.0",
|
||||||
|
"prettier": "3.6.2",
|
||||||
|
"react-app-rewired": "^2.2.1",
|
||||||
|
"zx": "^8.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12734
pnpm-lock.yaml
generated
12734
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
20
public/_locales/de/messages.json
Normal file
20
public/_locales/de/messages.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"app_name": {
|
||||||
|
"message": "KISS Übersetzer"
|
||||||
|
},
|
||||||
|
"app_description": {
|
||||||
|
"message": "Eine einfache zweisprachige Übersetzungs-Erweiterung und Greasemonkey-Skript"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "Übersetzung umschalten"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "Stile umschalten"
|
||||||
|
},
|
||||||
|
"open_options": {
|
||||||
|
"message": "Einstellungen öffnen"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "Popup-Fenster öffnen"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,15 @@
|
|||||||
"message": "A simple bilingual translation extension & Greasemonkey script"
|
"message": "A simple bilingual translation extension & Greasemonkey script"
|
||||||
},
|
},
|
||||||
"toggle_translate": {
|
"toggle_translate": {
|
||||||
"message": "Toggle Translate"
|
"message": "Toggle Translation"
|
||||||
},
|
},
|
||||||
"toggle_style": {
|
"toggle_style": {
|
||||||
"message": "Toggle Style"
|
"message": "Toggle Styles"
|
||||||
},
|
},
|
||||||
"open_options": {
|
"open_options": {
|
||||||
"message": "Open Options"
|
"message": "Open Setting"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "Open Popup Box"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
public/_locales/es/messages.json
Normal file
20
public/_locales/es/messages.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"app_name": {
|
||||||
|
"message": "KISS Traductor"
|
||||||
|
},
|
||||||
|
"app_description": {
|
||||||
|
"message": "Una sencilla extensión y script de Greasemonkey para traducción bilingüe"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "Alternar traducción"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "Cambiar estilo"
|
||||||
|
},
|
||||||
|
"open_options": {
|
||||||
|
"message": "Abrir configuración"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "Abrir ventana emergente"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
public/_locales/fr/messages.json
Normal file
20
public/_locales/fr/messages.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"app_name": {
|
||||||
|
"message": "KISS Traducteur"
|
||||||
|
},
|
||||||
|
"app_description": {
|
||||||
|
"message": "Une extension et un script Greasemonkey de traduction bilingue simple"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "Activer/désactiver la traduction"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "Changer de style"
|
||||||
|
},
|
||||||
|
"open_options": {
|
||||||
|
"message": "Ouvrir les paramètres"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "Ouvrir la fenêtre contextuelle"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
public/_locales/ja/messages.json
Normal file
20
public/_locales/ja/messages.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"app_name": {
|
||||||
|
"message": "シンプル翻訳"
|
||||||
|
},
|
||||||
|
"app_description": {
|
||||||
|
"message": "シンプルなバイリンガル対訳翻訳拡張機能&Tampermonkeyスクリプト"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "翻訳の切り替え"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "スタイル切り替え"
|
||||||
|
},
|
||||||
|
"open_options": {
|
||||||
|
"message": "設定を開く"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "ポップアップを開く"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
public/_locales/ko/messages.json
Normal file
20
public/_locales/ko/messages.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"app_name": {
|
||||||
|
"message": "심플 번역"
|
||||||
|
},
|
||||||
|
"app_description": {
|
||||||
|
"message": "심플한 이중 언어 대조 번역 확장 프로그램 & Tampermonkey 스크립트"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "번역 켜기"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "스타일 전환"
|
||||||
|
},
|
||||||
|
"open_options": {
|
||||||
|
"message": "설정 열기"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "팝업 열기"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,5 +13,8 @@
|
|||||||
},
|
},
|
||||||
"open_options": {
|
"open_options": {
|
||||||
"message": "打开设置"
|
"message": "打开设置"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "打开弹窗"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
public/_locales/zh_TW/messages.json
Normal file
20
public/_locales/zh_TW/messages.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"app_name": {
|
||||||
|
"message": "簡約翻譯"
|
||||||
|
},
|
||||||
|
"app_description": {
|
||||||
|
"message": "一個簡約的雙語對照翻譯擴充功能與 Tampermonkey 腳本"
|
||||||
|
},
|
||||||
|
"toggle_translate": {
|
||||||
|
"message": "開啟翻譯"
|
||||||
|
},
|
||||||
|
"toggle_style": {
|
||||||
|
"message": "切換樣式"
|
||||||
|
},
|
||||||
|
"open_options": {
|
||||||
|
"message": "開啟設定"
|
||||||
|
},
|
||||||
|
"open_tranbox": {
|
||||||
|
"message": "開啟彈出視窗"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,298 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>%REACT_APP_NAME%</title>
|
<title>%REACT_APP_NAME%</title>
|
||||||
<style>
|
|
||||||
img {
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
max-width: 1.2em;
|
|
||||||
max-height: 1.2em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
// (() => {
|
|
||||||
// var shadow = document.querySelector("#shadow1");
|
|
||||||
// var root = shadow.attachShadow({ mode: "open" });
|
|
||||||
// var newLine = document.createElement("p");
|
|
||||||
// newLine.innerText = "new line";
|
|
||||||
// root.appendChild(newLine);
|
|
||||||
// })();
|
|
||||||
|
|
||||||
// setTimeout(function () {
|
|
||||||
// var shadow = document.querySelector("#shadow2");
|
|
||||||
// var root = shadow.attachShadow({ mode: "open" });
|
|
||||||
// }, 1000);
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// var newLine = document.createElement("p");
|
|
||||||
// newLine.innerText = "new line";
|
|
||||||
// var shadow = document.querySelector("#shadow2");
|
|
||||||
// shadow.shadowRoot.appendChild(newLine);
|
|
||||||
// }, 2000);
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
|
||||||
// var newLine = document.createElement("div");
|
|
||||||
// newLine.innerHTML = "<p>second line</p><p>third line</p>";
|
|
||||||
// var shadow = document.querySelector("#shadow2");
|
|
||||||
// shadow.shadowRoot.appendChild(newLine);
|
|
||||||
// }, 3000);
|
|
||||||
|
|
||||||
// setTimeout(function () {
|
|
||||||
// var el = document.querySelector("h2");
|
|
||||||
// el.innerText = "hello world";
|
|
||||||
|
|
||||||
// var title = document.querySelector("#addtitle");
|
|
||||||
// title.innerHTML =
|
|
||||||
// "<div><p>second title</p><ul><li>second title</li><li><p>second title</p></li></ul></div>";
|
|
||||||
// }, 1000);
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
var el = document.querySelector("h2>p>span");
|
|
||||||
el.innerText = "hello world";
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root">
|
<div id="root">
|
||||||
<div id="content">
|
|
||||||
<p>You need to enable JavaScript to run <span>this app.</span></p>
|
|
||||||
The <span>embargo</span> has just lifted to confirm that AmpereOne is
|
|
||||||
coming to Google Cloud with the C3A instances.
|
|
||||||
<br />
|
|
||||||
But these upcoming instances for now are only in private preview form.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Needless to say I also haven't had any AmpereOne access to check out the
|
|
||||||
performance and power efficiency of these new Arm server processors from
|
|
||||||
Ampere Computing.
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
<h2>
|
|
||||||
<p>
|
|
||||||
<span
|
|
||||||
>React is a JavaScript library for building user interfaces.</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</h2>
|
|
||||||
<hr />
|
|
||||||
<input id="input1" style="width: 80%;" />
|
|
||||||
<hr />
|
|
||||||
<textarea id="textarea1" style="width: 80%;">test</textarea>
|
|
||||||
<hr />
|
|
||||||
<div id="addtitle"></div>
|
|
||||||
<h2>Shadow 1</h2>
|
|
||||||
<div id="shadow1"></div>
|
|
||||||
<h2>Shadow 2</h2>
|
|
||||||
<div id="shadow2"></div>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<h2>
|
|
||||||
React Server Components (or RSC) is a new application architecture
|
|
||||||
designed by the React team.
|
|
||||||
</h2>
|
|
||||||
<iframe
|
|
||||||
id="iframe1"
|
|
||||||
width="800px"
|
|
||||||
height="600px"
|
|
||||||
src="http://localhost:3000/index.html"
|
|
||||||
></iframe>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<h2>
|
|
||||||
We’ve first shared our research on RSC in an introductory talk and an
|
|
||||||
RFC.
|
|
||||||
</h2>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<h2>
|
|
||||||
To recap them, we are introducing a new kind of component—Server
|
|
||||||
Components—that run ahead of time and are excluded from your JavaScript
|
|
||||||
bundle.
|
|
||||||
</h2>
|
|
||||||
<iframe
|
|
||||||
id="iframe2"
|
|
||||||
width="800px"
|
|
||||||
height="600px"
|
|
||||||
src="https://react.dev/"
|
|
||||||
></iframe>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<div class="cont cont1">
|
|
||||||
<h2>
|
|
||||||
Server Components can run during the build, letting you read from the
|
|
||||||
filesystem or fetch static content.
|
|
||||||
</h2>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
They can also run on the server, letting you access your data layer
|
|
||||||
without having to build an API. You can pass data by props from
|
|
||||||
Server Components to the interactive Client Components in the
|
|
||||||
browser.
|
|
||||||
</li>
|
|
||||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<div class="cont cont2">
|
|
||||||
<h2>
|
|
||||||
Since our last update, we have merged the React Server Components RFC
|
|
||||||
to ratify the proposal.
|
|
||||||
</h2>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
RSC combines the simple “request/response” mental model of
|
|
||||||
server-centric Multi-Page Apps with the seamless interactivity of
|
|
||||||
client-centric Single-Page Apps, giving you the best of both worlds.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
React 使创建交互式 UI
|
|
||||||
变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据变动时 React
|
|
||||||
能高效更新并渲染合适的组件。
|
|
||||||
</li>
|
|
||||||
<li>以声明式编写 UI,可以让你的代码更加可靠,且方便调试。</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!--
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.7.12",
|
"version": "2.0.12",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
@@ -12,10 +12,14 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"js": ["content.js"],
|
"js": ["content.js"],
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["<all_urls>", "file://*/*"],
|
||||||
"all_frames": true
|
"all_frames": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"injector-subtitle.js",
|
||||||
|
"injector-shadowroot.js"
|
||||||
|
],
|
||||||
"commands": {
|
"commands": {
|
||||||
"_execute_browser_action": {
|
"_execute_browser_action": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
@@ -28,6 +32,12 @@
|
|||||||
},
|
},
|
||||||
"description": "__MSG_toggle_translate__"
|
"description": "__MSG_toggle_translate__"
|
||||||
},
|
},
|
||||||
|
"openTranbox": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+S"
|
||||||
|
},
|
||||||
|
"description": "__MSG_open_tranbox__"
|
||||||
|
},
|
||||||
"toggleStyle": {
|
"toggleStyle": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
"default": "Alt+C"
|
"default": "Alt+C"
|
||||||
@@ -35,13 +45,16 @@
|
|||||||
"description": "__MSG_toggle_style__"
|
"description": "__MSG_toggle_style__"
|
||||||
},
|
},
|
||||||
"openOptions": {
|
"openOptions": {
|
||||||
"suggested_key": {
|
|
||||||
"default": "Alt+O"
|
|
||||||
},
|
|
||||||
"description": "__MSG_open_options__"
|
"description": "__MSG_open_options__"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permissions": ["<all_urls>", "storage"],
|
"permissions": [
|
||||||
|
"<all_urls>",
|
||||||
|
"storage",
|
||||||
|
"contextMenus",
|
||||||
|
"scripting",
|
||||||
|
"declarativeNetRequest"
|
||||||
|
],
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "images/logo16.png",
|
"16": "images/logo16.png",
|
||||||
"32": "images/logo32.png",
|
"32": "images/logo32.png",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_app_name__",
|
"name": "__MSG_app_name__",
|
||||||
"description": "__MSG_app_description__",
|
"description": "__MSG_app_description__",
|
||||||
"version": "1.7.12",
|
"version": "2.0.12",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"author": "Gabe<yugang2002@gmail.com>",
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
@@ -13,10 +13,20 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"js": ["content.js"],
|
"js": ["content.js"],
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["<all_urls>", "file://*/*"],
|
||||||
"all_frames": true
|
"all_frames": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["injector-subtitle.js"],
|
||||||
|
"matches": ["https://www.youtube.com/*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resources": ["injector-shadowroot.js"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"commands": {
|
"commands": {
|
||||||
"_execute_action": {
|
"_execute_action": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
@@ -29,6 +39,12 @@
|
|||||||
},
|
},
|
||||||
"description": "__MSG_toggle_translate__"
|
"description": "__MSG_toggle_translate__"
|
||||||
},
|
},
|
||||||
|
"openTranbox": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+S"
|
||||||
|
},
|
||||||
|
"description": "__MSG_open_tranbox__"
|
||||||
|
},
|
||||||
"toggleStyle": {
|
"toggleStyle": {
|
||||||
"suggested_key": {
|
"suggested_key": {
|
||||||
"default": "Alt+C"
|
"default": "Alt+C"
|
||||||
@@ -36,13 +52,10 @@
|
|||||||
"description": "__MSG_toggle_style__"
|
"description": "__MSG_toggle_style__"
|
||||||
},
|
},
|
||||||
"openOptions": {
|
"openOptions": {
|
||||||
"suggested_key": {
|
|
||||||
"default": "Alt+O"
|
|
||||||
},
|
|
||||||
"description": "__MSG_open_options__"
|
"description": "__MSG_open_options__"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permissions": ["storage"],
|
"permissions": ["storage", "contextMenus", "scripting", "declarativeNetRequest", "declarativeNetRequestWithHostAccess"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "images/logo16.png",
|
"16": "images/logo16.png",
|
||||||
|
|||||||
82
public/manifest.thunderbird.json
Normal file
82
public/manifest.thunderbird.json
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 2,
|
||||||
|
"name": "__MSG_app_name__",
|
||||||
|
"description": "__MSG_app_description__",
|
||||||
|
"version": "2.0.12",
|
||||||
|
"default_locale": "en",
|
||||||
|
"author": "Gabe<yugang2002@gmail.com>",
|
||||||
|
"homepage_url": "https://github.com/fishjar/kiss-translator",
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "yugang2002@gmail.com",
|
||||||
|
"strict_min_version": "78.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"scripts": ["background.js"]
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"js": ["content.js"],
|
||||||
|
"matches": ["<all_urls>", "file://*/*"],
|
||||||
|
"all_frames": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"injector-subtitle.js",
|
||||||
|
"injector-shadowroot.js"
|
||||||
|
],
|
||||||
|
"commands": {
|
||||||
|
"_execute_browser_action": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+K"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toggleTranslate": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+Q"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_translate__"
|
||||||
|
},
|
||||||
|
"openTranbox": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+S"
|
||||||
|
},
|
||||||
|
"description": "__MSG_open_tranbox__"
|
||||||
|
},
|
||||||
|
"toggleStyle": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+C"
|
||||||
|
},
|
||||||
|
"description": "__MSG_toggle_style__"
|
||||||
|
},
|
||||||
|
"openOptions": {
|
||||||
|
"description": "__MSG_open_options__"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"<all_urls>",
|
||||||
|
"storage",
|
||||||
|
"menus",
|
||||||
|
"messagesModify",
|
||||||
|
"scripting",
|
||||||
|
"declarativeNetRequest"
|
||||||
|
],
|
||||||
|
"icons": {
|
||||||
|
"16": "images/logo16.png",
|
||||||
|
"32": "images/logo32.png",
|
||||||
|
"48": "images/logo48.png",
|
||||||
|
"128": "images/logo128.png"
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_icon": {
|
||||||
|
"128": "images/logo128.png"
|
||||||
|
},
|
||||||
|
"default_title": "__MSG_app_name__",
|
||||||
|
"default_popup": "popup.html"
|
||||||
|
},
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,230 +1,19 @@
|
|||||||
import queryString from "query-string";
|
import { DEFAULT_USER_AGENT } from "../config";
|
||||||
import { getBdauth, setBdauth } from "../libs/storage";
|
|
||||||
import { URL_BAIDU_WEB, URL_BAIDU_TRAN } from "../config";
|
|
||||||
import { fetchApi } from "../libs/fetch";
|
|
||||||
|
|
||||||
/* eslint-disable */
|
export const genBaidu = ({ texts, from, to }) => {
|
||||||
function n(t, e) {
|
const body = {
|
||||||
for (var n = 0; n < e.length - 2; n += 3) {
|
|
||||||
var r = e.charAt(n + 2);
|
|
||||||
(r = "a" <= r ? r.charCodeAt(0) - 87 : Number(r)),
|
|
||||||
(r = "+" === e.charAt(n + 1) ? t >>> r : t << r),
|
|
||||||
(t = "+" === e.charAt(n) ? (t + r) & 4294967295 : t ^ r);
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
function e(t, e) {
|
|
||||||
(null == e || e > t.length) && (e = t.length);
|
|
||||||
for (var n = 0, r = new Array(e); n < e; n++) r[n] = t[n];
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
function getSign(t, gtk, r = null) {
|
|
||||||
var o,
|
|
||||||
i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
|
|
||||||
if (null === i) {
|
|
||||||
var a = t.length;
|
|
||||||
a > 30 &&
|
|
||||||
(t = ""
|
|
||||||
.concat(t.substr(0, 10))
|
|
||||||
.concat(t.substr(Math.floor(a / 2) - 5, 10))
|
|
||||||
.concat(t.substr(-10, 10)));
|
|
||||||
} else {
|
|
||||||
for (
|
|
||||||
var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/),
|
|
||||||
c = 0,
|
|
||||||
u = s.length,
|
|
||||||
l = [];
|
|
||||||
c < u;
|
|
||||||
c++
|
|
||||||
)
|
|
||||||
"" !== s[c] &&
|
|
||||||
l.push.apply(
|
|
||||||
l,
|
|
||||||
(function (t) {
|
|
||||||
if (Array.isArray(t)) return e(t);
|
|
||||||
})((o = s[c].split(""))) ||
|
|
||||||
(function (t) {
|
|
||||||
if (
|
|
||||||
("undefined" != typeof Symbol && null != t[Symbol.iterator]) ||
|
|
||||||
null != t["@@iterator"]
|
|
||||||
)
|
|
||||||
return Array.from(t);
|
|
||||||
})(o) ||
|
|
||||||
(function (t, n) {
|
|
||||||
if (t) {
|
|
||||||
if ("string" == typeof t) return e(t, n);
|
|
||||||
var r = Object.prototype.toString.call(t).slice(8, -1);
|
|
||||||
return (
|
|
||||||
"Object" === r && t.constructor && (r = t.constructor.name),
|
|
||||||
"Map" === r || "Set" === r
|
|
||||||
? Array.from(t)
|
|
||||||
: "Arguments" === r ||
|
|
||||||
/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)
|
|
||||||
? e(t, n)
|
|
||||||
: void 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})(o) ||
|
|
||||||
(function () {
|
|
||||||
throw new TypeError(
|
|
||||||
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
),
|
|
||||||
c !== u - 1 && l.push(i[c]);
|
|
||||||
var p = l.length;
|
|
||||||
p > 30 &&
|
|
||||||
(t =
|
|
||||||
l.slice(0, 10).join("") +
|
|
||||||
l.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") +
|
|
||||||
l.slice(-10).join(""));
|
|
||||||
}
|
|
||||||
for (
|
|
||||||
var d = ""
|
|
||||||
.concat(String.fromCharCode(103))
|
|
||||||
.concat(String.fromCharCode(116))
|
|
||||||
.concat(String.fromCharCode(107)),
|
|
||||||
h = (null !== r ? r : (r = gtk || "") || "").split("."),
|
|
||||||
f = Number(h[0]) || 0,
|
|
||||||
m = Number(h[1]) || 0,
|
|
||||||
g = [],
|
|
||||||
y = 0,
|
|
||||||
v = 0;
|
|
||||||
v < t.length;
|
|
||||||
v++
|
|
||||||
) {
|
|
||||||
var _ = t.charCodeAt(v);
|
|
||||||
_ < 128
|
|
||||||
? (g[y++] = _)
|
|
||||||
: (_ < 2048
|
|
||||||
? (g[y++] = (_ >> 6) | 192)
|
|
||||||
: (55296 == (64512 & _) &&
|
|
||||||
v + 1 < t.length &&
|
|
||||||
56320 == (64512 & t.charCodeAt(v + 1))
|
|
||||||
? ((_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v))),
|
|
||||||
(g[y++] = (_ >> 18) | 240),
|
|
||||||
(g[y++] = ((_ >> 12) & 63) | 128))
|
|
||||||
: (g[y++] = (_ >> 12) | 224),
|
|
||||||
(g[y++] = ((_ >> 6) & 63) | 128)),
|
|
||||||
(g[y++] = (63 & _) | 128));
|
|
||||||
}
|
|
||||||
for (
|
|
||||||
var b = f,
|
|
||||||
w =
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(45))
|
|
||||||
.concat(String.fromCharCode(97)) +
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(94))
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(54)),
|
|
||||||
k =
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(45))
|
|
||||||
.concat(String.fromCharCode(51)) +
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(94))
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(98)) +
|
|
||||||
""
|
|
||||||
.concat(String.fromCharCode(43))
|
|
||||||
.concat(String.fromCharCode(45))
|
|
||||||
.concat(String.fromCharCode(102)),
|
|
||||||
x = 0;
|
|
||||||
x < g.length;
|
|
||||||
x++
|
|
||||||
)
|
|
||||||
b = n((b += g[x]), w);
|
|
||||||
return (
|
|
||||||
(b = n(b, k)),
|
|
||||||
(b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
|
|
||||||
"".concat((b %= 1e6).toString(), ".").concat(b ^ f)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getToken = async () => {
|
|
||||||
const res = await fetchApi({
|
|
||||||
input: URL_BAIDU_WEB,
|
|
||||||
init: {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "text/html; charset=utf-8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(res.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = await res.text();
|
|
||||||
const token = text.match(/token: '(.*)',/)[1];
|
|
||||||
const gtk = text.match(/gtk = "(.*)";/)[1];
|
|
||||||
const exp = Date.now() + 8 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
if (!token || !gtk) {
|
|
||||||
throw new Error("[baidu] get token error");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { token, gtk, exp };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 闭包缓存token,减少对storage查询
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const _bdAuth = () => {
|
|
||||||
let store;
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
// 查询内存缓存
|
|
||||||
if (store && store.exp > now) {
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询storage缓存
|
|
||||||
store = await getBdauth();
|
|
||||||
if (store && store.exp > now) {
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存没有或失效,查询接口
|
|
||||||
store = await getToken();
|
|
||||||
await setBdauth(store);
|
|
||||||
return store;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const bdAuth = _bdAuth();
|
|
||||||
|
|
||||||
export const genBaidu = async ({ text, from, to }) => {
|
|
||||||
const { token, gtk } = await bdAuth();
|
|
||||||
const sign = getSign(text, gtk);
|
|
||||||
const data = {
|
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
query: text,
|
query: texts.join(" "),
|
||||||
simple_means_flag: 3,
|
source: "txt",
|
||||||
sign,
|
|
||||||
token,
|
|
||||||
domain: "common",
|
|
||||||
ts: Date.now(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const input = `${URL_BAIDU_TRAN}?from=${from}&to=${to}`;
|
const url = "https://fanyi.baidu.com/transapi";
|
||||||
const init = {
|
const headers = {
|
||||||
headers: {
|
// Origin: "https://fanyi.baidu.com",
|
||||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
},
|
"User-Agent": DEFAULT_USER_AGENT,
|
||||||
method: "POST",
|
|
||||||
body: queryString.stringify(data),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return [input, init];
|
return { url, body, headers };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { URL_DEEPLFREE_TRAN } from "../config";
|
|
||||||
|
|
||||||
let id = 1e4 * Math.round(1e4 * Math.random());
|
let id = 1e4 * Math.round(1e4 * Math.random());
|
||||||
|
|
||||||
export const genDeeplFree = ({ text, from, to }) => {
|
export const genDeeplFree = ({ texts, from, to }) => {
|
||||||
|
const text = texts.join(" ");
|
||||||
const iCount = (text.match(/[i]/g) || []).length + 1;
|
const iCount = (text.match(/[i]/g) || []).length + 1;
|
||||||
let timestamp = Date.now();
|
let timestamp = Date.now();
|
||||||
timestamp = timestamp + (iCount - (timestamp % iCount));
|
timestamp = timestamp + (iCount - (timestamp % iCount));
|
||||||
id++;
|
id++;
|
||||||
|
|
||||||
let body = JSON.stringify({
|
const url = "https://www2.deepl.com/jsonrpc";
|
||||||
|
|
||||||
|
const body = {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
method: "LMT_handle_texts",
|
method: "LMT_handle_texts",
|
||||||
params: {
|
params: {
|
||||||
@@ -30,29 +31,20 @@ export const genDeeplFree = ({ text, from, to }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
body = body.replace(
|
|
||||||
'method":"',
|
|
||||||
(id + 3) % 13 === 0 || (id + 5) % 29 === 0 ? 'method" : "' : 'method": "'
|
|
||||||
);
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "*/*",
|
|
||||||
"x-app-os-name": "iOS",
|
|
||||||
"x-app-os-version": "16.3.0",
|
|
||||||
"Accept-Language": "en-US,en;q=0.9",
|
|
||||||
"Accept-Encoding": "gzip, deflate, br",
|
|
||||||
"x-app-device": "iPhone13,2",
|
|
||||||
"User-Agent": "DeepL-iOS/2.9.1 iOS 16.3.0 (iPhone13,2)",
|
|
||||||
"x-app-build": "510265",
|
|
||||||
"x-app-version": "2.9.1",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return [URL_DEEPLFREE_TRAN, init];
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "*/*",
|
||||||
|
"x-app-os-name": "iOS",
|
||||||
|
"x-app-os-version": "16.3.0",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"x-app-device": "iPhone13,2",
|
||||||
|
"User-Agent": "DeepL-iOS/2.9.1 iOS 16.3.0 (iPhone13,2)",
|
||||||
|
"x-app-build": "510265",
|
||||||
|
"x-app-version": "2.9.1",
|
||||||
|
};
|
||||||
|
|
||||||
|
return { url, body, headers };
|
||||||
};
|
};
|
||||||
|
|||||||
39
src/apis/history.js
Normal file
39
src/apis/history.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { DEFAULT_CONTEXT_SIZE } from "../config";
|
||||||
|
|
||||||
|
const historyMap = new Map();
|
||||||
|
|
||||||
|
const MsgHistory = (maxSize = DEFAULT_CONTEXT_SIZE) => {
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
const add = (...msgs) => {
|
||||||
|
messages.push(...msgs.filter(Boolean));
|
||||||
|
const extra = messages.length - maxSize;
|
||||||
|
if (extra > 0) {
|
||||||
|
messages.splice(0, extra);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAll = () => {
|
||||||
|
return [...messages];
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
messages.length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
add,
|
||||||
|
getAll,
|
||||||
|
clear,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMsgHistory = (apiSlug, maxSize) => {
|
||||||
|
if (historyMap.has(apiSlug)) {
|
||||||
|
return historyMap.get(apiSlug);
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgHistory = MsgHistory(maxSize);
|
||||||
|
historyMap.set(apiSlug, msgHistory);
|
||||||
|
return msgHistory;
|
||||||
|
};
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { fetchPolyfill } from "../libs/fetch";
|
import { fetchData } from "../libs/fetch";
|
||||||
import {
|
import {
|
||||||
OPT_TRANS_GOOGLE,
|
|
||||||
OPT_TRANS_MICROSOFT,
|
|
||||||
OPT_TRANS_DEEPL,
|
|
||||||
OPT_TRANS_DEEPLFREE,
|
|
||||||
OPT_TRANS_DEEPLX,
|
|
||||||
OPT_TRANS_BAIDU,
|
|
||||||
OPT_TRANS_TENCENT,
|
|
||||||
OPT_TRANS_OPENAI,
|
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
|
||||||
OPT_TRANS_CUSTOMIZE,
|
|
||||||
URL_CACHE_TRAN,
|
URL_CACHE_TRAN,
|
||||||
|
URL_CACHE_DELANG,
|
||||||
|
URL_CACHE_BINGDICT,
|
||||||
KV_SALT_SYNC,
|
KV_SALT_SYNC,
|
||||||
URL_BAIDU_LANGDETECT,
|
OPT_LANGS_TO_SPEC,
|
||||||
OPT_LANGS_BAIDU,
|
OPT_LANGS_SPEC_DEFAULT,
|
||||||
URL_TENCENT_TRANSMART,
|
API_SPE_TYPES,
|
||||||
OPT_LANGS_TENCENT,
|
DEFAULT_API_SETTING,
|
||||||
OPT_LANGS_SPECIAL,
|
OPT_TRANS_MICROSOFT,
|
||||||
|
MSG_BUILTINAI_DETECT,
|
||||||
|
MSG_BUILTINAI_TRANSLATE,
|
||||||
|
OPT_TRANS_BUILTINAI,
|
||||||
|
URL_CACHE_SUBTITLE,
|
||||||
|
OPT_LANGS_TO_CODE,
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import { sha256 } from "../libs/utils";
|
import { sha256, withTimeout } from "../libs/utils";
|
||||||
|
import {
|
||||||
|
handleTranslate,
|
||||||
|
handleSubtitle,
|
||||||
|
handleMicrosoftLangdetect,
|
||||||
|
} from "./trans";
|
||||||
|
import { getHttpCachePolyfill, putHttpCachePolyfill } from "../libs/cache";
|
||||||
|
import { getBatchQueue } from "../libs/batchQueue";
|
||||||
|
import { isBuiltinAIAvailable } from "../libs/browser";
|
||||||
|
import { chromeDetect, chromeTranslate } from "../libs/builtinAI";
|
||||||
|
import { fnPolyfill } from "../libs/fetch";
|
||||||
|
import { getFetchPool } from "../libs/pool";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同步数据
|
* 同步数据
|
||||||
@@ -29,7 +37,7 @@ import { sha256 } from "../libs/utils";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiSyncData = async (url, key, data) =>
|
export const apiSyncData = async (url, key, data) =>
|
||||||
fetchPolyfill(url, {
|
fetchData(url, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
Authorization: `Bearer ${await sha256(key, KV_SALT_SYNC)}`,
|
||||||
@@ -43,7 +51,135 @@ export const apiSyncData = async (url, key, data) =>
|
|||||||
* @param {*} url
|
* @param {*} url
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiFetch = (url) => fetchPolyfill(url);
|
export const apiFetch = (url) => fetchData(url);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft token
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiMsAuth = async () =>
|
||||||
|
fetchData("https://edge.microsoft.com/translate/auth");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google语言识别
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiGoogleLangdetect = async (text) => {
|
||||||
|
const params = {
|
||||||
|
client: "gtx",
|
||||||
|
dt: "t",
|
||||||
|
dj: 1,
|
||||||
|
ie: "UTF-8",
|
||||||
|
sl: "auto",
|
||||||
|
tl: "zh-CN",
|
||||||
|
q: text,
|
||||||
|
};
|
||||||
|
const input = `https://translate.googleapis.com/translate_a/single?${queryString.stringify(params)}`;
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = await fetchData(input, init, { useCache: true });
|
||||||
|
|
||||||
|
if (res?.src) {
|
||||||
|
await putHttpCachePolyfill(input, init, res);
|
||||||
|
return res.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft语言识别
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiMicrosoftLangdetect = async (text) => {
|
||||||
|
const cacheOpts = { text, detector: OPT_TRANS_MICROSOFT };
|
||||||
|
const cacheInput = `${URL_CACHE_DELANG}?${queryString.stringify(cacheOpts)}`;
|
||||||
|
const cache = await getHttpCachePolyfill(cacheInput);
|
||||||
|
if (cache) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${URL_CACHE_DELANG}_${OPT_TRANS_MICROSOFT}`;
|
||||||
|
const queue = getBatchQueue(key, handleMicrosoftLangdetect, {
|
||||||
|
batchInterval: 200,
|
||||||
|
batchSize: 20,
|
||||||
|
batchLength: 100000,
|
||||||
|
});
|
||||||
|
const lang = await queue.addTask(text);
|
||||||
|
|
||||||
|
if (lang) {
|
||||||
|
putHttpCachePolyfill(cacheInput, null, lang);
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Microsoft词典
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiMicrosoftDict = async (text) => {
|
||||||
|
const cacheOpts = { text };
|
||||||
|
const cacheInput = `${URL_CACHE_BINGDICT}?${queryString.stringify(cacheOpts)}`;
|
||||||
|
const cache = await getHttpCachePolyfill(cacheInput);
|
||||||
|
if (cache) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = "https://www.bing.com";
|
||||||
|
const url = `${host}/dict/search?q=${text}&FORM=BDVSP6&cc=cn`;
|
||||||
|
const str = await fetchData(
|
||||||
|
url,
|
||||||
|
{ credentials: "include" },
|
||||||
|
{ useCache: false }
|
||||||
|
);
|
||||||
|
if (!str) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(str, "text/html");
|
||||||
|
|
||||||
|
const word = doc.querySelector("#headword > h1")?.textContent.trim();
|
||||||
|
if (!word) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trs = [];
|
||||||
|
doc.querySelectorAll("div.qdef > ul > li").forEach(($li) => {
|
||||||
|
const pos = $li.querySelector(".pos")?.textContent?.trim();
|
||||||
|
const def = $li.querySelector(".def")?.textContent?.trim();
|
||||||
|
trs.push({ pos, def });
|
||||||
|
});
|
||||||
|
|
||||||
|
const aus = [];
|
||||||
|
const $audioUK = doc.querySelector("#bigaud_uk");
|
||||||
|
const $audioUS = doc.querySelector("#bigaud_us");
|
||||||
|
if ($audioUK) {
|
||||||
|
const audioUK = host + $audioUK?.dataset?.mp3link;
|
||||||
|
const $phoneticUK = $audioUK.parentElement?.previousElementSibling;
|
||||||
|
const phoneticUK = $phoneticUK?.textContent?.trim();
|
||||||
|
aus.push({ key: "UK", audio: audioUK, phonetic: phoneticUK });
|
||||||
|
}
|
||||||
|
if ($audioUS) {
|
||||||
|
const audioUS = host + $audioUS?.dataset?.mp3link;
|
||||||
|
const $phoneticUS = $audioUS.parentElement?.previousElementSibling;
|
||||||
|
const phoneticUS = $phoneticUS?.textContent?.trim();
|
||||||
|
aus.push({ key: "US", audio: audioUS, phonetic: phoneticUS });
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = { word, trs, aus };
|
||||||
|
putHttpCachePolyfill(cacheInput, null, res);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 百度语言识别
|
* 百度语言识别
|
||||||
@@ -51,7 +187,8 @@ export const apiFetch = (url) => fetchPolyfill(url);
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiBaiduLangdetect = async (text) => {
|
export const apiBaiduLangdetect = async (text) => {
|
||||||
const res = await fetchPolyfill(URL_BAIDU_LANGDETECT, {
|
const input = "https://fanyi.baidu.com/langdetect";
|
||||||
|
const init = {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
},
|
},
|
||||||
@@ -59,39 +196,216 @@ export const apiBaiduLangdetect = async (text) => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: text,
|
query: text,
|
||||||
}),
|
}),
|
||||||
useCache: true,
|
};
|
||||||
});
|
const res = await fetchData(input, init, { useCache: true });
|
||||||
|
|
||||||
if (res.error === 0) {
|
if (res?.error === 0) {
|
||||||
return OPT_LANGS_BAIDU.get(res.lan) ?? res.lan;
|
await putHttpCachePolyfill(input, init, res);
|
||||||
|
return res.lan;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 百度翻译建议
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiBaiduSuggest = async (text) => {
|
||||||
|
const input = "https://fanyi.baidu.com/sug";
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
kw: text,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const res = await fetchData(input, init, { useCache: true });
|
||||||
|
|
||||||
|
if (res?.errno === 0) {
|
||||||
|
await putHttpCachePolyfill(input, init, res);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有道翻译建议
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiYoudaoSuggest = async (text) => {
|
||||||
|
const params = {
|
||||||
|
num: 5,
|
||||||
|
ver: 3.0,
|
||||||
|
doctype: "json",
|
||||||
|
cache: false,
|
||||||
|
le: "en",
|
||||||
|
q: text,
|
||||||
|
};
|
||||||
|
const input = `https://dict.youdao.com/suggest?${queryString.stringify(params)}`;
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
accept: "application/json, text/plain, */*",
|
||||||
|
"accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,ja;q=0.6",
|
||||||
|
"content-type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
};
|
||||||
|
const res = await fetchData(input, init, { useCache: true });
|
||||||
|
|
||||||
|
if (res?.result?.code === 200) {
|
||||||
|
await putHttpCachePolyfill(input, init, res);
|
||||||
|
return res.data.entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有道词典
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiYoudaoDict = async (text) => {
|
||||||
|
const params = {
|
||||||
|
doctype: "json",
|
||||||
|
jsonversion: 4,
|
||||||
|
};
|
||||||
|
const input = `https://dict.youdao.com/jsonapi_s?${queryString.stringify(params)}`;
|
||||||
|
const body = queryString.stringify({
|
||||||
|
q: text,
|
||||||
|
le: "en",
|
||||||
|
t: 3,
|
||||||
|
client: "web",
|
||||||
|
// sign: "",
|
||||||
|
keyfrom: "webdict",
|
||||||
|
});
|
||||||
|
const init = {
|
||||||
|
headers: {
|
||||||
|
accept: "application/json, text/plain, */*",
|
||||||
|
"accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,ja;q=0.6",
|
||||||
|
"content-type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
const res = await fetchData(input, init, { useCache: true });
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
await putHttpCachePolyfill(input, init, res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 百度语音
|
||||||
|
* @param {*} text
|
||||||
|
* @param {*} lan
|
||||||
|
* @param {*} spd
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiBaiduTTS = (text, lan = "uk", spd = 3) => {
|
||||||
|
const input = `https://fanyi.baidu.com/gettts?${queryString.stringify({ lan, text, spd })}`;
|
||||||
|
return fetchData(input);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 腾讯语言识别
|
* 腾讯语言识别
|
||||||
* @param {*} text
|
* @param {*} text
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiTencentLangdetect = async (text) => {
|
export const apiTencentLangdetect = async (text) => {
|
||||||
|
const input = "https://transmart.qq.com/api/imt";
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
header: {
|
header: {
|
||||||
fn: "text_analysis",
|
fn: "text_analysis",
|
||||||
|
client_key:
|
||||||
|
"browser-chrome-110.0.0-Mac OS-df4bd4c5-a65d-44b2-a40f-42f34f3535f2-1677486696487",
|
||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
|
const init = {
|
||||||
const res = await fetchPolyfill(URL_TENCENT_TRANSMART, {
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
|
"user-agent":
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
|
||||||
|
referer: "https://transmart.qq.com/zh-CN/index",
|
||||||
},
|
},
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body,
|
body,
|
||||||
useCache: true,
|
};
|
||||||
});
|
const res = await fetchData(input, init, { useCache: true });
|
||||||
|
|
||||||
return OPT_LANGS_TENCENT.get(res.language) ?? res.language;
|
if (res?.language) {
|
||||||
|
await putHttpCachePolyfill(input, init, res);
|
||||||
|
return res.language;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 浏览器内置AI语言识别
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const apiBuiltinAIDetect = async (text) => {
|
||||||
|
if (!isBuiltinAIAvailable) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const [lang, error] = await fnPolyfill({
|
||||||
|
fn: chromeDetect,
|
||||||
|
msg: MSG_BUILTINAI_DETECT,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
if (!error) {
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 浏览器内置AI翻译
|
||||||
|
* @param {*} param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const apiBuiltinAITranslate = async ({ text, from, to, apiSetting }) => {
|
||||||
|
if (!isBuiltinAIAvailable) {
|
||||||
|
return ["", true];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fetchInterval, fetchLimit, httpTimeout } = apiSetting;
|
||||||
|
const fetchPool = getFetchPool(fetchInterval, fetchLimit);
|
||||||
|
const result = await withTimeout(
|
||||||
|
fetchPool.push(fnPolyfill, {
|
||||||
|
fn: chromeTranslate,
|
||||||
|
msg: MSG_BUILTINAI_TRANSLATE,
|
||||||
|
text,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}),
|
||||||
|
httpTimeout
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("apiBuiltinAITranslate got null reault");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [trText, srLang, error] = result;
|
||||||
|
if (error) {
|
||||||
|
throw new Error("apiBuiltinAITranslate got error", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [trText, srLang];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,97 +414,146 @@ export const apiTencentLangdetect = async (text) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const apiTranslate = async ({
|
export const apiTranslate = async ({
|
||||||
translator,
|
|
||||||
text,
|
text,
|
||||||
fromLang,
|
fromLang = "auto",
|
||||||
toLang,
|
toLang,
|
||||||
apiSetting = {},
|
apiSetting = DEFAULT_API_SETTING,
|
||||||
|
docInfo = {},
|
||||||
|
glossary,
|
||||||
useCache = true,
|
useCache = true,
|
||||||
usePool = true,
|
usePool = true,
|
||||||
}) => {
|
}) => {
|
||||||
let trText = "";
|
|
||||||
let isSame = false;
|
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return [trText, true];
|
throw new Error("The text cannot be empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const from =
|
const { apiType, apiSlug, useBatchFetch } = apiSetting;
|
||||||
OPT_LANGS_SPECIAL[translator].get(fromLang) ??
|
const langMap = OPT_LANGS_TO_SPEC[apiType] || OPT_LANGS_SPEC_DEFAULT;
|
||||||
OPT_LANGS_SPECIAL[translator].get("auto");
|
const from = langMap.get(fromLang);
|
||||||
const to = OPT_LANGS_SPECIAL[translator].get(toLang);
|
const to = langMap.get(toLang);
|
||||||
if (!to) {
|
if (!to) {
|
||||||
console.log(`[trans] target lang: ${toLang} not support`);
|
throw new Error(`The target lang: ${toLang} not support`);
|
||||||
return [trText, isSame];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: 优化缓存失效因素
|
||||||
|
const [v1, v2] = process.env.REACT_APP_VERSION.split(".");
|
||||||
const cacheOpts = {
|
const cacheOpts = {
|
||||||
translator,
|
apiSlug,
|
||||||
text,
|
text,
|
||||||
fromLang,
|
fromLang,
|
||||||
toLang,
|
toLang,
|
||||||
|
version: [v1, v2].join("."),
|
||||||
};
|
};
|
||||||
|
const cacheInput = `${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`;
|
||||||
|
|
||||||
const transOpts = {
|
// 查询缓存数据
|
||||||
translator,
|
if (useCache) {
|
||||||
text,
|
const cache = await getHttpCachePolyfill(cacheInput);
|
||||||
from,
|
if (cache?.trText) {
|
||||||
to,
|
return cache;
|
||||||
};
|
|
||||||
|
|
||||||
const res = await fetchPolyfill(
|
|
||||||
`${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`,
|
|
||||||
{
|
|
||||||
useCache,
|
|
||||||
usePool,
|
|
||||||
transOpts,
|
|
||||||
apiSetting,
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
switch (translator) {
|
|
||||||
case OPT_TRANS_GOOGLE:
|
|
||||||
trText = res.sentences.map((item) => item.trans).join(" ");
|
|
||||||
isSame = to === res.src;
|
|
||||||
break;
|
|
||||||
case OPT_TRANS_MICROSOFT:
|
|
||||||
trText = res[0].translations.map((item) => item.text).join(" ");
|
|
||||||
isSame = text === trText;
|
|
||||||
break;
|
|
||||||
case OPT_TRANS_DEEPL:
|
|
||||||
trText = res.translations.map((item) => item.text).join(" ");
|
|
||||||
isSame = to === res.translations[0].detected_source_language;
|
|
||||||
break;
|
|
||||||
case OPT_TRANS_DEEPLFREE:
|
|
||||||
trText = res.result?.texts.map((item) => item.text).join(" ");
|
|
||||||
isSame = to === res.result?.lang;
|
|
||||||
break;
|
|
||||||
case OPT_TRANS_DEEPLX:
|
|
||||||
trText = res.data;
|
|
||||||
isSame = to === res.source_lang;
|
|
||||||
break;
|
|
||||||
case OPT_TRANS_BAIDU:
|
|
||||||
trText = res.trans_result?.data.map((item) => item.dst).join(" ");
|
|
||||||
isSame = res.trans_result?.to === res.trans_result?.from;
|
|
||||||
break;
|
|
||||||
case OPT_TRANS_TENCENT:
|
|
||||||
trText = res.auto_translation;
|
|
||||||
isSame = text === trText;
|
|
||||||
break;
|
|
||||||
case OPT_TRANS_OPENAI:
|
|
||||||
trText = res?.choices?.[0].message.content;
|
|
||||||
isSame = text === trText;
|
|
||||||
break;
|
|
||||||
case OPT_TRANS_CLOUDFLAREAI:
|
|
||||||
trText = res?.result?.translated_text;
|
|
||||||
isSame = text === trText;
|
|
||||||
break;
|
|
||||||
case OPT_TRANS_CUSTOMIZE:
|
|
||||||
trText = res.text;
|
|
||||||
isSame = to === res.from;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [trText, isSame, res];
|
// 请求接口数据
|
||||||
|
let tranlation = [];
|
||||||
|
if (apiType === OPT_TRANS_BUILTINAI) {
|
||||||
|
tranlation = await apiBuiltinAITranslate({
|
||||||
|
text,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
apiSetting,
|
||||||
|
});
|
||||||
|
} else if (useBatchFetch && API_SPE_TYPES.batch.has(apiType)) {
|
||||||
|
const { apiSlug, batchInterval, batchSize, batchLength } = apiSetting;
|
||||||
|
const key = `${apiSlug}_${fromLang}_${toLang}`;
|
||||||
|
const queue = getBatchQueue(key, handleTranslate, {
|
||||||
|
batchInterval,
|
||||||
|
batchSize,
|
||||||
|
batchLength,
|
||||||
|
});
|
||||||
|
tranlation = await queue.addTask(text, {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
langMap,
|
||||||
|
docInfo,
|
||||||
|
glossary,
|
||||||
|
apiSetting,
|
||||||
|
usePool,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
[tranlation] = await handleTranslate([text], {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
langMap,
|
||||||
|
docInfo,
|
||||||
|
glossary,
|
||||||
|
apiSetting,
|
||||||
|
usePool,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let trText = "";
|
||||||
|
let srLang = "";
|
||||||
|
let srCode = "";
|
||||||
|
if (Array.isArray(tranlation)) {
|
||||||
|
[trText, srLang = ""] = tranlation;
|
||||||
|
if (srLang) {
|
||||||
|
srCode = OPT_LANGS_TO_CODE[apiType].get(srLang) || "";
|
||||||
|
}
|
||||||
|
} else if (typeof tranlation === "string") {
|
||||||
|
trText = tranlation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trText) {
|
||||||
|
throw new Error("tanslate api got empty trtext");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSame = fromLang === "auto" && srLang === to;
|
||||||
|
|
||||||
|
// 插入缓存
|
||||||
|
if (useCache) {
|
||||||
|
putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang, srCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { trText, srLang, srCode, isSame };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 字幕处理/翻译
|
||||||
|
export const apiSubtitle = async ({
|
||||||
|
videoId,
|
||||||
|
chunkSign,
|
||||||
|
fromLang = "auto",
|
||||||
|
toLang,
|
||||||
|
events = [],
|
||||||
|
apiSetting,
|
||||||
|
}) => {
|
||||||
|
const cacheOpts = {
|
||||||
|
apiSlug: apiSetting.apiSlug,
|
||||||
|
videoId,
|
||||||
|
chunkSign,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
};
|
||||||
|
const cacheInput = `${URL_CACHE_SUBTITLE}?${queryString.stringify(cacheOpts)}`;
|
||||||
|
const cache = await getHttpCachePolyfill(cacheInput);
|
||||||
|
if (cache) {
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtitles = await handleSubtitle({
|
||||||
|
events,
|
||||||
|
from: fromLang,
|
||||||
|
to: toLang,
|
||||||
|
apiSetting,
|
||||||
|
});
|
||||||
|
if (subtitles?.length) {
|
||||||
|
putHttpCachePolyfill(cacheInput, null, subtitles);
|
||||||
|
return subtitles;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
|
|||||||
1092
src/apis/trans.js
Normal file
1092
src/apis/trans.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,89 +1,296 @@
|
|||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import {
|
import {
|
||||||
MSG_FETCH,
|
MSG_FETCH,
|
||||||
MSG_FETCH_LIMIT,
|
MSG_GET_HTTPCACHE,
|
||||||
MSG_FETCH_CLEAR,
|
MSG_PUT_HTTPCACHE,
|
||||||
MSG_TRANS_TOGGLE,
|
MSG_TRANS_TOGGLE,
|
||||||
MSG_OPEN_OPTIONS,
|
MSG_OPEN_OPTIONS,
|
||||||
MSG_SAVE_RULE,
|
MSG_SAVE_RULE,
|
||||||
MSG_TRANS_TOGGLE_STYLE,
|
MSG_TRANS_TOGGLE_STYLE,
|
||||||
|
MSG_OPEN_TRANBOX,
|
||||||
|
MSG_CONTEXT_MENUS,
|
||||||
|
MSG_COMMAND_SHORTCUTS,
|
||||||
|
MSG_INJECT_JS,
|
||||||
|
MSG_INJECT_CSS,
|
||||||
|
MSG_UPDATE_CSP,
|
||||||
|
MSG_BUILTINAI_DETECT,
|
||||||
|
MSG_BUILTINAI_TRANSLATE,
|
||||||
CMD_TOGGLE_TRANSLATE,
|
CMD_TOGGLE_TRANSLATE,
|
||||||
CMD_TOGGLE_STYLE,
|
CMD_TOGGLE_STYLE,
|
||||||
CMD_OPEN_OPTIONS,
|
CMD_OPEN_OPTIONS,
|
||||||
|
CMD_OPEN_TRANBOX,
|
||||||
|
CLIENT_THUNDERBIRD,
|
||||||
|
MSG_SET_LOGLEVEL,
|
||||||
|
MSG_CLEAR_CACHES,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
import { getSettingWithDefault, tryInitDefaultData } from "./libs/storage";
|
||||||
import { trySyncSettingAndRules } from "./libs/sync";
|
import { trySyncSettingAndRules } from "./libs/sync";
|
||||||
import { fetchData, fetchPool } from "./libs/fetch";
|
import { fetchHandle } from "./libs/fetch";
|
||||||
|
import { tryClearCaches, getHttpCache, putHttpCache } from "./libs/cache";
|
||||||
import { sendTabMsg } from "./libs/msg";
|
import { sendTabMsg } from "./libs/msg";
|
||||||
import { trySyncAllSubRules } from "./libs/subRules";
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
import { tryClearCaches } from "./libs";
|
|
||||||
import { saveRule } from "./libs/rules";
|
import { saveRule } from "./libs/rules";
|
||||||
|
import { getCurTabId } from "./libs/msg";
|
||||||
|
import { injectInlineJsBg, injectInternalCss } from "./libs/injector";
|
||||||
|
import { kissLog, logger } from "./libs/log";
|
||||||
|
import { chromeDetect, chromeTranslate } from "./libs/builtinAI";
|
||||||
|
|
||||||
globalThis.ContextType = "BACKGROUND";
|
globalThis.__KISS_CONTEXT__ = "background";
|
||||||
|
|
||||||
|
const CSP_RULE_START_ID = 1;
|
||||||
|
const ORI_RULE_START_ID = 10000;
|
||||||
|
const CSP_REMOVE_HEADERS = [
|
||||||
|
`content-security-policy`,
|
||||||
|
`content-security-policy-report-only`,
|
||||||
|
`x-webkit-csp`,
|
||||||
|
`x-content-security-policy`,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加右键菜单
|
||||||
|
*/
|
||||||
|
async function addContextMenus(contextMenuType = 1) {
|
||||||
|
// 添加前先删除,避免重复ID的错误
|
||||||
|
try {
|
||||||
|
await browser.contextMenus.removeAll();
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("remove contextMenus", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (contextMenuType) {
|
||||||
|
case 1:
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: CMD_TOGGLE_TRANSLATE,
|
||||||
|
title: browser.i18n.getMessage("app_name"),
|
||||||
|
contexts: ["page", "selection"],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: CMD_TOGGLE_TRANSLATE,
|
||||||
|
title: browser.i18n.getMessage("toggle_translate"),
|
||||||
|
contexts: ["page", "selection"],
|
||||||
|
});
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: CMD_TOGGLE_STYLE,
|
||||||
|
title: browser.i18n.getMessage("toggle_style"),
|
||||||
|
contexts: ["page", "selection"],
|
||||||
|
});
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: CMD_OPEN_TRANBOX,
|
||||||
|
title: browser.i18n.getMessage("open_tranbox"),
|
||||||
|
contexts: ["page", "selection"],
|
||||||
|
});
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: "options_separator",
|
||||||
|
type: "separator",
|
||||||
|
contexts: ["page", "selection"],
|
||||||
|
});
|
||||||
|
browser.contextMenus.create({
|
||||||
|
id: CMD_OPEN_OPTIONS,
|
||||||
|
title: browser.i18n.getMessage("open_options"),
|
||||||
|
contexts: ["page", "selection"],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新CSP策略
|
||||||
|
* @param {*} csplist
|
||||||
|
*/
|
||||||
|
async function updateCspRules({ csplist, orilist }) {
|
||||||
|
try {
|
||||||
|
const oldRules = await browser.declarativeNetRequest.getDynamicRules();
|
||||||
|
|
||||||
|
const rulesToAdd = [];
|
||||||
|
const idsToRemove = [];
|
||||||
|
|
||||||
|
if (csplist !== undefined) {
|
||||||
|
let processedCspList = csplist;
|
||||||
|
if (typeof processedCspList === "string") {
|
||||||
|
processedCspList = processedCspList
|
||||||
|
.split(/\n|,/)
|
||||||
|
.map((url) => url.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldCspRuleIds = oldRules
|
||||||
|
.filter(
|
||||||
|
(rule) => rule.id >= CSP_RULE_START_ID && rule.id < ORI_RULE_START_ID
|
||||||
|
)
|
||||||
|
.map((rule) => rule.id);
|
||||||
|
idsToRemove.push(...oldCspRuleIds);
|
||||||
|
|
||||||
|
const newCspRules = processedCspList.map((url, index) => ({
|
||||||
|
id: CSP_RULE_START_ID + index,
|
||||||
|
action: {
|
||||||
|
type: "modifyHeaders",
|
||||||
|
responseHeaders: CSP_REMOVE_HEADERS.map((header) => ({
|
||||||
|
operation: "remove",
|
||||||
|
header,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
urlFilter: url,
|
||||||
|
resourceTypes: ["main_frame", "sub_frame"],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
rulesToAdd.push(...newCspRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orilist !== undefined) {
|
||||||
|
let processedOriList = orilist;
|
||||||
|
if (typeof processedOriList === "string") {
|
||||||
|
processedOriList = processedOriList
|
||||||
|
.split(/\n|,/)
|
||||||
|
.map((url) => url.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldOriRuleIds = oldRules
|
||||||
|
.filter((rule) => rule.id >= ORI_RULE_START_ID)
|
||||||
|
.map((rule) => rule.id);
|
||||||
|
idsToRemove.push(...oldOriRuleIds);
|
||||||
|
|
||||||
|
const newOriRules = processedOriList.map((url, index) => ({
|
||||||
|
id: ORI_RULE_START_ID + index,
|
||||||
|
action: {
|
||||||
|
type: "modifyHeaders",
|
||||||
|
requestHeaders: [{ header: "Origin", operation: "set", value: url }],
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
urlFilter: url,
|
||||||
|
resourceTypes: ["xmlhttprequest"],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
rulesToAdd.push(...newOriRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idsToRemove.length > 0 || rulesToAdd.length > 0) {
|
||||||
|
await browser.declarativeNetRequest.updateDynamicRules({
|
||||||
|
removeRuleIds: idsToRemove,
|
||||||
|
addRules: rulesToAdd,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("update csp rules", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册邮件显示脚本
|
||||||
|
*/
|
||||||
|
async function registerMsgDisplayScript() {
|
||||||
|
await messenger.messageDisplayScripts.register({
|
||||||
|
js: [{ file: "/content.js" }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件安装
|
* 插件安装
|
||||||
*/
|
*/
|
||||||
browser.runtime.onInstalled.addListener(() => {
|
browser.runtime.onInstalled.addListener(async () => {
|
||||||
tryInitDefaultData();
|
await tryInitDefaultData();
|
||||||
|
|
||||||
|
//在thunderbird中注册脚本
|
||||||
|
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||||
|
registerMsgDisplayScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { contextMenuType, csplist, orilist } = await getSettingWithDefault();
|
||||||
|
|
||||||
|
// 右键菜单
|
||||||
|
addContextMenus(contextMenuType);
|
||||||
|
|
||||||
|
// 禁用CSP
|
||||||
|
updateCspRules({ csplist, orilist });
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 浏览器启动
|
* 浏览器启动
|
||||||
*/
|
*/
|
||||||
browser.runtime.onStartup.addListener(async () => {
|
browser.runtime.onStartup.addListener(async () => {
|
||||||
console.log("browser onStartup");
|
const {
|
||||||
|
clearCache,
|
||||||
|
contextMenuType,
|
||||||
|
subrulesList,
|
||||||
|
csplist,
|
||||||
|
orilist,
|
||||||
|
logLevel,
|
||||||
|
} = await getSettingWithDefault();
|
||||||
|
|
||||||
// 同步数据
|
// 设置日志
|
||||||
await trySyncSettingAndRules();
|
logger.setLevel(logLevel);
|
||||||
|
|
||||||
// 清除缓存
|
// 清除缓存
|
||||||
const setting = await getSettingWithDefault();
|
if (clearCache) {
|
||||||
if (setting.clearCache) {
|
|
||||||
tryClearCaches();
|
tryClearCaches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//在thunderbird中注册脚本
|
||||||
|
if (process.env.REACT_APP_CLIENT === CLIENT_THUNDERBIRD) {
|
||||||
|
registerMsgDisplayScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右键菜单
|
||||||
|
// firefox重启后菜单会消失,故重复添加
|
||||||
|
addContextMenus(contextMenuType);
|
||||||
|
|
||||||
|
// 禁用CSP
|
||||||
|
updateCspRules({ csplist, orilist });
|
||||||
|
|
||||||
|
// 同步数据
|
||||||
|
trySyncSettingAndRules();
|
||||||
|
|
||||||
// 同步订阅规则
|
// 同步订阅规则
|
||||||
trySyncAllSubRules(setting);
|
trySyncAllSubRules({ subrulesList });
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听消息
|
* 向当前活动标签页注入脚本或CSS
|
||||||
*/
|
*/
|
||||||
browser.runtime.onMessage.addListener(
|
const injectToCurrentTab = async (func, args) => {
|
||||||
({ action, args }, sender, sendResponse) => {
|
const tabId = await getCurTabId();
|
||||||
switch (action) {
|
return browser.scripting.executeScript({
|
||||||
case MSG_FETCH:
|
target: { tabId, allFrames: true },
|
||||||
const { input, opts } = args;
|
func: func,
|
||||||
fetchData(input, opts)
|
args: [args],
|
||||||
.then((data) => {
|
world: "MAIN",
|
||||||
sendResponse({ data });
|
});
|
||||||
})
|
};
|
||||||
.catch((error) => {
|
|
||||||
sendResponse({ error: error.message, cause: error.cause });
|
// 动作处理器映射表
|
||||||
});
|
const messageHandlers = {
|
||||||
break;
|
[MSG_FETCH]: (args) => fetchHandle(args),
|
||||||
case MSG_FETCH_LIMIT:
|
[MSG_GET_HTTPCACHE]: (args) => getHttpCache(args),
|
||||||
const { interval, limit } = args;
|
[MSG_PUT_HTTPCACHE]: (args) => putHttpCache(args),
|
||||||
fetchPool.update(interval, limit);
|
[MSG_OPEN_OPTIONS]: () => browser.runtime.openOptionsPage(),
|
||||||
sendResponse({ data: "ok" });
|
[MSG_SAVE_RULE]: (args) => saveRule(args),
|
||||||
break;
|
[MSG_INJECT_JS]: (args) => injectToCurrentTab(injectInlineJsBg, args),
|
||||||
case MSG_FETCH_CLEAR:
|
[MSG_INJECT_CSS]: (args) => injectToCurrentTab(injectInternalCss, args),
|
||||||
fetchPool.clear();
|
[MSG_UPDATE_CSP]: (args) => updateCspRules(args),
|
||||||
sendResponse({ data: "ok" });
|
[MSG_CONTEXT_MENUS]: (args) => addContextMenus(args),
|
||||||
break;
|
[MSG_COMMAND_SHORTCUTS]: () => browser.commands.getAll(),
|
||||||
case MSG_OPEN_OPTIONS:
|
[MSG_BUILTINAI_DETECT]: (args) => chromeDetect(args),
|
||||||
browser.runtime.openOptionsPage();
|
[MSG_BUILTINAI_TRANSLATE]: (args) => chromeTranslate(args),
|
||||||
break;
|
[MSG_SET_LOGLEVEL]: (args) => logger.setLevel(args),
|
||||||
case MSG_SAVE_RULE:
|
[MSG_CLEAR_CACHES]: () => tryClearCaches(),
|
||||||
saveRule(args);
|
};
|
||||||
break;
|
|
||||||
default:
|
/**
|
||||||
sendResponse({ error: `message action is unavailable: ${action}` });
|
* 监听消息
|
||||||
}
|
* todo: 返回含错误的结构化信息
|
||||||
return true;
|
*/
|
||||||
|
browser.runtime.onMessage.addListener(async ({ action, args }) => {
|
||||||
|
const handler = messageHandlers[action];
|
||||||
|
if (!handler) {
|
||||||
|
throw new Error(`Message action is unavailable: ${action}`);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
return handler(args);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听快捷键
|
* 监听快捷键
|
||||||
@@ -94,6 +301,9 @@ browser.commands.onCommand.addListener((command) => {
|
|||||||
case CMD_TOGGLE_TRANSLATE:
|
case CMD_TOGGLE_TRANSLATE:
|
||||||
sendTabMsg(MSG_TRANS_TOGGLE);
|
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
break;
|
break;
|
||||||
|
case CMD_OPEN_TRANBOX:
|
||||||
|
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||||
|
break;
|
||||||
case CMD_TOGGLE_STYLE:
|
case CMD_TOGGLE_STYLE:
|
||||||
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
break;
|
break;
|
||||||
@@ -103,3 +313,24 @@ browser.commands.onCommand.addListener((command) => {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听右键菜单
|
||||||
|
*/
|
||||||
|
browser.contextMenus.onClicked.addListener(({ menuItemId }) => {
|
||||||
|
switch (menuItemId) {
|
||||||
|
case CMD_TOGGLE_TRANSLATE:
|
||||||
|
sendTabMsg(MSG_TRANS_TOGGLE);
|
||||||
|
break;
|
||||||
|
case CMD_TOGGLE_STYLE:
|
||||||
|
sendTabMsg(MSG_TRANS_TOGGLE_STYLE);
|
||||||
|
break;
|
||||||
|
case CMD_OPEN_TRANBOX:
|
||||||
|
sendTabMsg(MSG_OPEN_TRANBOX);
|
||||||
|
break;
|
||||||
|
case CMD_OPEN_OPTIONS:
|
||||||
|
browser.runtime.openOptionsPage();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
290
src/common.js
290
src/common.js
@@ -1,143 +1,181 @@
|
|||||||
import React from "react";
|
import { OPT_HIGHLIGHT_WORDS_DISABLE } from "./config";
|
||||||
import ReactDOM from "react-dom/client";
|
|
||||||
import Action from "./views/Action";
|
|
||||||
import createCache from "@emotion/cache";
|
|
||||||
import { CacheProvider } from "@emotion/react";
|
|
||||||
import {
|
import {
|
||||||
MSG_TRANS_TOGGLE,
|
getFabWithDefault,
|
||||||
MSG_TRANS_TOGGLE_STYLE,
|
getSettingWithDefault,
|
||||||
MSG_TRANS_GETRULE,
|
getWordsWithDefault,
|
||||||
MSG_TRANS_PUTRULE,
|
} from "./libs/storage";
|
||||||
APP_LCNAME,
|
import { isIframe } from "./libs/iframe";
|
||||||
DEFAULT_TRANBOX_SETTING,
|
import { genEventName } from "./libs/utils";
|
||||||
} from "./config";
|
import { handlePing, injectScript } from "./libs/gm";
|
||||||
import { getRulesWithDefault, getFabWithDefault } from "./libs/storage";
|
|
||||||
import { Translator } from "./libs/translator";
|
|
||||||
import { sendIframeMsg, sendParentMsg } from "./libs/iframe";
|
|
||||||
import { matchRule } from "./libs/rules";
|
import { matchRule } from "./libs/rules";
|
||||||
import Slection from "./views/Selection";
|
import { trySyncAllSubRules } from "./libs/subRules";
|
||||||
import { touchTapListener } from "./libs/touch";
|
import { isInBlacklist } from "./libs/blacklist";
|
||||||
import { debounce } from "./libs/utils";
|
import { runSubtitle } from "./subtitle/subtitle";
|
||||||
|
import { logger } from "./libs/log";
|
||||||
|
import { injectInlineJs } from "./libs/injector";
|
||||||
|
import TranslatorManager from "./libs/translatorManager";
|
||||||
|
|
||||||
export async function runTranslator(setting) {
|
/**
|
||||||
const href = document.location.href;
|
* 油猴脚本设置页面
|
||||||
const rules = await getRulesWithDefault();
|
*/
|
||||||
const rule = await matchRule(rules, href, setting);
|
function runSettingPage() {
|
||||||
const translator = new Translator(rule, setting);
|
if (GM?.info?.script?.grant?.includes("unsafeWindow")) {
|
||||||
|
unsafeWindow.GM = GM;
|
||||||
return { translator, rule };
|
unsafeWindow.APP_INFO = {
|
||||||
|
name: process.env.REACT_APP_NAME,
|
||||||
|
version: process.env.REACT_APP_VERSION,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const ping = genEventName();
|
||||||
|
window.addEventListener(ping, handlePing);
|
||||||
|
// window.eval(`(${injectScript})("${ping}")`); // eslint-disable-line
|
||||||
|
injectInlineJs(
|
||||||
|
`(${injectScript})("${ping}")`,
|
||||||
|
"kiss-translator-options-injector"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runIframe(setting) {
|
/**
|
||||||
let translator;
|
* 显示错误信息到页面顶部
|
||||||
window.addEventListener("message", (e) => {
|
* @param {*} message
|
||||||
const { action, args } = e.data || {};
|
*/
|
||||||
switch (action) {
|
function showErr(message) {
|
||||||
case MSG_TRANS_TOGGLE:
|
const bannerId = "KISS-Translator-Message";
|
||||||
translator?.toggle();
|
const existingBanner = document.getElementById(bannerId);
|
||||||
break;
|
if (existingBanner) {
|
||||||
case MSG_TRANS_TOGGLE_STYLE:
|
existingBanner.remove();
|
||||||
translator?.toggleStyle();
|
}
|
||||||
break;
|
|
||||||
case MSG_TRANS_PUTRULE:
|
const banner = document.createElement("div");
|
||||||
if (!translator) {
|
banner.id = bannerId;
|
||||||
translator = new Translator(args, setting);
|
|
||||||
} else {
|
Object.assign(banner.style, {
|
||||||
translator.updateRule(args || {});
|
position: "fixed",
|
||||||
}
|
top: "0",
|
||||||
break;
|
left: "0",
|
||||||
default:
|
width: "100%",
|
||||||
|
backgroundColor: "#f44336",
|
||||||
|
color: "white",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "8px 16px",
|
||||||
|
zIndex: "1001",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
fontSize: "16px",
|
||||||
|
boxShadow: "0 2px 5px rgba(0,0,0,0.2)",
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeButton = document.createElement("span");
|
||||||
|
closeButton.textContent = "×";
|
||||||
|
|
||||||
|
Object.assign(closeButton.style, {
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
right: "20px",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "22px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageText = document.createTextNode(`KISS-Translator: ${message}`);
|
||||||
|
banner.appendChild(messageText);
|
||||||
|
banner.appendChild(closeButton);
|
||||||
|
|
||||||
|
document.body.appendChild(banner);
|
||||||
|
|
||||||
|
const removeBanner = () => {
|
||||||
|
banner.style.transition = "opacity 0.5s ease";
|
||||||
|
banner.style.opacity = "0";
|
||||||
|
setTimeout(() => {
|
||||||
|
if (banner && banner.parentNode) {
|
||||||
|
banner.parentNode.removeChild(banner);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
closeButton.onclick = removeBanner;
|
||||||
|
setTimeout(removeBanner, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFavWords(rule) {
|
||||||
|
if (
|
||||||
|
rule.highlightWords &&
|
||||||
|
rule.highlightWords !== OPT_HIGHLIGHT_WORDS_DISABLE
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return Object.keys(await getWordsWithDefault());
|
||||||
|
} catch (err) {
|
||||||
|
logger.info("get fav words", err);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
sendParentMsg(MSG_TRANS_GETRULE);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function showFab(translator) {
|
|
||||||
const fab = await getFabWithDefault();
|
|
||||||
if (fab.isHide) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const $action = document.createElement("div");
|
return [];
|
||||||
$action.setAttribute("id", APP_LCNAME);
|
|
||||||
document.body.parentElement.appendChild($action);
|
|
||||||
const shadowContainer = $action.attachShadow({ mode: "closed" });
|
|
||||||
const emotionRoot = document.createElement("style");
|
|
||||||
const shadowRootElement = document.createElement("div");
|
|
||||||
shadowContainer.appendChild(emotionRoot);
|
|
||||||
shadowContainer.appendChild(shadowRootElement);
|
|
||||||
const cache = createCache({
|
|
||||||
key: APP_LCNAME,
|
|
||||||
prepend: true,
|
|
||||||
container: emotionRoot,
|
|
||||||
});
|
|
||||||
ReactDOM.createRoot(shadowRootElement).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<CacheProvider value={cache}>
|
|
||||||
<Action translator={translator} fab={fab} />
|
|
||||||
</CacheProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showTransbox({
|
/**
|
||||||
tranboxSetting = DEFAULT_TRANBOX_SETTING,
|
* 入口函数
|
||||||
transApis,
|
*/
|
||||||
}) {
|
export async function run(isUserscript = false) {
|
||||||
if (!tranboxSetting?.transOpen) {
|
try {
|
||||||
return;
|
// 读取设置信息
|
||||||
}
|
const setting = await getSettingWithDefault();
|
||||||
|
|
||||||
const $tranbox = document.createElement("div");
|
// 日志
|
||||||
$tranbox.setAttribute("id", "kiss-transbox");
|
logger.setLevel(setting.logLevel);
|
||||||
document.body.parentElement.appendChild($tranbox);
|
|
||||||
const shadowContainer = $tranbox.attachShadow({ mode: "closed" });
|
|
||||||
const emotionRoot = document.createElement("style");
|
|
||||||
const shadowRootElement = document.createElement("div");
|
|
||||||
shadowContainer.appendChild(emotionRoot);
|
|
||||||
shadowContainer.appendChild(shadowRootElement);
|
|
||||||
const cache = createCache({
|
|
||||||
key: "kiss-transbox",
|
|
||||||
prepend: true,
|
|
||||||
container: emotionRoot,
|
|
||||||
});
|
|
||||||
ReactDOM.createRoot(shadowRootElement).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<CacheProvider value={cache}>
|
|
||||||
<Slection tranboxSetting={tranboxSetting} transApis={transApis} />
|
|
||||||
</CacheProvider>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function windowListener(rule) {
|
// if (document?.documentElement?.tagName?.toUpperCase() !== "HTML") {
|
||||||
window.addEventListener("message", (e) => {
|
// return;
|
||||||
const { action } = e.data || {};
|
// }
|
||||||
switch (action) {
|
const contentType = document?.contentType?.toLowerCase() || "";
|
||||||
case MSG_TRANS_GETRULE:
|
if (!contentType.includes("text") && !contentType.includes("html")) {
|
||||||
sendIframeMsg(MSG_TRANS_PUTRULE, rule);
|
logger.info("Skip running in document content type: ", contentType);
|
||||||
break;
|
return;
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showErr(message) {
|
const href = document?.location?.href || "";
|
||||||
const $err = document.createElement("div");
|
|
||||||
$err.innerText = `KISS-Translator: ${message}`;
|
|
||||||
$err.style.cssText = "background:red; color:#fff;";
|
|
||||||
document.body.prepend($err);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function touchOperation(translator) {
|
// 设置页面
|
||||||
const { touchTranslate = 2 } = translator.setting;
|
if (
|
||||||
if (touchTranslate === 0) {
|
isUserscript &&
|
||||||
return;
|
(href.includes(process.env.REACT_APP_OPTIONSPAGE_DEV) ||
|
||||||
|
href.includes(process.env.REACT_APP_OPTIONSPAGE))
|
||||||
|
) {
|
||||||
|
runSettingPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 黑名单
|
||||||
|
if (isInBlacklist(href, setting)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 翻译网页
|
||||||
|
const rule = await matchRule(href, setting);
|
||||||
|
const favWords = await getFavWords(rule);
|
||||||
|
const fabConfig = await getFabWithDefault();
|
||||||
|
const translatorManager = new TranslatorManager({
|
||||||
|
setting,
|
||||||
|
rule,
|
||||||
|
fabConfig,
|
||||||
|
favWords,
|
||||||
|
isIframe,
|
||||||
|
isUserscript,
|
||||||
|
});
|
||||||
|
translatorManager.start();
|
||||||
|
|
||||||
|
if (isIframe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字幕翻译
|
||||||
|
runSubtitle({ href, setting, rule, isUserscript });
|
||||||
|
|
||||||
|
if (isUserscript) {
|
||||||
|
trySyncAllSubRules(setting);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[KISS-Translator]", err);
|
||||||
|
showErr(err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTap = debounce(() => {
|
|
||||||
translator.toggle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
|
||||||
});
|
|
||||||
touchTapListener(handleTap, touchTranslate);
|
|
||||||
}
|
}
|
||||||
|
|||||||
578
src/config/api.js
Normal file
578
src/config/api.js
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
export const DEFAULT_HTTP_TIMEOUT = 10000; // 调用超时时间
|
||||||
|
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
|
||||||
|
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
|
||||||
|
export const DEFAULT_BATCH_INTERVAL = 400; // 批处理请求间隔时间
|
||||||
|
export const DEFAULT_BATCH_SIZE = 10; // 每次最多发送段落数量
|
||||||
|
export const DEFAULT_BATCH_LENGTH = 10000; // 每次发送最大文字数量
|
||||||
|
export const DEFAULT_CONTEXT_SIZE = 3; // 上下文会话数量
|
||||||
|
|
||||||
|
export const INPUT_PLACE_URL = "{{url}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_FROM = "{{from}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_TO = "{{to}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_FROM_LANG = "{{fromLang}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_TO_LANG = "{{toLang}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_TEXT = "{{text}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_TONE = "{{tone}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_TITLE = "{{title}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_DESCRIPTION = "{{description}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_KEY = "{{key}}"; // 占位符
|
||||||
|
export const INPUT_PLACE_MODEL = "{{model}}"; // 占位符
|
||||||
|
|
||||||
|
// export const OPT_DICT_BAIDU = "Baidu";
|
||||||
|
export const OPT_DICT_BING = "Bing";
|
||||||
|
export const OPT_DICT_YOUDAO = "Youdao";
|
||||||
|
export const OPT_DICT_ALL = [OPT_DICT_BING, OPT_DICT_YOUDAO];
|
||||||
|
export const OPT_DICT_MAP = new Set(OPT_DICT_ALL);
|
||||||
|
|
||||||
|
export const OPT_SUG_BAIDU = "Baidu";
|
||||||
|
export const OPT_SUG_YOUDAO = "Youdao";
|
||||||
|
export const OPT_SUG_ALL = [OPT_SUG_BAIDU, OPT_SUG_YOUDAO];
|
||||||
|
export const OPT_SUG_MAP = new Set(OPT_SUG_ALL);
|
||||||
|
|
||||||
|
export const OPT_TRANS_BUILTINAI = "BuiltinAI";
|
||||||
|
export const OPT_TRANS_GOOGLE = "Google";
|
||||||
|
export const OPT_TRANS_GOOGLE_2 = "Google2";
|
||||||
|
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
||||||
|
export const OPT_TRANS_AZUREAI = "AzureAI";
|
||||||
|
export const OPT_TRANS_DEEPL = "DeepL";
|
||||||
|
export const OPT_TRANS_DEEPLX = "DeepLX";
|
||||||
|
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
|
||||||
|
export const OPT_TRANS_NIUTRANS = "NiuTrans";
|
||||||
|
export const OPT_TRANS_BAIDU = "Baidu";
|
||||||
|
export const OPT_TRANS_TENCENT = "Tencent";
|
||||||
|
export const OPT_TRANS_VOLCENGINE = "Volcengine";
|
||||||
|
export const OPT_TRANS_OPENAI = "OpenAI";
|
||||||
|
export const OPT_TRANS_GEMINI = "Gemini";
|
||||||
|
export const OPT_TRANS_GEMINI_2 = "Gemini2";
|
||||||
|
export const OPT_TRANS_CLAUDE = "Claude";
|
||||||
|
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
||||||
|
export const OPT_TRANS_OLLAMA = "Ollama";
|
||||||
|
export const OPT_TRANS_OPENROUTER = "OpenRouter";
|
||||||
|
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
||||||
|
|
||||||
|
// 内置支持的翻译引擎
|
||||||
|
export const OPT_ALL_TRANS_TYPES = [
|
||||||
|
OPT_TRANS_BUILTINAI,
|
||||||
|
OPT_TRANS_GOOGLE,
|
||||||
|
OPT_TRANS_GOOGLE_2,
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_AZUREAI,
|
||||||
|
// OPT_TRANS_BAIDU,
|
||||||
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_TRANS_VOLCENGINE,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
|
OPT_TRANS_DEEPLFREE,
|
||||||
|
OPT_TRANS_DEEPLX,
|
||||||
|
OPT_TRANS_NIUTRANS,
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OPENROUTER,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OPT_LANGDETECTOR_ALL = [
|
||||||
|
OPT_TRANS_BUILTINAI,
|
||||||
|
OPT_TRANS_GOOGLE,
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_BAIDU,
|
||||||
|
OPT_TRANS_TENCENT,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OPT_LANGDETECTOR_MAP = new Set(OPT_LANGDETECTOR_ALL);
|
||||||
|
|
||||||
|
// 翻译引擎特殊集合
|
||||||
|
export const API_SPE_TYPES = {
|
||||||
|
// 内置翻译
|
||||||
|
builtin: new Set(OPT_ALL_TRANS_TYPES),
|
||||||
|
// 机器翻译
|
||||||
|
machine: new Set([
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_DEEPLFREE,
|
||||||
|
OPT_TRANS_BAIDU,
|
||||||
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_TRANS_VOLCENGINE,
|
||||||
|
]),
|
||||||
|
// AI翻译
|
||||||
|
ai: new Set([
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OPENROUTER,
|
||||||
|
]),
|
||||||
|
// 支持多key
|
||||||
|
mulkeys: new Set([
|
||||||
|
OPT_TRANS_AZUREAI,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_CLOUDFLAREAI,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OPENROUTER,
|
||||||
|
OPT_TRANS_NIUTRANS,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
]),
|
||||||
|
// 支持批处理
|
||||||
|
batch: new Set([
|
||||||
|
OPT_TRANS_AZUREAI,
|
||||||
|
OPT_TRANS_GOOGLE_2,
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_TRANS_DEEPL,
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OPENROUTER,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
]),
|
||||||
|
// 支持上下文
|
||||||
|
context: new Set([
|
||||||
|
OPT_TRANS_OPENAI,
|
||||||
|
OPT_TRANS_GEMINI,
|
||||||
|
OPT_TRANS_GEMINI_2,
|
||||||
|
OPT_TRANS_CLAUDE,
|
||||||
|
OPT_TRANS_OLLAMA,
|
||||||
|
OPT_TRANS_OPENROUTER,
|
||||||
|
OPT_TRANS_CUSTOMIZE,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BUILTIN_STONES = [
|
||||||
|
"formal", // 正式风格
|
||||||
|
"casual", // 口语风格
|
||||||
|
"neutral", // 中性风格
|
||||||
|
"technical", // 技术风格
|
||||||
|
"marketing", // 营销风格
|
||||||
|
"Literary", // 文学风格
|
||||||
|
"academic", // 学术风格
|
||||||
|
"legal", // 法律风格
|
||||||
|
"literal", // 直译风格
|
||||||
|
"ldiomatic", // 意译风格
|
||||||
|
"transcreation", // 创译风格
|
||||||
|
"machine-like", // 机器风格
|
||||||
|
"concise", // 简明风格
|
||||||
|
];
|
||||||
|
export const BUILTIN_PLACEHOLDERS = ["{ }", "{{ }}", "[ ]", "[[ ]]"];
|
||||||
|
export const BUILTIN_PLACETAGS = ["i", "a", "b", "x"];
|
||||||
|
|
||||||
|
export const OPT_LANGS_TO = [
|
||||||
|
["en", "English - English"],
|
||||||
|
["zh-CN", "Simplified Chinese - 简体中文"],
|
||||||
|
["zh-TW", "Traditional Chinese - 繁體中文"],
|
||||||
|
["ar", "Arabic - العربية"],
|
||||||
|
["bg", "Bulgarian - Български"],
|
||||||
|
["ca", "Catalan - Català"],
|
||||||
|
["hr", "Croatian - Hrvatski"],
|
||||||
|
["cs", "Czech - Čeština"],
|
||||||
|
["da", "Danish - Dansk"],
|
||||||
|
["nl", "Dutch - Nederlands"],
|
||||||
|
["fa", "Persian - فارسی"],
|
||||||
|
["fi", "Finnish - Suomi"],
|
||||||
|
["fr", "French - Français"],
|
||||||
|
["de", "German - Deutsch"],
|
||||||
|
["el", "Greek - Ελληνικά"],
|
||||||
|
["hi", "Hindi - हिन्दी"],
|
||||||
|
["hu", "Hungarian - Magyar"],
|
||||||
|
["id", "Indonesian - Indonesia"],
|
||||||
|
["it", "Italian - Italiano"],
|
||||||
|
["ja", "Japanese - 日本語"],
|
||||||
|
["ko", "Korean - 한국어"],
|
||||||
|
["ms", "Malay - Melayu"],
|
||||||
|
["mt", "Maltese - Malti"],
|
||||||
|
["nb", "Norwegian - Norsk Bokmål"],
|
||||||
|
["pl", "Polish - Polski"],
|
||||||
|
["pt", "Portuguese - Português"],
|
||||||
|
["ro", "Romanian - Română"],
|
||||||
|
["ru", "Russian - Русский"],
|
||||||
|
["sk", "Slovak - Slovenčina"],
|
||||||
|
["sl", "Slovenian - Slovenščina"],
|
||||||
|
["es", "Spanish - Español"],
|
||||||
|
["sv", "Swedish - Svenska"],
|
||||||
|
["ta", "Tamil - தமிழ்"],
|
||||||
|
["te", "Telugu - తెలుగు"],
|
||||||
|
["th", "Thai - ไทย"],
|
||||||
|
["tr", "Turkish - Türkçe"],
|
||||||
|
["uk", "Ukrainian - Українська"],
|
||||||
|
["vi", "Vietnamese - Tiếng Việt"],
|
||||||
|
];
|
||||||
|
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
||||||
|
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
|
||||||
|
export const OPT_LANGS_MAP = new Map(OPT_LANGS_TO);
|
||||||
|
|
||||||
|
// CODE->名称
|
||||||
|
export const OPT_LANGS_SPEC_NAME = new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
||||||
|
);
|
||||||
|
export const OPT_LANGS_SPEC_DEFAULT = new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key]) => [key, key])
|
||||||
|
);
|
||||||
|
export const OPT_LANGS_SPEC_DEFAULT_UC = new Map(
|
||||||
|
OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()])
|
||||||
|
);
|
||||||
|
export const OPT_LANGS_TO_SPEC = {
|
||||||
|
[OPT_TRANS_BUILTINAI]: new Map([
|
||||||
|
...OPT_LANGS_SPEC_DEFAULT,
|
||||||
|
["zh-CN", "zh"],
|
||||||
|
["zh-TW", "zh"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_GOOGLE]: OPT_LANGS_SPEC_DEFAULT,
|
||||||
|
[OPT_TRANS_GOOGLE_2]: OPT_LANGS_SPEC_DEFAULT,
|
||||||
|
[OPT_TRANS_MICROSOFT]: new Map([
|
||||||
|
...OPT_LANGS_SPEC_DEFAULT,
|
||||||
|
["auto", ""],
|
||||||
|
["zh-CN", "zh-Hans"],
|
||||||
|
["zh-TW", "zh-Hant"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_AZUREAI]: new Map([
|
||||||
|
...OPT_LANGS_SPEC_DEFAULT,
|
||||||
|
["auto", ""],
|
||||||
|
["zh-CN", "zh-Hans"],
|
||||||
|
["zh-TW", "zh-Hant"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_DEEPL]: new Map([
|
||||||
|
...OPT_LANGS_SPEC_DEFAULT_UC,
|
||||||
|
["auto", ""],
|
||||||
|
["zh-CN", "ZH"],
|
||||||
|
["zh-TW", "ZH"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_DEEPLFREE]: new Map([
|
||||||
|
...OPT_LANGS_SPEC_DEFAULT_UC,
|
||||||
|
["auto", "auto"],
|
||||||
|
["zh-CN", "ZH"],
|
||||||
|
["zh-TW", "ZH"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_DEEPLX]: new Map([
|
||||||
|
...OPT_LANGS_SPEC_DEFAULT_UC,
|
||||||
|
["auto", "auto"],
|
||||||
|
["zh-CN", "ZH"],
|
||||||
|
["zh-TW", "ZH"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_NIUTRANS]: new Map([
|
||||||
|
...OPT_LANGS_SPEC_DEFAULT,
|
||||||
|
["auto", "auto"],
|
||||||
|
["zh-CN", "zh"],
|
||||||
|
["zh-TW", "cht"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_VOLCENGINE]: new Map([
|
||||||
|
...OPT_LANGS_SPEC_DEFAULT,
|
||||||
|
["auto", "auto"],
|
||||||
|
["zh-CN", "zh"],
|
||||||
|
["zh-TW", "zh-Hant"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_BAIDU]: new Map([
|
||||||
|
...OPT_LANGS_SPEC_DEFAULT,
|
||||||
|
["zh-CN", "zh"],
|
||||||
|
["zh-TW", "cht"],
|
||||||
|
["ar", "ara"],
|
||||||
|
["bg", "bul"],
|
||||||
|
["ca", "cat"],
|
||||||
|
["hr", "hrv"],
|
||||||
|
["da", "dan"],
|
||||||
|
["fi", "fin"],
|
||||||
|
["fr", "fra"],
|
||||||
|
["hi", "mai"],
|
||||||
|
["ja", "jp"],
|
||||||
|
["ko", "kor"],
|
||||||
|
["ms", "may"],
|
||||||
|
["mt", "mlt"],
|
||||||
|
["nb", "nor"],
|
||||||
|
["ro", "rom"],
|
||||||
|
["ru", "ru"],
|
||||||
|
["sl", "slo"],
|
||||||
|
["es", "spa"],
|
||||||
|
["sv", "swe"],
|
||||||
|
["ta", "tam"],
|
||||||
|
["te", "tel"],
|
||||||
|
["uk", "ukr"],
|
||||||
|
["vi", "vie"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_TENCENT]: new Map([
|
||||||
|
["auto", "auto"],
|
||||||
|
["zh-CN", "zh"],
|
||||||
|
["zh-TW", "zh"],
|
||||||
|
["en", "en"],
|
||||||
|
["ar", "ar"],
|
||||||
|
["de", "de"],
|
||||||
|
["ru", "ru"],
|
||||||
|
["fr", "fr"],
|
||||||
|
["fi", "fil"],
|
||||||
|
["ko", "ko"],
|
||||||
|
["ms", "ms"],
|
||||||
|
["pt", "pt"],
|
||||||
|
["ja", "ja"],
|
||||||
|
["th", "th"],
|
||||||
|
["tr", "tr"],
|
||||||
|
["es", "es"],
|
||||||
|
["it", "it"],
|
||||||
|
["hi", "hi"],
|
||||||
|
["id", "id"],
|
||||||
|
["vi", "vi"],
|
||||||
|
]),
|
||||||
|
[OPT_TRANS_OPENAI]: OPT_LANGS_SPEC_NAME,
|
||||||
|
[OPT_TRANS_GEMINI]: OPT_LANGS_SPEC_NAME,
|
||||||
|
[OPT_TRANS_GEMINI_2]: OPT_LANGS_SPEC_NAME,
|
||||||
|
[OPT_TRANS_CLAUDE]: OPT_LANGS_SPEC_NAME,
|
||||||
|
[OPT_TRANS_OLLAMA]: OPT_LANGS_SPEC_NAME,
|
||||||
|
[OPT_TRANS_OPENROUTER]: OPT_LANGS_SPEC_NAME,
|
||||||
|
[OPT_TRANS_CLOUDFLAREAI]: OPT_LANGS_SPEC_NAME,
|
||||||
|
[OPT_TRANS_CUSTOMIZE]: OPT_LANGS_SPEC_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
const specToCode = (m) =>
|
||||||
|
new Map(
|
||||||
|
Array.from(m.entries()).map(([k, v]) => {
|
||||||
|
if (v === "") {
|
||||||
|
return ["auto", "auto"];
|
||||||
|
}
|
||||||
|
if (v === "zh" || v === "ZH") {
|
||||||
|
return [v, "zh-CN"];
|
||||||
|
}
|
||||||
|
return [v, k];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 名称->CODE
|
||||||
|
export const OPT_LANGS_TO_CODE = {};
|
||||||
|
Object.entries(OPT_LANGS_TO_SPEC).forEach(([t, m]) => {
|
||||||
|
OPT_LANGS_TO_CODE[t] = specToCode(m);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const defaultNobatchPrompt = `You are a professional, authentic machine translation engine.`;
|
||||||
|
export const defaultNobatchUserPrompt = `Translate the following source text to ${INPUT_PLACE_TO}. Output translation directly without any additional text.\n\nSource Text: ${INPUT_PLACE_TEXT}\n\nTranslated Text:`;
|
||||||
|
|
||||||
|
export const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
{"targetLanguage":"<lang>","title":"<context>","description":"<context>","segments":[{"id":1,"text":"..."}],"glossary":{"sourceTerm":"targetTerm"},"tone":"<formal|casual>"}
|
||||||
|
|
||||||
|
Output:
|
||||||
|
{"translations":[{"id":1,"text":"...","sourceLanguage":"<detected>"}]}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Use title/description for context only; do not output them.
|
||||||
|
2. Keep id, order, and count of segments.
|
||||||
|
3. Preserve whitespace, HTML entities, and all HTML-like tags (e.g., <i1>, <a1>). Translate inner text only.
|
||||||
|
4. Highest priority: Follow 'glossary'. Use value for translation; if value is "", keep the key.
|
||||||
|
5. Do not translate: content in <code>, <pre>, text enclosed in backticks, or placeholders like {1}, {{1}}, [1], [[1]].
|
||||||
|
6. Apply the specified tone to the translation.
|
||||||
|
7. Detect sourceLanguage for each segment.
|
||||||
|
8. Return empty or unchanged inputs as is.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
Input: {"targetLanguage":"zh-CN","segments":[{"id":1,"text":"A <b>React</b> component."}],"glossary":{"component":"组件","React":""}}
|
||||||
|
Output: {"translations":[{"id":1,"text":"一个<b>React</b>组件","sourceLanguage":"en"}]}
|
||||||
|
|
||||||
|
Fail-safe: On any error, return {"translations":[]}.`;
|
||||||
|
|
||||||
|
// const defaultSubtitlePrompt = `Goal: Convert raw subtitle event JSON into a clean, sentence-based JSON array.
|
||||||
|
|
||||||
|
// Output (valid JSON array, output ONLY this array):
|
||||||
|
// [{
|
||||||
|
// "text": "string", // Full sentence with correct punctuation
|
||||||
|
// "translation": "string", // Translation in ${INPUT_PLACE_TO}
|
||||||
|
// "start": int, // Start time (ms)
|
||||||
|
// "end": int, // End time (ms)
|
||||||
|
// }]
|
||||||
|
|
||||||
|
// Guidelines:
|
||||||
|
// 1. **Segmentation**: Merge sequential 'utf8' strings from 'segs' into full sentences, merging groups logically.
|
||||||
|
// 2. **Punctuation**: Ensure proper sentence-final punctuation (., ?, !); add if missing.
|
||||||
|
// 3. **Translation**: Translate 'text' into ${INPUT_PLACE_TO}, place result in 'translation'.
|
||||||
|
// 4. **Special Cases**: '[Music]' (and similar cues) are standalone entries. Translate appropriately (e.g., '[音乐]', '[Musique]').
|
||||||
|
// `;
|
||||||
|
|
||||||
|
export const defaultSubtitlePrompt = `You are an expert AI for subtitle generation. Convert a JSON array of word-level timestamps into a bilingual VTT file.
|
||||||
|
|
||||||
|
**Workflow:**
|
||||||
|
1. Merge \`text\` fields into complete sentences; ignore empty text.
|
||||||
|
2. Split long sentences into smaller, manageable subtitle cues (one sentence per cue).
|
||||||
|
3. Translate each cue into ${INPUT_PLACE_TO}.
|
||||||
|
4. Format as VTT:
|
||||||
|
- Start with \`WEBVTT\`.
|
||||||
|
- Each cue: timestamps (\`start --> end\` in milliseconds), original text, translated text.
|
||||||
|
- Keep non-speech text (e.g., \`[Music]\`) untranslated.
|
||||||
|
- Separate cues with a blank line.
|
||||||
|
|
||||||
|
**Output:** Only the pure VTT content.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
\`\`\`vtt
|
||||||
|
WEBVTT
|
||||||
|
|
||||||
|
1000 --> 3500
|
||||||
|
Hello world!
|
||||||
|
你好,世界!
|
||||||
|
|
||||||
|
4000 --> 6000
|
||||||
|
Good morning.
|
||||||
|
早上好。
|
||||||
|
\`\`\``;
|
||||||
|
|
||||||
|
const defaultRequestHook = `async (args, { url, body, headers, userMsg, method } = {}) => {
|
||||||
|
console.log("request hook args:", { args, url, body, headers, userMsg, method });
|
||||||
|
// return { url, body, headers, userMsg, method };
|
||||||
|
};`;
|
||||||
|
|
||||||
|
const defaultResponseHook = `async ({ res, ...args }) => {
|
||||||
|
console.log("reaponse hook args:", { res, args });
|
||||||
|
// const translations = [["你好", "zh"]];
|
||||||
|
// const modelMsg = "";
|
||||||
|
// return { translations, modelMsg };
|
||||||
|
};`;
|
||||||
|
|
||||||
|
// 翻译接口默认参数
|
||||||
|
const defaultApi = {
|
||||||
|
apiSlug: "", // 唯一标识
|
||||||
|
apiName: "", // 接口名称
|
||||||
|
apiType: "", // 接口类型
|
||||||
|
url: "",
|
||||||
|
key: "",
|
||||||
|
model: "", // 模型名称
|
||||||
|
systemPrompt: defaultSystemPrompt,
|
||||||
|
subtitlePrompt: defaultSubtitlePrompt,
|
||||||
|
nobatchPrompt: defaultNobatchPrompt,
|
||||||
|
nobatchUserPrompt: defaultNobatchUserPrompt,
|
||||||
|
userPrompt: "",
|
||||||
|
tone: BUILTIN_STONES[0], // 翻译风格
|
||||||
|
placeholder: BUILTIN_PLACEHOLDERS[0], // 占位符
|
||||||
|
placetag: [BUILTIN_PLACETAGS[0]], // 占位标签
|
||||||
|
// aiTerms: false, // AI智能专业术语 (todo: 备用)
|
||||||
|
customHeader: "",
|
||||||
|
customBody: "",
|
||||||
|
reqHook: "", // request 钩子函数
|
||||||
|
resHook: "", // response 钩子函数
|
||||||
|
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大请求数量
|
||||||
|
fetchInterval: DEFAULT_FETCH_INTERVAL, // 请求间隔时间
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT * 3, // 请求超时时间
|
||||||
|
batchInterval: DEFAULT_BATCH_INTERVAL, // 批处理请求间隔时间
|
||||||
|
batchSize: DEFAULT_BATCH_SIZE, // 每次最多发送段落数量
|
||||||
|
batchLength: DEFAULT_BATCH_LENGTH, // 每次发送最大文字数量
|
||||||
|
useBatchFetch: false, // 是否启用聚合发送请求
|
||||||
|
useContext: false, // 是否启用智能上下文
|
||||||
|
contextSize: DEFAULT_CONTEXT_SIZE, // 智能上下文保留会话数
|
||||||
|
temperature: 0.0,
|
||||||
|
maxTokens: 20480,
|
||||||
|
// think: false, // (OpenAI 兼容接口未支持,暂时移除)
|
||||||
|
// thinkIgnore: "qwen3,deepseek-r1", // (OpenAI 兼容接口未支持,暂时移除)
|
||||||
|
isDisabled: false, // 是否不显示,
|
||||||
|
region: "", // Azure 专用
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultApiOpts = {
|
||||||
|
[OPT_TRANS_BUILTINAI]: defaultApi,
|
||||||
|
[OPT_TRANS_GOOGLE]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://translate.googleapis.com/translate_a/single",
|
||||||
|
},
|
||||||
|
[OPT_TRANS_GOOGLE_2]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://translate-pa.googleapis.com/v1/translateHtml",
|
||||||
|
key: "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_MICROSOFT]: {
|
||||||
|
...defaultApi,
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_AZUREAI]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_BAIDU]: {
|
||||||
|
...defaultApi,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_TENCENT]: {
|
||||||
|
...defaultApi,
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_VOLCENGINE]: {
|
||||||
|
...defaultApi,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_DEEPL]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api-free.deepl.com/v2/translate",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_DEEPLFREE]: {
|
||||||
|
...defaultApi,
|
||||||
|
fetchLimit: 1,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_DEEPLX]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "http://localhost:1188/translate",
|
||||||
|
},
|
||||||
|
[OPT_TRANS_NIUTRANS]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api.niutrans.com/NiuTransServer/translation",
|
||||||
|
dictNo: "",
|
||||||
|
memoryNo: "",
|
||||||
|
},
|
||||||
|
[OPT_TRANS_OPENAI]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api.openai.com/v1/chat/completions",
|
||||||
|
model: "gpt-4",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_GEMINI]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
|
||||||
|
model: "gemini-2.5-flash",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_GEMINI_2]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
|
||||||
|
model: "gemini-2.0-flash",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_CLAUDE]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api.anthropic.com/v1/messages",
|
||||||
|
model: "claude-3-haiku-20240307",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_CLOUDFLAREAI]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
|
||||||
|
},
|
||||||
|
[OPT_TRANS_OLLAMA]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "http://localhost:11434/v1/chat/completions",
|
||||||
|
model: "llama3.1",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_OPENROUTER]: {
|
||||||
|
...defaultApi,
|
||||||
|
url: "https://openrouter.ai/api/v1/chat/completions",
|
||||||
|
model: "openai/gpt-4o",
|
||||||
|
useBatchFetch: true,
|
||||||
|
},
|
||||||
|
[OPT_TRANS_CUSTOMIZE]: {
|
||||||
|
...defaultApi,
|
||||||
|
reqHook: defaultRequestHook,
|
||||||
|
resHook: defaultResponseHook,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 内置翻译接口列表(带参数)
|
||||||
|
export const DEFAULT_API_LIST = OPT_ALL_TRANS_TYPES.map((apiType) => ({
|
||||||
|
...defaultApiOpts[apiType],
|
||||||
|
apiSlug: apiType,
|
||||||
|
apiName: apiType,
|
||||||
|
apiType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const DEFAULT_API_TYPE = OPT_TRANS_MICROSOFT;
|
||||||
|
export const DEFAULT_API_SETTING = DEFAULT_API_LIST.find(
|
||||||
|
(a) => a.apiType === DEFAULT_API_TYPE
|
||||||
|
);
|
||||||
@@ -2,3 +2,14 @@ export const APP_NAME = process.env.REACT_APP_NAME.trim()
|
|||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.join("-");
|
.join("-");
|
||||||
export const APP_LCNAME = APP_NAME.toLowerCase();
|
export const APP_LCNAME = APP_NAME.toLowerCase();
|
||||||
|
export const APP_UPNAME = APP_NAME.toUpperCase();
|
||||||
|
export const APP_CONSTS = {
|
||||||
|
fabID: `${APP_LCNAME}-fab`,
|
||||||
|
boxID: `${APP_LCNAME}-box`,
|
||||||
|
popupID: `${APP_LCNAME}-popup`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const APP_VERSION = process.env.REACT_APP_VERSION.split(".");
|
||||||
|
|
||||||
|
export const THEME_LIGHT = "light";
|
||||||
|
export const THEME_DARK = "dark";
|
||||||
|
|||||||
15
src/config/client.js
Normal file
15
src/config/client.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const CLIENT_WEB = "web";
|
||||||
|
export const CLIENT_CHROME = "chrome";
|
||||||
|
export const CLIENT_EDGE = "edge";
|
||||||
|
export const CLIENT_FIREFOX = "firefox";
|
||||||
|
export const CLIENT_USERSCRIPT = "userscript";
|
||||||
|
export const CLIENT_THUNDERBIRD = "thunderbird";
|
||||||
|
export const CLIENT_EXTS = [
|
||||||
|
CLIENT_CHROME,
|
||||||
|
CLIENT_EDGE,
|
||||||
|
CLIENT_FIREFOX,
|
||||||
|
CLIENT_THUNDERBIRD,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_USER_AGENT =
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
|
||||||
2082
src/config/i18n.js
2082
src/config/i18n.js
File diff suppressed because it is too large
Load Diff
@@ -1,445 +1,10 @@
|
|||||||
import {
|
export * from "./app";
|
||||||
DEFAULT_SELECTOR,
|
export * from "./rules";
|
||||||
GLOBAL_KEY,
|
export * from "./api";
|
||||||
REMAIN_KEY,
|
export * from "./setting";
|
||||||
SHADOW_KEY,
|
export * from "./i18n";
|
||||||
DEFAULT_RULE,
|
export * from "./storage";
|
||||||
DEFAULT_OW_RULE,
|
export * from "./url";
|
||||||
BUILTIN_RULES,
|
export * from "./msg";
|
||||||
} from "./rules";
|
export * from "./client";
|
||||||
import { APP_NAME, APP_LCNAME } from "./app";
|
export * from "./styles";
|
||||||
export { I18N, UI_LANGS } from "./i18n";
|
|
||||||
export {
|
|
||||||
GLOBAL_KEY,
|
|
||||||
REMAIN_KEY,
|
|
||||||
SHADOW_KEY,
|
|
||||||
DEFAULT_RULE,
|
|
||||||
DEFAULT_OW_RULE,
|
|
||||||
BUILTIN_RULES,
|
|
||||||
APP_LCNAME,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
|
||||||
export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
|
|
||||||
export const STOKEY_SETTING = `${APP_NAME}_setting`;
|
|
||||||
export const STOKEY_RULES = `${APP_NAME}_rules`;
|
|
||||||
export const STOKEY_WFRULES = `${APP_NAME}_webfix_rules`;
|
|
||||||
export const STOKEY_WORDS = `${APP_NAME}_words`;
|
|
||||||
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
|
||||||
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
|
||||||
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
|
||||||
export const STOKEY_WEBFIXCACHE_PREFIX = `${APP_NAME}_webfixcache_`;
|
|
||||||
|
|
||||||
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
|
|
||||||
export const CMD_TOGGLE_STYLE = "toggleStyle";
|
|
||||||
export const CMD_OPEN_OPTIONS = "openOptions";
|
|
||||||
|
|
||||||
export const CLIENT_WEB = "web";
|
|
||||||
export const CLIENT_CHROME = "chrome";
|
|
||||||
export const CLIENT_EDGE = "edge";
|
|
||||||
export const CLIENT_FIREFOX = "firefox";
|
|
||||||
export const CLIENT_USERSCRIPT = "userscript";
|
|
||||||
export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX];
|
|
||||||
|
|
||||||
export const KV_RULES_KEY = "kiss-rules.json";
|
|
||||||
export const KV_WFRULES_KEY = "kiss-webfix.json";
|
|
||||||
export const KV_WORDS_KEY = "kiss-words.json";
|
|
||||||
export const KV_RULES_SHARE_KEY = "kiss-rules-share.json";
|
|
||||||
export const KV_SETTING_KEY = "kiss-setting.json";
|
|
||||||
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
|
||||||
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
|
||||||
|
|
||||||
export const CACHE_NAME = `${APP_NAME}_cache`;
|
|
||||||
|
|
||||||
export const MSG_FETCH = "fetch";
|
|
||||||
export const MSG_FETCH_LIMIT = "fetch_limit";
|
|
||||||
export const MSG_FETCH_CLEAR = "fetch_clear";
|
|
||||||
export const MSG_OPEN_OPTIONS = "open_options";
|
|
||||||
export const MSG_SAVE_RULE = "save_rule";
|
|
||||||
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
|
||||||
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
|
||||||
export const MSG_TRANS_GETRULE = "trans_getrule";
|
|
||||||
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
|
||||||
export const MSG_TRANS_CURRULE = "trans_currule";
|
|
||||||
|
|
||||||
export const THEME_LIGHT = "light";
|
|
||||||
export const THEME_DARK = "dark";
|
|
||||||
|
|
||||||
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
|
|
||||||
export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
|
|
||||||
export const URL_KISS_RULES = "https://github.com/fishjar/kiss-rules";
|
|
||||||
export const URL_KISS_RULES_NEW_ISSUE =
|
|
||||||
"https://github.com/fishjar/kiss-rules/issues/new";
|
|
||||||
export const URL_RAW_PREFIX =
|
|
||||||
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
|
||||||
|
|
||||||
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
|
|
||||||
export const URL_MICROSOFT_TRAN =
|
|
||||||
"https://api-edge.cognitive.microsofttranslator.com/translate";
|
|
||||||
export const URL_MICROSOFT_AUTH = "https://edge.microsoft.com/translate/auth";
|
|
||||||
export const URL_BAIDU_LANGDETECT = "https://fanyi.baidu.com/langdetect";
|
|
||||||
export const URL_BAIDU_WEB = "https://fanyi.baidu.com/";
|
|
||||||
export const URL_BAIDU_TRAN = "https://fanyi.baidu.com/v2transapi";
|
|
||||||
export const URL_DEEPLFREE_TRAN = "https://www2.deepl.com/jsonrpc";
|
|
||||||
export const URL_TENCENT_TRANSMART = "https://transmart.qq.com/api/imt";
|
|
||||||
|
|
||||||
export const OPT_TRANS_GOOGLE = "Google";
|
|
||||||
export const OPT_TRANS_MICROSOFT = "Microsoft";
|
|
||||||
export const OPT_TRANS_DEEPL = "DeepL";
|
|
||||||
export const OPT_TRANS_DEEPLX = "DeepLX";
|
|
||||||
export const OPT_TRANS_DEEPLFREE = "DeepLFree";
|
|
||||||
export const OPT_TRANS_BAIDU = "Baidu";
|
|
||||||
export const OPT_TRANS_TENCENT = "Tencent";
|
|
||||||
export const OPT_TRANS_OPENAI = "OpenAI";
|
|
||||||
export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI";
|
|
||||||
export const OPT_TRANS_CUSTOMIZE = "Custom";
|
|
||||||
export const OPT_TRANS_ALL = [
|
|
||||||
OPT_TRANS_GOOGLE,
|
|
||||||
OPT_TRANS_MICROSOFT,
|
|
||||||
OPT_TRANS_DEEPL,
|
|
||||||
OPT_TRANS_DEEPLFREE,
|
|
||||||
OPT_TRANS_DEEPLX,
|
|
||||||
OPT_TRANS_BAIDU,
|
|
||||||
OPT_TRANS_TENCENT,
|
|
||||||
OPT_TRANS_OPENAI,
|
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
|
||||||
OPT_TRANS_CUSTOMIZE,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const OPT_LANGS_TO = [
|
|
||||||
["en", "English - English"],
|
|
||||||
["zh-CN", "Simplified Chinese - 简体中文"],
|
|
||||||
["zh-TW", "Traditional Chinese - 繁體中文"],
|
|
||||||
["ar", "Arabic - العربية"],
|
|
||||||
["bg", "Bulgarian - Български"],
|
|
||||||
["ca", "Catalan - Català"],
|
|
||||||
["hr", "Croatian - Hrvatski"],
|
|
||||||
["cs", "Czech - Čeština"],
|
|
||||||
["da", "Danish - Dansk"],
|
|
||||||
["nl", "Dutch - Nederlands"],
|
|
||||||
["fi", "Finnish - Suomi"],
|
|
||||||
["fr", "French - Français"],
|
|
||||||
["de", "German - Deutsch"],
|
|
||||||
["el", "Greek - Ελληνικά"],
|
|
||||||
["hi", "Hindi - हिन्दी"],
|
|
||||||
["hu", "Hungarian - Magyar"],
|
|
||||||
["id", "Indonesian - Indonesia"],
|
|
||||||
["it", "Italian - Italiano"],
|
|
||||||
["ja", "Japanese - 日本語"],
|
|
||||||
["ko", "Korean - 한국어"],
|
|
||||||
["ms", "Malay - Melayu"],
|
|
||||||
["mt", "Maltese - Malti"],
|
|
||||||
["nb", "Norwegian - Norsk Bokmål"],
|
|
||||||
["pl", "Polish - Polski"],
|
|
||||||
["pt", "Portuguese - Português"],
|
|
||||||
["ro", "Romanian - Română"],
|
|
||||||
["ru", "Russian - Русский"],
|
|
||||||
["sk", "Slovak - Slovenčina"],
|
|
||||||
["sl", "Slovenian - Slovenščina"],
|
|
||||||
["es", "Spanish - Español"],
|
|
||||||
["sv", "Swedish - Svenska"],
|
|
||||||
["ta", "Tamil - தமிழ்"],
|
|
||||||
["te", "Telugu - తెలుగు"],
|
|
||||||
["th", "Thai - ไทย"],
|
|
||||||
["tr", "Turkish - Türkçe"],
|
|
||||||
["uk", "Ukrainian - Українська"],
|
|
||||||
["vi", "Vietnamese - Tiếng Việt"],
|
|
||||||
];
|
|
||||||
export const OPT_LANGS_FROM = [["auto", "Auto-detect"], ...OPT_LANGS_TO];
|
|
||||||
export const OPT_LANGS_SPECIAL = {
|
|
||||||
[OPT_TRANS_GOOGLE]: new Map(OPT_LANGS_FROM.map(([key]) => [key, key])),
|
|
||||||
[OPT_TRANS_MICROSOFT]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
|
||||||
["auto", ""],
|
|
||||||
["zh-CN", "zh-Hans"],
|
|
||||||
["zh-TW", "zh-Hant"],
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_DEEPL]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
|
||||||
["auto", ""],
|
|
||||||
["zh-CN", "ZH"],
|
|
||||||
["zh-TW", "ZH"],
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_DEEPLFREE]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
|
||||||
["auto", "auto"],
|
|
||||||
["zh-CN", "ZH"],
|
|
||||||
["zh-TW", "ZH"],
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_DEEPLX]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key.toUpperCase()]),
|
|
||||||
["auto", ""],
|
|
||||||
["zh-CN", "ZH"],
|
|
||||||
["zh-TW", "ZH"],
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_BAIDU]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
|
||||||
["zh-CN", "zh"],
|
|
||||||
["zh-TW", "cht"],
|
|
||||||
["ar", "ara"],
|
|
||||||
["bg", "bul"],
|
|
||||||
["ca", "cat"],
|
|
||||||
["hr", "hrv"],
|
|
||||||
["da", "dan"],
|
|
||||||
["fi", "fin"],
|
|
||||||
["fr", "fra"],
|
|
||||||
["hi", "mai"],
|
|
||||||
["ja", "jp"],
|
|
||||||
["ko", "kor"],
|
|
||||||
["ms", "may"],
|
|
||||||
["mt", "mlt"],
|
|
||||||
["nb", "nor"],
|
|
||||||
["ro", "rom"],
|
|
||||||
["ru", "ru"],
|
|
||||||
["sl", "slo"],
|
|
||||||
["es", "spa"],
|
|
||||||
["sv", "swe"],
|
|
||||||
["ta", "tam"],
|
|
||||||
["te", "tel"],
|
|
||||||
["uk", "ukr"],
|
|
||||||
["vi", "vie"],
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_TENCENT]: new Map([
|
|
||||||
["auto", "auto"],
|
|
||||||
["zh-CN", "zh"],
|
|
||||||
["zh-TW", "zh"],
|
|
||||||
["en", "en"],
|
|
||||||
["ar", "ar"],
|
|
||||||
["de", "de"],
|
|
||||||
["ru", "ru"],
|
|
||||||
["fr", "fr"],
|
|
||||||
["fi", "fil"],
|
|
||||||
["ko", "ko"],
|
|
||||||
["ms", "ms"],
|
|
||||||
["pt", "pt"],
|
|
||||||
["ja", "ja"],
|
|
||||||
["th", "th"],
|
|
||||||
["tr", "tr"],
|
|
||||||
["es", "es"],
|
|
||||||
["it", "it"],
|
|
||||||
["hi", "hi"],
|
|
||||||
["id", "id"],
|
|
||||||
["vi", "vi"],
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_OPENAI]: new Map(
|
|
||||||
OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]])
|
|
||||||
),
|
|
||||||
[OPT_TRANS_CLOUDFLAREAI]: new Map([
|
|
||||||
["auto", ""],
|
|
||||||
["zh-CN", "chinese"],
|
|
||||||
["zh-TW", "chinese"],
|
|
||||||
["en", "english"],
|
|
||||||
["ar", "arabic"],
|
|
||||||
["de", "german"],
|
|
||||||
["ru", "russian"],
|
|
||||||
["fr", "french"],
|
|
||||||
["pt", "portuguese"],
|
|
||||||
["ja", "japanese"],
|
|
||||||
["es", "spanish"],
|
|
||||||
["hi", "hindi"],
|
|
||||||
]),
|
|
||||||
[OPT_TRANS_CUSTOMIZE]: new Map([
|
|
||||||
...OPT_LANGS_FROM.map(([key]) => [key, key]),
|
|
||||||
["auto", ""],
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang);
|
|
||||||
export const OPT_LANGS_BAIDU = new Map(
|
|
||||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_BAIDU].entries()).map(([k, v]) => [
|
|
||||||
v,
|
|
||||||
k,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
export const OPT_LANGS_TENCENT = new Map(
|
|
||||||
Array.from(OPT_LANGS_SPECIAL[OPT_TRANS_TENCENT].entries()).map(([k, v]) => [
|
|
||||||
v,
|
|
||||||
k,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
OPT_LANGS_TENCENT.set("zh", "zh-CN");
|
|
||||||
|
|
||||||
export const OPT_STYLE_NONE = "style_none"; // 无
|
|
||||||
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
|
||||||
export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
|
|
||||||
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
|
||||||
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
|
||||||
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
|
||||||
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
|
||||||
export const OPT_STYLE_DIY = "diy_style"; // 自定义样式
|
|
||||||
export const OPT_STYLE_ALL = [
|
|
||||||
OPT_STYLE_NONE,
|
|
||||||
OPT_STYLE_LINE,
|
|
||||||
OPT_STYLE_DOTLINE,
|
|
||||||
OPT_STYLE_DASHLINE,
|
|
||||||
OPT_STYLE_WAVYLINE,
|
|
||||||
OPT_STYLE_FUZZY,
|
|
||||||
OPT_STYLE_HIGHLIGHT,
|
|
||||||
OPT_STYLE_DIY,
|
|
||||||
];
|
|
||||||
export const OPT_STYLE_USE_COLOR = [
|
|
||||||
OPT_STYLE_LINE,
|
|
||||||
OPT_STYLE_DOTLINE,
|
|
||||||
OPT_STYLE_DASHLINE,
|
|
||||||
OPT_STYLE_WAVYLINE,
|
|
||||||
OPT_STYLE_HIGHLIGHT,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const OPT_MOUSEKEY_DISABLE = "mk_disable";
|
|
||||||
export const OPT_MOUSEKEY_MOUSEOVER = "mk_mouseover";
|
|
||||||
export const OPT_MOUSEKEY_CONTROL = "mk_ctrlKey";
|
|
||||||
export const OPT_MOUSEKEY_SHIFT = "mk_shiftKey";
|
|
||||||
export const OPT_MOUSEKEY_ALT = "mk_altKey";
|
|
||||||
export const OPT_MOUSEKEY_ALL = [
|
|
||||||
OPT_MOUSEKEY_DISABLE,
|
|
||||||
OPT_MOUSEKEY_MOUSEOVER,
|
|
||||||
OPT_MOUSEKEY_CONTROL,
|
|
||||||
OPT_MOUSEKEY_SHIFT,
|
|
||||||
OPT_MOUSEKEY_ALT,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const DEFAULT_FETCH_LIMIT = 10; // 默认最大任务数量
|
|
||||||
export const DEFAULT_FETCH_INTERVAL = 100; // 默认任务间隔时间
|
|
||||||
|
|
||||||
export const PROMPT_PLACE_FROM = "{{from}}"; // 占位符
|
|
||||||
export const PROMPT_PLACE_TO = "{{to}}"; // 占位符
|
|
||||||
|
|
||||||
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
|
||||||
|
|
||||||
// 全局规则
|
|
||||||
export const GLOBLA_RULE = {
|
|
||||||
pattern: "*",
|
|
||||||
selector: DEFAULT_SELECTOR,
|
|
||||||
translator: OPT_TRANS_MICROSOFT,
|
|
||||||
fromLang: "auto",
|
|
||||||
toLang: "zh-CN",
|
|
||||||
textStyle: OPT_STYLE_DASHLINE,
|
|
||||||
transOpen: "false",
|
|
||||||
bgColor: "",
|
|
||||||
textDiyStyle: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 输入框翻译
|
|
||||||
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
|
|
||||||
export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
|
|
||||||
export const DEFAULT_INPUT_RULE = {
|
|
||||||
transOpen: true,
|
|
||||||
translator: OPT_TRANS_MICROSOFT,
|
|
||||||
fromLang: "auto",
|
|
||||||
toLang: "en",
|
|
||||||
triggerShortcut: DEFAULT_INPUT_SHORTCUT,
|
|
||||||
triggerCount: 1,
|
|
||||||
triggerTime: 200,
|
|
||||||
transSign: OPT_INPUT_TRANS_SIGNS[0],
|
|
||||||
};
|
|
||||||
|
|
||||||
// 划词翻译
|
|
||||||
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyB"];
|
|
||||||
export const DEFAULT_TRANBOX_SETTING = {
|
|
||||||
transOpen: true,
|
|
||||||
translator: OPT_TRANS_MICROSOFT,
|
|
||||||
fromLang: "auto",
|
|
||||||
toLang: "zh-CN",
|
|
||||||
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
|
|
||||||
btnOffsetX: 10,
|
|
||||||
btnOffsetY: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 订阅列表
|
|
||||||
export const DEFAULT_SUBRULES_LIST = [
|
|
||||||
{
|
|
||||||
url: process.env.REACT_APP_RULESURL,
|
|
||||||
selected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: process.env.REACT_APP_RULESURL_ON,
|
|
||||||
selected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: process.env.REACT_APP_RULESURL_OFF,
|
|
||||||
selected: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 翻译接口
|
|
||||||
export const DEFAULT_TRANS_APIS = {
|
|
||||||
[OPT_TRANS_GOOGLE]: {
|
|
||||||
url: "https://translate.googleapis.com/translate_a/single",
|
|
||||||
key: "",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_DEEPL]: {
|
|
||||||
url: "https://api-free.deepl.com/v2/translate",
|
|
||||||
key: "",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_DEEPLX]: {
|
|
||||||
url: "http://localhost:1188/translate",
|
|
||||||
key: "",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_OPENAI]: {
|
|
||||||
url: "https://api.openai.com/v1/chat/completions",
|
|
||||||
key: "",
|
|
||||||
model: "gpt-4",
|
|
||||||
prompt: `You will be provided with a sentence in ${PROMPT_PLACE_FROM}, and your task is to translate it into ${PROMPT_PLACE_TO}.`,
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CLOUDFLAREAI]: {
|
|
||||||
url: "https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/run/@cf/meta/m2m100-1.2b",
|
|
||||||
key: "",
|
|
||||||
},
|
|
||||||
[OPT_TRANS_CUSTOMIZE]: {
|
|
||||||
url: "",
|
|
||||||
key: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 默认快捷键
|
|
||||||
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
|
|
||||||
export const OPT_SHORTCUT_STYLE = "toggleStyle";
|
|
||||||
export const OPT_SHORTCUT_POPUP = "togglePopup";
|
|
||||||
export const OPT_SHORTCUT_SETTING = "openSetting";
|
|
||||||
export const DEFAULT_SHORTCUTS = {
|
|
||||||
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
|
|
||||||
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
|
|
||||||
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
|
|
||||||
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyN"],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TRANS_MIN_LENGTH = 5; // 最短翻译长度
|
|
||||||
export const TRANS_MAX_LENGTH = 5000; // 最长翻译长度
|
|
||||||
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
|
|
||||||
|
|
||||||
export const DEFAULT_SETTING = {
|
|
||||||
darkMode: false, // 深色模式
|
|
||||||
uiLang: "en", // 界面语言
|
|
||||||
fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量
|
|
||||||
fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间
|
|
||||||
minLength: TRANS_MIN_LENGTH,
|
|
||||||
maxLength: TRANS_MAX_LENGTH,
|
|
||||||
newlineLength: TRANS_NEWLINE_LENGTH,
|
|
||||||
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
|
||||||
injectRules: true, // 是否注入订阅规则
|
|
||||||
injectWebfix: true, // 是否注入修复补丁
|
|
||||||
detectRemote: false, // 是否使用远程语言检测
|
|
||||||
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
|
||||||
owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
|
|
||||||
transApis: DEFAULT_TRANS_APIS, // 翻译接口
|
|
||||||
mouseKey: OPT_MOUSEKEY_DISABLE, // 鼠标悬停翻译
|
|
||||||
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
|
||||||
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
|
||||||
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
|
|
||||||
touchTranslate: 2, // 触屏翻译
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_RULES = [GLOBLA_RULE];
|
|
||||||
|
|
||||||
export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
|
|
||||||
export const OPT_SYNCTYPE_WEBDAV = "WebDAV";
|
|
||||||
export const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
|
|
||||||
|
|
||||||
export const DEFAULT_SYNC = {
|
|
||||||
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
|
|
||||||
syncUrl: "", // 数据同步接口
|
|
||||||
syncUser: "", // 数据同步用户名
|
|
||||||
syncKey: "", // 数据同步密钥
|
|
||||||
syncMeta: {}, // 数据更新及同步信息
|
|
||||||
subRulesSyncAt: 0, // 订阅规则同步时间
|
|
||||||
dataCaches: {}, // 缓存同步时间
|
|
||||||
};
|
|
||||||
|
|||||||
38
src/config/msg.js
Normal file
38
src/config/msg.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export const CMD_TOGGLE_TRANSLATE = "toggleTranslate";
|
||||||
|
export const CMD_TOGGLE_STYLE = "toggleStyle";
|
||||||
|
export const CMD_OPEN_OPTIONS = "openOptions";
|
||||||
|
export const CMD_OPEN_TRANBOX = "openTranbox";
|
||||||
|
|
||||||
|
export const MSG_FETCH = "kiss_fetch";
|
||||||
|
export const MSG_GET_HTTPCACHE = "get_httpcache";
|
||||||
|
export const MSG_PUT_HTTPCACHE = "put_httpcache";
|
||||||
|
export const MSG_OPEN_OPTIONS = "open_options";
|
||||||
|
export const MSG_SAVE_RULE = "save_rule";
|
||||||
|
export const MSG_TRANS_TOGGLE = "trans_toggle";
|
||||||
|
export const MSG_TRANS_TOGGLE_STYLE = "trans_toggle_style";
|
||||||
|
export const MSG_OPEN_TRANBOX = "open_tranbox";
|
||||||
|
export const MSG_TRANS_GETRULE = "trans_getrule";
|
||||||
|
export const MSG_TRANS_PUTRULE = "trans_putrule";
|
||||||
|
export const MSG_TRANS_CURRULE = "trans_currule";
|
||||||
|
export const MSG_TRANSBOX_TOGGLE = "transbox_toggle";
|
||||||
|
export const MSG_POPUP_TOGGLE = "popup_toggle";
|
||||||
|
export const MSG_MOUSEHOVER_TOGGLE = "mousehover_toggle";
|
||||||
|
export const MSG_TRANSINPUT_TOGGLE = "transinput_toggle";
|
||||||
|
export const MSG_CONTEXT_MENUS = "context_menus";
|
||||||
|
export const MSG_COMMAND_SHORTCUTS = "command_shortcuts";
|
||||||
|
export const MSG_INJECT_JS = "inject_js";
|
||||||
|
export const MSG_INJECT_CSS = "inject_css";
|
||||||
|
export const MSG_UPDATE_CSP = "update_csp";
|
||||||
|
export const MSG_BUILTINAI_DETECT = "builtinai_detect";
|
||||||
|
export const MSG_BUILTINAI_TRANSLATE = "builtinai_translte";
|
||||||
|
export const MSG_SET_LOGLEVEL = "set_loglevel";
|
||||||
|
export const MSG_CLEAR_CACHES = "clear_caches";
|
||||||
|
|
||||||
|
export const EVENT_KISS = "event_kiss_translate";
|
||||||
|
|
||||||
|
export const MSG_XHR_DATA_YOUTUBE = "KISS_XHR_DATA_YOUTUBE";
|
||||||
|
// export const MSG_GLOBAL_VAR_FETCH = "KISS_GLOBAL_VAR_FETCH";
|
||||||
|
// export const MSG_GLOBAL_VAR_BACK = "KISS_GLOBAL_VAR_BACK";
|
||||||
|
|
||||||
|
export const MSG_MENUS_PROGRESSED = "progressed";
|
||||||
|
export const MSG_MENUS_UPDATEFORM = "updateFormData";
|
||||||
713
src/config/quotes.js
Normal file
713
src/config/quotes.js
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
const quotes = [
|
||||||
|
{
|
||||||
|
en: "The unexamined life is not worth living.",
|
||||||
|
zh: "未经审视的人生不值得过。",
|
||||||
|
zh_TW: "未經審視的人生不值得過。",
|
||||||
|
ja: "吟味されない人生は生きるに値しない。",
|
||||||
|
ko: "성찰하지 않는 삶은 살 가치가 없다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "I think, therefore I am.",
|
||||||
|
zh: "我思故我在。",
|
||||||
|
zh_TW: "我思故我在。",
|
||||||
|
ja: "我思う、ゆえに我あり。",
|
||||||
|
ko: "나는 생각한다, 고로 존재한다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "He who has a why to live for can bear almost any how.",
|
||||||
|
zh: "知道为何而活的人,几乎能忍受任何一种生活。",
|
||||||
|
zh_TW: "知道為何而活的人,幾乎能忍受任何一種生活。",
|
||||||
|
ja: "生きるための「なぜ」を持つ者は、ほとんどあらゆる「どのように」にも耐えることができる。",
|
||||||
|
ko: "살아야 할 이유를 아는 사람은 거의 모든 상황을 견딜 수 있다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Life is what happens when you're busy making other plans.",
|
||||||
|
zh: "生活就是当你忙着制定其他计划时所发生的事情。",
|
||||||
|
zh_TW: "生活就是當你忙著制定其他計劃時所發生的事情。",
|
||||||
|
ja: "人生とは、他の計画を立てるのに忙しいときに起こるものだ。",
|
||||||
|
ko: "인생은 다른 계획을 세우느라 바쁠 때 일어나는 일이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Get busy living or get busy dying.",
|
||||||
|
zh: "要么忙着活,要么忙着死。",
|
||||||
|
zh_TW: "要么忙著活,要么忙著死。",
|
||||||
|
ja: "必死に生きるか、必死に死ぬかだ。",
|
||||||
|
ko: "바쁘게 살거나, 바쁘게 죽거나。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "We are what we repeatedly do. Excellence, then, is not an act, but a habit.",
|
||||||
|
zh: "我们由我们反复做的事情构成的。因此,卓越不是一种行为,而是一种习惯。",
|
||||||
|
zh_TW:
|
||||||
|
"我們由我們反覆做的事情構成的。因此,卓越不是一種行為,而是一種習慣。",
|
||||||
|
ja: "我々は繰り返し行うことの集大成である。卓越とは行為ではなく、習慣なのだ。",
|
||||||
|
ko: "우리는 우리가 반복적으로 하는 일의 결과물이다. 그렇다면 탁월함은 행동이 아니라 습관이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Man is condemned to be free.",
|
||||||
|
zh: "人注定是自由的。",
|
||||||
|
zh_TW: "人註定是自由的。",
|
||||||
|
ja: "人間は自由であるように呪われている。",
|
||||||
|
ko: "인간은 자유롭도록 저주받았다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "To be, or not to be: that is the question.",
|
||||||
|
zh: "生存还是毁灭,这是一个问题。",
|
||||||
|
zh_TW: "生存還是毀滅,這是一個問題。",
|
||||||
|
ja: "生きるべきか、死ぬべきか、それが問題だ。",
|
||||||
|
ko: "죽느냐 사느냐, 그것이 문제로다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The purpose of life is not to be happy. It is to be useful, to be honorable, to be compassionate, to have it make some difference that you have lived and lived well.",
|
||||||
|
zh: "人生的目的不是快乐,而是有用、高尚、富有同情心,让你活过并且活得好,从而使世界有所不同。",
|
||||||
|
zh_TW:
|
||||||
|
"人生的目的不是快樂,而是有用、高尚、富有同情心,讓你活過並且活得好,從而使世界有所不同。",
|
||||||
|
ja: "人生(じんせい)の目的(もくてき)は幸(しあわ)せになることではない。役(やく)に立(た)つこと、名誉(めいよ)あること、思(おも)いやりを持(も)つこと、そして自分(じぶん)が生(い)きてきたこと、よく生(い)きたことが何(なに)かの違(ちが)いをもたらすようにすることだ。",
|
||||||
|
ko: "삶의 목적은 행복해지는 것이 아니다. 유용하고, 명예롭고, 자비로우며, 당신이 살았고 잘 살았다는 것이 어떤 차이를 만들도록 하는 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Life is 10% what happens to us and 90% how we react to it.",
|
||||||
|
zh: "生活 10% 取决于发生在我们身上的事,90% 取决于我们如何反应。",
|
||||||
|
zh_TW: "生活 10% 取決於發生在我們身上的事,90% 取決於我們如何反應。",
|
||||||
|
ja: "人生は、我々に起こることが10%で、それにどう反応するかが90%だ。",
|
||||||
|
ko: "인생은 우리에게 일어나는 일이 10%이고, 그 일에 대해 우리가 어떻게 반응하느냐가 90%이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The two most important days in your life are the day you are born and the day you find out why.",
|
||||||
|
zh: "你一生中最重要的两天是:你出生的那天和你明白你为何出生的那天。",
|
||||||
|
zh_TW: "你一生中最重要的兩天是:你出生的那天和你明白你為何出生的那天。",
|
||||||
|
ja: "人生で最も重要な日は二日ある。生まれた日と、なぜ生まれたかを悟る日だ。",
|
||||||
|
ko: "당신의 인생에서 가장 중요한 날은 두 번이다. 당신이 태어난 날과 그 이유를 깨닫는 날이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "In three words I can sum up everything I've learned about life: it goes on.",
|
||||||
|
zh: "关于人生,我所学到的一切可以总结为三个词:它在继续。",
|
||||||
|
zh_TW: "關於人生,我所學到的一切可以總結為三個詞:它在繼續。",
|
||||||
|
ja: "人生について学んだすべてを3語でまとめることができる。それは「それでも続く」ということだ。",
|
||||||
|
ko: "내가 인생에 대해 배운 모든 것을 세 단어로 요약할 수 있다: '삶은 계속된다'는 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Not all those who wander are lost.",
|
||||||
|
zh: "并非所有流浪者都迷失了方向。",
|
||||||
|
zh_TW: "並非所有流浪者都迷失了方向。",
|
||||||
|
ja: "さまよう者がすべて道に迷っているわけではない。",
|
||||||
|
ko: "방황하는 자가 다 길을 잃은 것은 아니다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Life is simple, but we insist on making it complicated.",
|
||||||
|
zh: "生活本简单,但我们坚持要把它弄复杂。",
|
||||||
|
zh_TW: "生活本簡單,但我們堅持要把它弄複雜。",
|
||||||
|
ja: "人生はシンプルだ。だが我々はそれを複雑にしようと躍起になる。",
|
||||||
|
ko: "인생은 단순하지만, 우리가 복잡하게 만들기를 고집한다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Our life is what our thoughts make it.",
|
||||||
|
zh: "我们的生活是由我们的思想造成的。",
|
||||||
|
zh_TW: "我們的生活是由我們的思想造成的。",
|
||||||
|
ja: "我々の人生は、我々の思考が作るものだ。",
|
||||||
|
ko: "우리의 삶은 우리의 생각이 만드는 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Find purpose, the means will follow.",
|
||||||
|
zh: "找到目标,方法自会随之而来。",
|
||||||
|
zh_TW: "找到目標,方法自會隨之而來。",
|
||||||
|
ja: "目的を見つけよ、手段は後からついてくる。",
|
||||||
|
ko: "목적을 찾으라, 수단은 따라올 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The goal of life is living in agreement with nature.",
|
||||||
|
zh: "生活的目标是与自然和谐相处。",
|
||||||
|
zh_TW: "生活的目標是與自然和諧相處。",
|
||||||
|
ja: "人生の目標は、自然と調和して生きることである。",
|
||||||
|
ko: "삶의 목표는 자연과 조화를 이루며 사는 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The only true wisdom is in knowing you know nothing.",
|
||||||
|
zh: "唯一的真正智慧在于知道自己一无所有。",
|
||||||
|
zh_TW: "唯一的真正智慧在於知道自己一無所有。",
|
||||||
|
ja: "唯一真の知恵は、自分が何も知らないことを知ることにある。",
|
||||||
|
ko: "유일한 참된 지혜는 자신이 아무것도 모른다는 것을 아는 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Knowledge is power.",
|
||||||
|
zh: "知识就是力量。",
|
||||||
|
zh_TW: "知識就是力量。",
|
||||||
|
ja: "知識は力なり。",
|
||||||
|
ko: "아는 것이 힘이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Knowing yourself is the beginning of all wisdom.",
|
||||||
|
zh: "了解自己是所有智慧的开端。",
|
||||||
|
zh_TW: "了解自己是所有智慧的開端。",
|
||||||
|
ja: "自分自身を知ることが、すべての知恵の始まりである。",
|
||||||
|
ko: "자신을 아는 것이 모든 지혜의 시작이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The journey of a thousand miles begins with a single step.",
|
||||||
|
zh: "千里之行,始于足下。",
|
||||||
|
zh_TW: "千里之行,始於足下。",
|
||||||
|
ja: "千里の道も一歩から。",
|
||||||
|
ko: "천 리 길도 한 걸음부터。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The only source of knowledge is experience.",
|
||||||
|
zh: "知识的唯一来源是经验。",
|
||||||
|
zh_TW: "知識的唯一來源是經驗。",
|
||||||
|
ja: "知識の唯一の源泉は経験である。",
|
||||||
|
ko: "지식의 유일한 원천은 경험이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "A fool thinks himself to be wise, but a wise man knows himself to be a fool.",
|
||||||
|
zh: "愚者自以为聪明,智者自知愚蠢。",
|
||||||
|
zh_TW: "愚者自以為聰明,智者自知愚蠢。",
|
||||||
|
ja: "愚か者は自分を賢いと思うが、賢い者は自分が愚かであることを知っている。",
|
||||||
|
ko: "바보는 자신이 현명하다고 생각하지만, 현명한 사람은 자신이 바보라는 것을 안다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "We learn from failure, not from success!",
|
||||||
|
zh: "我们从失败中学习,而不是从成功中!",
|
||||||
|
zh_TW: "我們從失敗中學習,而不是從成功中!",
|
||||||
|
ja: "我々は成功からではなく、失敗から学ぶ!",
|
||||||
|
ko: "우리는 성공이 아닌, 실패로부터 배운다!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The wise man is one who knows what he does not know.",
|
||||||
|
zh: "智者,知其所不知。",
|
||||||
|
zh_TW: "智者,知其所不知。",
|
||||||
|
ja: "賢い者とは、自分が何を知らないかを知っている者である。",
|
||||||
|
ko: "현명한 사람은 자신이 모르는 것을 아는 사람이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "To know that we know what we know, and that we do not know what we do not know, that is true knowledge.",
|
||||||
|
zh: "知之为知之,不知为不知,是知也。",
|
||||||
|
zh_TW: "知之為知之,不知為不知,是知也。",
|
||||||
|
ja: "知るを知るとなし、知らざるを知らずとなす、これ知るなり。",
|
||||||
|
ko: "아는 것을 안다고 하고, 모르는 것을 모른다고 하는 것, 그것이 참된 앎이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Curiosity is the wick in the candle of learning.",
|
||||||
|
zh: "好奇心是学习这支蜡烛的灯芯。",
|
||||||
|
zh_TW: "好奇心是學習這支蠟燭的燈芯。",
|
||||||
|
ja: "好奇心は、学習というロウソクの芯である。",
|
||||||
|
ko: "호기심은 배움이라는 촛불의 심지이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "It is the mark of an educated mind to be able to entertain a thought without accepting it.",
|
||||||
|
zh: "能够容纳一种思想而不同意它,这是一个受过教育的头脑的标志。",
|
||||||
|
zh_TW: "能夠容納一種思想而不同意它,這是一個受過教育的頭腦的標誌。",
|
||||||
|
ja: "ある考えを受け入れずに、その考えを持ち続けることができるのが、教育ある精神の証である。",
|
||||||
|
ko: "어떤 생각을 받아들이지 않고도 그 생각을 해볼 수 있는 것이 교육받은 마음의 특징이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Never stop questioning.",
|
||||||
|
zh: "永远不要停止提问。",
|
||||||
|
zh_TW: "永遠不要停止提問。",
|
||||||
|
ja: "疑問を持つことを決してやめるな。",
|
||||||
|
ko: "질문하는 것을 절대 멈추지 마라。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The man who asks a question is a fool for a minute, the man who does not ask is a fool for life.",
|
||||||
|
zh: "问问题的人,只傻一分钟;不问的人,傻一生。",
|
||||||
|
zh_TW: "問問題的人,只傻一分鐘;不問的人,傻一生。",
|
||||||
|
ja: "問う者は一時の恥、問わぬ者は一生の恥。",
|
||||||
|
ko: "질문하는 사람은 1분 동안 바보가 되지만, 질문하지 않는 사람은 평생 바보가 된다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Wisdom is not a product of schooling but of the lifelong attempt to acquire it.",
|
||||||
|
zh: "智慧不是学校教育的产物,而是终生努力获得的产物。",
|
||||||
|
zh_TW: "智慧不是學校教育的產物,而是終生努力獲得的產物。",
|
||||||
|
ja: "知恵とは学校教育の産物ではなく、生涯をかけて獲得しようと試みることで得られるものである。",
|
||||||
|
ko: "지혜는 학교 교육의 산물이 아니라, 평생에 걸쳐 그것을 얻으려는 노력의 산물이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The greatest enemy of knowledge is not ignorance, it is the illusion of knowledge.",
|
||||||
|
zh: "知识最大的敌人不是无知,而是自以为拥有知识的幻觉。",
|
||||||
|
zh_TW: "知識最大的敵人不是無知,而是自以為擁有知識的幻覺。",
|
||||||
|
ja: "知識の最大の敵は無知ではなく、知っているという幻想である。",
|
||||||
|
ko: "지식의 가장 큰 적은 무지가 아니라, 안다는 착각이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "True wisdom comes to each of us when we realize how little we understand about life, ourselves, and the world around us.",
|
||||||
|
zh: "当我们认识到自己对生命、对自身、对周围世界了解得多么少时,真正的智慧才会降临到我们每个人身上。",
|
||||||
|
zh_TW:
|
||||||
|
"當我們認識到自己對生命、對自身、對周圍世界了解得多麼少時,真正的智慧才會降臨到我們每個人身上。",
|
||||||
|
ja: "真の知恵は、我々が人生や自分自身、そして我々を取り巻く世界について、いかにわずかしか理解していないかを悟ったときに訪れる。",
|
||||||
|
ko: "진정한 지혜는 우리가 삶과 우리 자신, 그리고 우리를 둘러싼 세계에 대해 얼마나 아는 것이 없는지를 깨달을 때 찾아온다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Beware of false knowledge; it is more dangerous than ignorance.",
|
||||||
|
zh: "谨防虚假的知识;它比无知更危险。",
|
||||||
|
zh_TW: "謹防虛假的知識;它比無知更危險。",
|
||||||
|
ja: "偽りの知識に用心せよ。それは無知よりも危険である。",
|
||||||
|
ko: "거짓된 지식을 경계하라. 그것은 무지보다 더 위험하다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "What does not kill me makes me stronger.",
|
||||||
|
zh: "杀不死我的,使我更强大。",
|
||||||
|
zh_TW: "殺不死我的,使我更強大。",
|
||||||
|
ja: "私を殺さないものは、私をより強くする。",
|
||||||
|
ko: "나를 죽이지 못하는 것은 나를 더 강하게 만든다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The only constant in life is change.",
|
||||||
|
zh: "生活中唯一不变的就是变化。",
|
||||||
|
zh_TW: "生活中唯一不變的就是變化。",
|
||||||
|
ja: "人生で唯一変わらないものは、変化そのものである。",
|
||||||
|
ko: "삶에서 유일하게 변하지 않는 것은 변화뿐이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "If you are going through hell, keep going.",
|
||||||
|
zh: "如果你正在经历地狱,那就继续走下去。",
|
||||||
|
zh_TW: "如果你正在經歷地獄,那就繼續走下去。",
|
||||||
|
ja: "地獄を経験しているなら、進み続けろ。",
|
||||||
|
ko: "지옥을 겪고 있다면, 계속 나아가라。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "In the middle of difficulty lies opportunity.",
|
||||||
|
zh: "机会蕴藏在困难之中。",
|
||||||
|
zh_TW: "機會蘊藏在困難之中。",
|
||||||
|
ja: "困難の真っ只中に、好機がある。",
|
||||||
|
ko: "어려움의 한가운데에 기회가 있다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "It is not the strongest of the species that survive, nor the most intelligent, but the one most responsive to change.",
|
||||||
|
zh: "存活下来的物种不是最强壮的,也不是最聪明的,而是最能适应变化的。",
|
||||||
|
zh_TW: "存活下來的物種不是最强壯的,也不是最聰明的,而是最能適應變化的。",
|
||||||
|
ja: "生き残る種とは、最も強いものでも、最も知的なものでもない。最も変化に対応できるものである。",
|
||||||
|
ko: "살아남는 종은 가장 강한 종도, 가장 지능이 높은 종도 아니다. 변화에 가장 잘 적응하는 종이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "We must become the change we wish to see in the world.",
|
||||||
|
zh: "我们必须成为我们希望在世界上看到的改变。",
|
||||||
|
zh_TW: "我們必須成為我們希望在世界上看到的改變。",
|
||||||
|
ja: "世界に変化を望むなら、まず自らがその変化となれ。",
|
||||||
|
ko: "우리는 세상에서 보고 싶은 변화가 되어야 한다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "A smooth sea never made a skilled sailor.",
|
||||||
|
zh: "平静的大海练不出熟练的水手。",
|
||||||
|
zh_TW: "平靜的大海練不出熟練的水手。",
|
||||||
|
ja: "穏やかな海は、熟練した船乗りを育てない。",
|
||||||
|
ko: "순탄한 바다는 노련한 뱃사공을 만들지 못한다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Obstacles don't block the path, they are the path.",
|
||||||
|
zh: "障碍不是挡住了路,障碍本身就是路。",
|
||||||
|
zh_TW: "障礙不是擋住了路,障礙本身就是路。",
|
||||||
|
ja: "障害は道を塞ぐものではなく、道そのものである。",
|
||||||
|
ko: "장애물은 길을 막는 것이 아니라, 그 자체가 길이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Fall seven times, stand up eight.",
|
||||||
|
zh: "七次跌倒,八次站起。",
|
||||||
|
zh_TW: "七次跌倒,八次站起。",
|
||||||
|
ja: "七転び八起き。",
|
||||||
|
ko: "일곱 번 넘어져도, 여덟 번 일어선다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The art of life lies in a constant readjustment to our surroundings.",
|
||||||
|
zh: "生活的艺术在于不断地调整自己以适应环境。",
|
||||||
|
zh_TW: "生活的藝術在於不斷地調整自己以適應環境。",
|
||||||
|
ja: "人生(じんせい)の芸術(げいじゅつ)は、我々(われわれ)の環境(かんきょう)に対(たい)する絶(た)え間(ま)ない再調整(さいちょうせい)にある。",
|
||||||
|
ko: "삶의 기술은 우리를 둘러싼 환경에 끊임없이 재적응하는 데 있다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Adversity introduces a man to himself.",
|
||||||
|
zh: "逆境使人认识自己。",
|
||||||
|
zh_TW: "逆境使人認識自己。",
|
||||||
|
ja: "逆境は、人に自分自身を教えてくれる。",
|
||||||
|
ko: "역경은 사람에게 자기 자신을 소개한다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The wound is the place where the Light enters you.",
|
||||||
|
zh: "伤口是光进入你内心的入口。",
|
||||||
|
zh_TW: "傷口是光進入你內心的入口。",
|
||||||
|
ja: "傷口は、光があなたの中に入る場所だ。",
|
||||||
|
ko: "상처는 빛이 당신에게 들어오는 곳이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "When we are no longer able to change a situation, we are challenged to change ourselves.",
|
||||||
|
zh: "当我们无法改变现状时,我们就需要改变自己。",
|
||||||
|
zh_TW: "當我們無法改變現狀時,我們就需要改變自己。",
|
||||||
|
ja: "状況を変えることができなくなったとき、我々は自分自身を変えることを求められる。",
|
||||||
|
ko: "상황을 더 이상 바꿀 수 없을 때, 우리는 자신을 바꿔야 하는 도전에 직면한다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Be the change you wish to see in the world.",
|
||||||
|
zh: "成为你希望在世界上看到的改变。",
|
||||||
|
zh_TW: "成為你希望在世界上看到的改變。",
|
||||||
|
ja: "あなたが世界に見たいと願う変化に、あなた自身がなりなさい。",
|
||||||
|
ko: "세상에서 보고 싶은 변화가 있다면, 당신 자신이 그 변화가 되어라。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Do not pray for an easy life, pray for the strength to endure a difficult one.",
|
||||||
|
zh: "不要祈祷生活安逸,要祈祷有力量去忍受艰难的生活。",
|
||||||
|
zh_TW: "不要祈禱生活安逸,要祈禱有力量去忍受艱難的生活。",
|
||||||
|
ja: "楽な人生を祈るな。困難な人生を耐え抜く強さを祈れ。",
|
||||||
|
ko: "편안한 삶을 기도하지 말고, 어려운 삶을 견뎌낼 힘을 기도하라。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "A pessimist sees the difficulty in every opportunity; an optimist sees the opportunity in every difficulty.",
|
||||||
|
zh: "悲观者在每个机会中都看到困难;乐观者在每个困难中都看到机会。",
|
||||||
|
zh_TW: "悲觀者在每個機會中都看到困難;樂觀者在每個困難中都看到機會。",
|
||||||
|
ja: "悲観主義者はあらゆる好機の中に困難を見る。楽観主義者はあらゆる困難の中に好機を見る。",
|
||||||
|
ko: "비관론자는 모든 기회에서 어려움을 보고, 낙관론자는 모든 어려움에서 기회를 본다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "It's not what happens to you, but how you react to it that matters.",
|
||||||
|
zh: "重要的不是发生在你身上的事,而是你如何应对它。",
|
||||||
|
zh_TW: "重要的不是發生在你身上的事,而是你如何應對它。",
|
||||||
|
ja: "あなたに何が起こるかではなく、それにどう反応するかが重要だ。",
|
||||||
|
ko: "당신에게 무슨 일이 일어났는지가 중요한 것이 아니라, 당신이 그것에 어떻게 반응하는지가 중요하다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "To love oneself is the beginning of a lifelong romance.",
|
||||||
|
zh: "爱自己是终身浪漫的开始。",
|
||||||
|
zh_TW: "愛自己是終身浪漫的開始。",
|
||||||
|
ja: "自分自身を愛することは、一生続くロマンスの始まりだ。",
|
||||||
|
ko: "자신을 사랑하는 것은 평생 지속되는 로맨스의 시작이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Love is composed of a single soul inhabiting two bodies.",
|
||||||
|
zh: "爱是栖息于两个身体中的同一个灵魂。",
|
||||||
|
zh_TW: "愛是棲息於兩個身體中的同一個靈魂。",
|
||||||
|
ja: "愛とは、二つの体に宿る一つの魂で構成されている。",
|
||||||
|
ko: "사랑은 두 개의 몸에 깃든 하나의 영혼으로 이루어져 있다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Man is the measure of all things.",
|
||||||
|
zh: "人是万物的尺度。",
|
||||||
|
zh_TW: "人是萬物的尺度。",
|
||||||
|
ja: "人間は万物の尺度である。",
|
||||||
|
ko: "인간은 만물의 척도이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The best and most beautiful things in this world cannot be seen or even heard, but must be felt with the heart.",
|
||||||
|
zh: "世界上最好最美的东西是看不见也听不见的,必须用心去感受。",
|
||||||
|
zh_TW: "世界上最好最美的東西是看不見也聽不見的,必須用心去感受。",
|
||||||
|
ja: "この世で最も素晴らしく、最も美しいものは、目で見たり聞いたりすることはできない。心で感じなければならない。",
|
||||||
|
ko: "이 세상에서 가장 좋고 가장 아름다운 것들은 보이거나 들리지 않는다. 오직 마음으로만 느껴야 한다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Where there is love there is life.",
|
||||||
|
zh: "有爱的地方就有生命。",
|
||||||
|
zh_TW: "有愛的地方就有生命。",
|
||||||
|
ja: "愛があるところに人生がある。",
|
||||||
|
ko: "사랑이 있는 곳에 삶이 있다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "If you want to be loved, be lovable.",
|
||||||
|
zh: "如果你想被爱,就要变得可爱。",
|
||||||
|
zh_TW: "如果你想被愛,就要變得可愛。",
|
||||||
|
ja: "愛されたいなら、愛らしくあれ。",
|
||||||
|
ko: "사랑받고 싶다면, 사랑스러워져라。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "We are all in the gutter, but some of us are looking at the stars.",
|
||||||
|
zh: "我们都身处沟渠,但仍有人仰望星空。",
|
||||||
|
zh_TW: "我們都身處溝渠,但仍有人仰望星空。",
|
||||||
|
ja: "我々はみな溝の中にいる。だが、そこから星を見上げている者もいるのだ。",
|
||||||
|
ko: "우리는 모두 시궁창에 있지만, 우리 중 일부는 별을 바라보고 있다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The only thing we have to fear is fear itself.",
|
||||||
|
zh: "我们唯一需要恐惧的就是恐惧本身。",
|
||||||
|
zh_TW: "我們唯一需要恐懼的就是恐懼本身。",
|
||||||
|
ja: "我々が恐れるべき唯一のものは、恐れそのものである。",
|
||||||
|
ko: "우리가 두려워해야 할 유일한 것은 두려움 그 자체이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Be kind, for everyone you meet is fighting a hard battle.",
|
||||||
|
zh: "要友善,因为你遇到的每个人都在打一场艰苦的战斗。",
|
||||||
|
zh_TW: "要友善,因為你遇到的每個人都在打一場艱苦的戰鬥。",
|
||||||
|
ja: "親切にしなさい。あなたが出会う誰もが、困難な戦いを戦っているのだから。",
|
||||||
|
ko: "친절하라. 당신이 만나는 모든 사람은 힘겨운 싸움을 하고 있기 때문이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Man is born free, and everywhere he is in chains.",
|
||||||
|
zh: "人生而自由,却无往不在枷锁之中。",
|
||||||
|
zh_TW: "人生而自由,卻無往不在枷鎖之中。",
|
||||||
|
ja: "人は生まれながらにして自由だが、いたるところで鎖につながれている。",
|
||||||
|
ko: "인간은 자유롭게 태어났으나, 어디에서나 쇠사슬에 묶여 있다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "We love the things we love for what they are.",
|
||||||
|
zh: "我们爱我们所爱之物,只因它们本来的样子。",
|
||||||
|
zh_TW: "我們愛我們所愛之物,只因它們本來的樣子。",
|
||||||
|
ja: "我々が愛するものを愛するのは、それがそれであるからだ。",
|
||||||
|
ko: "우리는 우리가 사랑하는 것들을 그 자체로 사랑한다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Darkness cannot drive out darkness; only light can do that. Hate cannot drive out hate; only love can do that.",
|
||||||
|
zh: "黑暗无法驱逐黑暗,只有光明可以;仇恨无法驱逐仇恨,只有爱可以。",
|
||||||
|
zh_TW: "黑暗無法驅逐黑暗,只有光明可以;仇恨無法驅逐仇恨,只有愛可以。",
|
||||||
|
ja: "闇は闇を追い払うことはできない。光だけがそれを可能にする。憎しみは憎しみを追い払うことはできない。愛だけがそれを可能にする。",
|
||||||
|
ko: "어둠은 어둠을 몰아낼 수 없다. 오직 빛만이 할 수 있다. 증오는 증오를 몰아낼 수 없다. 오직 사랑만이 할 수 있다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "An eye for an eye only ends up making the whole world blind.",
|
||||||
|
zh: "以眼还眼,只会让整个世界都盲目。",
|
||||||
|
zh_TW: "以眼還眼,只會讓整個世界都盲目。",
|
||||||
|
ja: "「目には目を」は、全世界を盲目にするだけだ。",
|
||||||
|
ko: "'눈에는 눈'은 결국 온 세상을 눈멀게 할 뿐이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Hell is other people.",
|
||||||
|
zh: "他人即地狱。",
|
||||||
|
zh_TW: "他人即地獄。",
|
||||||
|
ja: "地獄とは、他人である。",
|
||||||
|
ko: "타인은 지옥이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "You will not be punished for your anger, you will be punished by your anger.",
|
||||||
|
zh: "你不会因为你的愤怒而受到惩罚,你会被你的愤怒所惩罚。",
|
||||||
|
zh_TW: "你不會因為你的憤怒而受到懲罰,你會被你的憤怒所懲罰。",
|
||||||
|
ja: "あなたは怒りのために罰せられるのではない。怒りによって罰せられるのだ。",
|
||||||
|
ko: "당신은 당신의 분노 때문에 벌을 받는 것이 아니라, 당신의 분노에 의해 벌을 받을 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "To err is human, to forgive divine.",
|
||||||
|
zh: "犯错是人性,宽恕是神性。",
|
||||||
|
zh_TW: "犯錯是人性,寬恕是神性。",
|
||||||
|
ja: "過つは人の常、許すは神の業。",
|
||||||
|
ko: "실수하는 것은 인간이고, 용서하는 것은 신이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Man is the only creature who refuses to be what he is.",
|
||||||
|
zh: "人是唯一拒绝承认自己本质的生物。",
|
||||||
|
zh_TW: "人是唯一拒絕承認自己本質的生物。",
|
||||||
|
ja: "人間は、自分が何者であるかを拒否する唯一の生き物である。",
|
||||||
|
ko: "인간은 자신이 무엇인지를 거부하는 유일한 생물이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Beauty is in the eye of the beholder.",
|
||||||
|
zh: "情人眼里出西施。",
|
||||||
|
zh_TW: "情人眼裡出西施。",
|
||||||
|
ja: "美は見る人の目の中にある。",
|
||||||
|
ko: "아름다움은 보는 사람의 눈에 달려 있다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "All that we see or seem is but a dream within a dream.",
|
||||||
|
zh: "我们所见所感,皆如梦中之梦。",
|
||||||
|
zh_TW: "我們所見所感,皆如夢中之夢。",
|
||||||
|
ja: "我々が見たり感じたりするすべては、夢の中の夢にすぎない。",
|
||||||
|
ko: "우리가 보거나 보이는 모든 것은 꿈속의 꿈일 뿐이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Everything you can imagine is real.",
|
||||||
|
zh: "你能想象的一切都是真实的。",
|
||||||
|
zh_TW: "你能想像的一切都是真實的。",
|
||||||
|
ja: "想像できることは、すべて現実なのだ。",
|
||||||
|
ko: "당신이 상상할 수 있는 모든 것은 현실이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The map is not the territory.",
|
||||||
|
zh: "地图并非领土。",
|
||||||
|
zh_TW: "地圖並非領土。",
|
||||||
|
ja: "地図は領土ではない。",
|
||||||
|
ko: "지도는 영토가 아니다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "We don't see things as they are, we see them as we are.",
|
||||||
|
zh: "我们看到的不是事物的原貌,而是我们自己的样子。",
|
||||||
|
zh_TW: "我們看到的不是事物的原貌,而是我們自己的樣子。",
|
||||||
|
ja: "我々は物事をあるがままに見ているのではない。我々があるがままに見ているのだ。",
|
||||||
|
ko: "우리는 사물을 있는 그대로 보지 않고, 우리 자신(의 모습)대로 본다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "There are two ways to be fooled. One is to believe what isn't true; the other is to refuse to believe what is true.",
|
||||||
|
zh: "被愚弄有两种方式。一种是相信不真实的东西;另一种是拒绝相信真实的东西。",
|
||||||
|
zh_TW:
|
||||||
|
"被愚弄有兩種方式。一種是相信不真實的東西;另一種是拒絕相信真實的東西。",
|
||||||
|
ja: "騙される方法は二つある。一つは真実でないことを信じること。もう一つは真実であることを信じようとしないことだ。",
|
||||||
|
ko: "속는 방법에는 두 가지가 있다. 하나는 사실이 아닌 것을 믿는 것이고, 다른 하나는 사실인 것을 믿기를 거부하는 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Simplicity is the ultimate sophistication.",
|
||||||
|
zh: "简约是极致的复杂。",
|
||||||
|
zh_TW: "簡約是極致的複雜。",
|
||||||
|
ja: "シンプルさは、究極の洗練である。",
|
||||||
|
ko: "단순함은 궁극의 정교함이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The truth will set you free.",
|
||||||
|
zh: "真相将使你自由。",
|
||||||
|
zh_TW: "真相將使你自由。",
|
||||||
|
ja: "真実は、あなたを自由にする。",
|
||||||
|
ko: "진리가 너희를 자유롭게 하리라。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Reality is merely an illusion, albeit a very persistent one.",
|
||||||
|
zh: "现实只是一种幻觉,尽管是一种非常持久的幻觉。",
|
||||||
|
zh_TW: "現實只是一種幻覺,儘管是一種非常持久的幻覺。",
|
||||||
|
ja: "現実とは、非常に根強いただの幻想にすぎない。",
|
||||||
|
ko: "현실은 단지 환상일 뿐이다. 비록 매우 집요한 환상이긴 하지만。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "What is rational is actual and what is actual is rational.",
|
||||||
|
zh: "凡是合乎理性的东西都是现实的,凡是现实的东西都是合乎理性的。",
|
||||||
|
zh_TW: "凡是合乎理性的東西都是現實的,凡是現實的東西都是合乎理性的。",
|
||||||
|
ja: "理性的なものは現実的であり、現実的なものは理性的である。",
|
||||||
|
ko: "이성적인 것은 현실적이고, 현실적인 것은 이성적이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Truth is like the sun. You can shut it out for a time, but it ain't goin' away.",
|
||||||
|
zh: "真相就像太阳。你可以暂时将它遮住,但它不会消失。",
|
||||||
|
zh_TW: "真相就像太陽。你可以暫時將它遮住,但它不會消失。",
|
||||||
|
ja: "真実は太陽のようなものだ。一時的に隠すことはできても、決してなくなりはしない。",
|
||||||
|
ko: "진실은 태양과 같다. 잠시 가릴 수는 있지만, 사라지게 할 수는 없다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Everything we hear is an opinion, not a fact. Everything we see is a perspective, not the truth.",
|
||||||
|
zh: "我们听到的一切都只是观点,而非事实。我们看到的一切都只是视角,而非真相。",
|
||||||
|
zh_TW:
|
||||||
|
"我們聽到的一切都只是觀點,而非事實。我們看到的一切都只是視角,而非真相。",
|
||||||
|
ja: "我々が聞くことすべてが意見であり、事実ではない。我々が見ることすべてが視点であり、真実ではない。",
|
||||||
|
ko: "우리가 듣는 모든 것은 의견이지, 사실이 아니다. 우리가 보는 모든 것은 관점이지, 진실이 아니다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "There is no truth. There is only perception.",
|
||||||
|
zh: "没有真相,只有认知。",
|
||||||
|
zh_TW: "沒有真相,只有認知。",
|
||||||
|
ja: "真実などない。ただ認識があるだけだ。",
|
||||||
|
ko: "진실은 없다. 오직 인식만이 있을 뿐이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "If you look deep enough into anything, you will find mathematics.",
|
||||||
|
zh: "如果你对任何事物看得足够深入,你都会发现数学。",
|
||||||
|
zh_TW: "如果你對任何事物看得足夠深入,你都會發現數學。",
|
||||||
|
ja: "何事も深く見つめれば、そこには数学がある。",
|
||||||
|
ko: "무엇이든 충분히 깊이 들여다보면, 수학을 발견하게 될 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The medium is the message.",
|
||||||
|
zh: "媒介即信息。",
|
||||||
|
zh_TW: "媒介即訊息。",
|
||||||
|
ja: "メディアはメッセージである。",
|
||||||
|
ko: "미디어는 메시지다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Nothing is true, everything is permitted.",
|
||||||
|
zh: "没有什么是真实的,一切都被允许。",
|
||||||
|
zh_TW: "沒有什麼是真實的,一切都被允許。",
|
||||||
|
ja: "真実などない、すべては許されている。",
|
||||||
|
ko: "진실은 없으며, 모든 것이 허용된다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "We are what we believe we are.",
|
||||||
|
zh: "我们相信自己是什么,我们就是什么。",
|
||||||
|
zh_TW: "我們相信自己是什麼,我們就是什麼。",
|
||||||
|
ja: "我々は、我々が信じる通りの人間である。",
|
||||||
|
ko: "우리는 우리가 그렇다고 믿는 존재이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Yesterday is history, tomorrow is a mystery, but today is a gift. That is why it is called the present.",
|
||||||
|
zh: "昨天是历史,明天是谜团,但今天是礼物。这就是为什么它被称为‘现在’(Present)。",
|
||||||
|
zh_TW:
|
||||||
|
"昨天是歷史,明天是謎團,但今天是禮物。這就是為什麼它被稱為‘現在’(Present)。",
|
||||||
|
ja: "昨日は歴史、明日はミステリー、しかし今日は贈り物だ。だからこそ、それは『プレゼント (現在)』と呼ばれる。",
|
||||||
|
ko: "어제는 역사이고, 내일은 미스터리이며, 오늘은 선물이다. 그래서 오늘을 '선물(present)'이라고 부른다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Time is money.",
|
||||||
|
zh: "时间就是金钱。",
|
||||||
|
zh_TW: "時間就是金錢。",
|
||||||
|
ja: "時は金なり。",
|
||||||
|
ko: "시간은 돈이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The only thing necessary for the triumph of evil is for good men to do nothing.",
|
||||||
|
zh: "邪恶得逞的唯一条件是好人袖手旁观。",
|
||||||
|
zh_TW: "邪惡得逞的唯一條件是好人袖手旁觀。",
|
||||||
|
ja: "悪が勝利するために必要なのは、善人が何もしないことだけである。",
|
||||||
|
ko: "악의 승리를 위해 필요한 유일한 것은 선한 사람들이 아무것도 하지 않는 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Carpe diem.",
|
||||||
|
zh: "活在当下。",
|
||||||
|
zh_TW: "活在當下。",
|
||||||
|
ja: "今を生きよ(カルペ・ディエム)。",
|
||||||
|
ko: "현재를 즐겨라 (카르페 디엠)。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Do not dwell in the past, do not dream of the future, concentrate the mind on the present moment.",
|
||||||
|
zh: "不要沉湎于过去,不要幻想未来,集中精神活在当下。",
|
||||||
|
zh_TW: "不要沉湎於過去,不要幻想未來,集中精神活在當下。",
|
||||||
|
ja: "過去に生きるな、未来を夢見るな、現在の瞬間に心を集中させよ。",
|
||||||
|
ko: "과거에 머물지 말고, 미래를 꿈꾸지 말며, 현재 이 순간에 마음을 집중하라。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The best time to plant a tree was 20 years ago. The second best time is now.",
|
||||||
|
zh: "种树的最佳时机是20年前。其次是现在。",
|
||||||
|
zh_TW: "種樹的最佳時機是20年前。其次是現在。",
|
||||||
|
ja: "木を植えるのに最適な時期は20年前だった。二番目に最適な時期は、今だ。",
|
||||||
|
ko: "나무를 심기에 가장 좋은 때는 20년 전이었다. 두 번째로 좋은 때는 바로 지금이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Action speaks louder than words.",
|
||||||
|
zh: "事实胜于雄辩。",
|
||||||
|
zh_TW: "事實勝於雄辯。",
|
||||||
|
ja: "行動は言葉よりも雄弁である。",
|
||||||
|
ko: "말보다 행동이 더 중요하다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Honesty is the first chapter in the book of wisdom.",
|
||||||
|
zh: "诚实是智慧之书的第一章。",
|
||||||
|
zh_TW: "誠實是智慧之書的第一章。",
|
||||||
|
ja: "誠実さは、知恵という本の第一章である。",
|
||||||
|
ko: "정직은 지혜라는 책의 첫 장이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Two things are infinite: the universe and human stupidity; and I'm not sure about the universe.",
|
||||||
|
zh: "有两样东西是无限的:宇宙和人类的愚蠢;而且我不太确定宇宙是否无限。",
|
||||||
|
zh_TW: "有兩樣東西是無限的:宇宙和人類的愚蠢;而且我不太確定宇宙是否無限。",
|
||||||
|
ja: "無限なものは二つある。宇宙と人間の愚かさだ。ただ、宇宙については私にもよく分からない。",
|
||||||
|
ko: "무한한 것은 두 가지뿐이다. 우주와 인간의 어리석음. 그런데 우주에 대해선 나도 확신이 없다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "You cannot step twice into the same river.",
|
||||||
|
zh: "人不能两次踏进同一条河流。",
|
||||||
|
zh_TW: "人不能兩次踏進同一條河流。",
|
||||||
|
ja: "同(おな)じ川(かわ)に二度(にど)入(はい)ることはできない。",
|
||||||
|
ko: "같은 강물에 두 번 발을 담글 수 없다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "The future belongs to those who believe in the beauty of their dreams.",
|
||||||
|
zh: "未来属于那些相信梦想之美的人。",
|
||||||
|
zh_TW: "未來屬於那些相信夢想之美的人。",
|
||||||
|
ja: "未来は、自分の夢の美しさを信じる者のものである。",
|
||||||
|
ko: "미래는 자신의 꿈의 아름다움을 믿는 사람들의 것이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "Procrastination is the thief of time.",
|
||||||
|
zh: "拖延是时间的大敌。",
|
||||||
|
zh_TW: "拖延是時間的大敵。",
|
||||||
|
ja: "先延ばしは時間泥棒である。",
|
||||||
|
ko: "미루는 습관은 시간 도둑이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "An investment in knowledge pays the best interest.",
|
||||||
|
zh: "投资知识,收益最佳。",
|
||||||
|
zh_TW: "投資知識,收益最佳。",
|
||||||
|
ja: "知識への投資は、最良の利息を生む。",
|
||||||
|
ko: "지식에 대한 투자는 최고의 이자를 지불한다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "I have not failed. I've just found 10,000 ways that won't work.",
|
||||||
|
zh: "我没有失败。我只是找到了一万种行不通的方法。",
|
||||||
|
zh_TW: "我沒有失敗。我只是找到了一萬種行不通的方法。",
|
||||||
|
ja: "私は失敗したことがない。ただ、うまくいかない1万通りの方法を見つけただけだ。",
|
||||||
|
ko: "나는 실패하지 않았다. 단지 작동하지 않는 1만 가지 방법을 찾았을 뿐이다。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
en: "That which is done, is done.",
|
||||||
|
zh: "木已成舟。",
|
||||||
|
zh_TW: "木已成舟。",
|
||||||
|
ja: "なされたことは、なされたことだ。(覆水盆に返らず)",
|
||||||
|
ko: "일어난 일은 일어난 일이다. (이미 엎질러진 물이다.)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getRandomQuote() {
|
||||||
|
const randomIndex = Math.floor(Math.random() * quotes.length);
|
||||||
|
return quotes[randomIndex];
|
||||||
|
}
|
||||||
@@ -1,186 +1,185 @@
|
|||||||
const els = `li, p, h1, h2, h3, h4, h5, h6, dd, blockquote`;
|
import { OPT_TRANS_MICROSOFT } from "./api";
|
||||||
|
import { OPT_STYLE_NONE } from "./styles";
|
||||||
export const DEFAULT_SELECTOR = `:is(${els})`;
|
|
||||||
|
|
||||||
export const GLOBAL_KEY = "*";
|
export const GLOBAL_KEY = "*";
|
||||||
export const REMAIN_KEY = "-";
|
export const REMAIN_KEY = "-";
|
||||||
|
|
||||||
export const SHADOW_KEY = ">>>";
|
export const SHADOW_KEY = ">>>";
|
||||||
|
|
||||||
export const DEFAULT_RULE = {
|
export const DEFAULT_COLOR = "#209CEE"; // 默认高亮背景色/线条颜色
|
||||||
pattern: "",
|
|
||||||
selector: "",
|
|
||||||
translator: GLOBAL_KEY,
|
|
||||||
fromLang: GLOBAL_KEY,
|
|
||||||
toLang: GLOBAL_KEY,
|
|
||||||
textStyle: GLOBAL_KEY,
|
|
||||||
transOpen: GLOBAL_KEY,
|
|
||||||
bgColor: "",
|
|
||||||
textDiyStyle: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_DIY_STYLE = `color: #666;
|
export const DEFAULT_TRANS_TAG = "font";
|
||||||
background: linear-gradient(
|
export const DEFAULT_SELECT_STYLE =
|
||||||
45deg,
|
"-webkit-line-clamp: unset; max-height: none; height: auto;";
|
||||||
LightGreen 20%,
|
|
||||||
LightPink 20% 40%,
|
|
||||||
LightSalmon 40% 60%,
|
|
||||||
LightSeaGreen 60% 80%,
|
|
||||||
LightSkyBlue 80%
|
|
||||||
);
|
|
||||||
&:hover {
|
|
||||||
color: #333;
|
|
||||||
};`;
|
|
||||||
|
|
||||||
export const DEFAULT_OW_RULE = {
|
export const OPT_TIMING_PAGESCROLL = "mk_pagescroll"; // 滚动加载翻译
|
||||||
translator: REMAIN_KEY,
|
export const OPT_TIMING_PAGEOPEN = "mk_pageopen"; // 直接翻译到底
|
||||||
fromLang: REMAIN_KEY,
|
export const OPT_TIMING_MOUSEOVER = "mk_mouseover";
|
||||||
toLang: REMAIN_KEY,
|
export const OPT_TIMING_CONTROL = "mk_ctrlKey";
|
||||||
textStyle: REMAIN_KEY,
|
export const OPT_TIMING_SHIFT = "mk_shiftKey";
|
||||||
transOpen: REMAIN_KEY,
|
export const OPT_TIMING_ALT = "mk_altKey";
|
||||||
bgColor: "",
|
export const OPT_TIMING_ALL = [
|
||||||
textDiyStyle: DEFAULT_DIY_STYLE,
|
OPT_TIMING_PAGESCROLL,
|
||||||
};
|
OPT_TIMING_PAGEOPEN,
|
||||||
|
OPT_TIMING_MOUSEOVER,
|
||||||
const RULES = [
|
OPT_TIMING_CONTROL,
|
||||||
{
|
OPT_TIMING_SHIFT,
|
||||||
pattern: `www.google.com/search`,
|
OPT_TIMING_ALT,
|
||||||
selector: `h3, .IsZvec, .VwiC3b`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `news.google.com`,
|
|
||||||
selector: `h4`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.foxnews.com`,
|
|
||||||
selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p, [data-spot-im-class="message-text"]`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php`,
|
|
||||||
selector: DEFAULT_SELECTOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `themessenger.com`,
|
|
||||||
selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.telegraph.co.uk`,
|
|
||||||
selector: `article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.theguardian.com`,
|
|
||||||
selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.semafor.com`,
|
|
||||||
selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.noemamag.com`,
|
|
||||||
selector: `.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `restofworld.org`,
|
|
||||||
selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.axios.com`,
|
|
||||||
selector: `.h7, ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.newyorker.com`,
|
|
||||||
selector: `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `https://time.com/`,
|
|
||||||
selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.dw.com`,
|
|
||||||
selector: `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.bbc.com`,
|
|
||||||
selector: `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-1mrs5ns-PromoLink, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.chinadaily.com.cn`,
|
|
||||||
selector: `h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.facebook.com`,
|
|
||||||
selector: `[role="main"] [dir="auto"]`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.reddit.com`,
|
|
||||||
selector: `[slot="title"], [slot="text-body"] ${DEFAULT_SELECTOR}, #-post-rtjson-content p`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.quora.com`,
|
|
||||||
selector: `.qu-wordBreak--break-word`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `edition.cnn.com`,
|
|
||||||
selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.reuters.com`,
|
|
||||||
selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.bloomberg.com`,
|
|
||||||
selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `deno.land, docs.github.com`,
|
|
||||||
selector: `main ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `doc.rust-lang.org`,
|
|
||||||
selector: `#content ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.indiehackers.com`,
|
|
||||||
selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `platform.openai.com/docs`,
|
|
||||||
selector: `.docs-body ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `en.wikipedia.org`,
|
|
||||||
selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `stackoverflow.com`,
|
|
||||||
selector: `h1, .s-prose p, .comment-body .comment-copy`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `www.npmjs.com/package/, developer.chrome.com/docs, medium.com, developers.cloudflare.com, react.dev, create-react-app.dev, pytorch.org/`,
|
|
||||||
selector: `article ${DEFAULT_SELECTOR}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `news.ycombinator.com`,
|
|
||||||
selector: `.title, .commtext`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `https://github.com/`,
|
|
||||||
selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop='description'], .markdown-title, bdi`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `twitter.com`,
|
|
||||||
selector: `[data-testid='tweetText']`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
pattern: `youtube.com`,
|
|
||||||
selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span`,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const BUILTIN_RULES = RULES.sort((a, b) =>
|
export const OPT_SPLIT_PARAGRAPH_DISABLE = "split_disable";
|
||||||
a.pattern.localeCompare(b.pattern)
|
export const OPT_SPLIT_PARAGRAPH_TEXTLENGTH = "split_textlength";
|
||||||
).map((item) => ({
|
export const OPT_SPLIT_PARAGRAPH_PUNCTUATION = "split_punctuation";
|
||||||
...DEFAULT_RULE,
|
export const OPT_SPLIT_PARAGRAPH_ALL = [
|
||||||
...item,
|
OPT_SPLIT_PARAGRAPH_DISABLE,
|
||||||
transOpen: "true",
|
OPT_SPLIT_PARAGRAPH_PUNCTUATION,
|
||||||
}));
|
OPT_SPLIT_PARAGRAPH_TEXTLENGTH,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OPT_HIGHLIGHT_WORDS_DISABLE = "highlight_disable";
|
||||||
|
export const OPT_HIGHLIGHT_WORDS_BEFORETRANS = "highlight_beforetrans";
|
||||||
|
export const OPT_HIGHLIGHT_WORDS_AFTERTRANS = "highlight_aftertrans";
|
||||||
|
export const OPT_HIGHLIGHT_WORDS_ALL = [
|
||||||
|
OPT_HIGHLIGHT_WORDS_DISABLE,
|
||||||
|
OPT_HIGHLIGHT_WORDS_BEFORETRANS,
|
||||||
|
OPT_HIGHLIGHT_WORDS_AFTERTRANS,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_SELECTOR =
|
||||||
|
"h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
|
||||||
|
export const DEFAULT_IGNORE_SELECTOR = "button, footer, pre, mark, nav";
|
||||||
|
export const DEFAULT_KEEP_SELECTOR = `code, cite, math, .math, a:has(code)`;
|
||||||
|
export const DEFAULT_RULE = {
|
||||||
|
pattern: "", // 匹配网址
|
||||||
|
selector: "", // 选择器
|
||||||
|
keepSelector: "", // 保留元素选择器
|
||||||
|
terms: "", // 专业术语
|
||||||
|
aiTerms: "", // AI专业术语
|
||||||
|
apiSlug: GLOBAL_KEY, // 翻译服务
|
||||||
|
fromLang: GLOBAL_KEY, // 源语言
|
||||||
|
toLang: GLOBAL_KEY, // 目标语言
|
||||||
|
textStyle: GLOBAL_KEY, // 译文样式
|
||||||
|
transOpen: GLOBAL_KEY, // 开启翻译
|
||||||
|
// bgColor: "", // 译文颜色 (作废)
|
||||||
|
// textDiyStyle: "", // 自定义译文样式 (作废)
|
||||||
|
textExtStyle: "", // 译文附加样式
|
||||||
|
termsStyle: "", // 专业术语样式
|
||||||
|
highlightStyle: "", // 高亮词汇样式
|
||||||
|
selectStyle: "", // 选择器节点样式
|
||||||
|
parentStyle: "", // 选择器父节点样式
|
||||||
|
grandStyle: "", // 选择器父节点样式
|
||||||
|
injectJs: "", // 注入JS
|
||||||
|
// injectCss: "", // 注入CSS (作废)
|
||||||
|
transOnly: GLOBAL_KEY, // 是否仅显示译文
|
||||||
|
// transTiming: GLOBAL_KEY, // 翻译时机/鼠标悬停翻译 (暂时作废)
|
||||||
|
transTag: GLOBAL_KEY, // 译文元素标签
|
||||||
|
transTitle: GLOBAL_KEY, // 是否同时翻译页面标题
|
||||||
|
// transSelected: GLOBAL_KEY, // 是否启用划词翻译 (移回setting)
|
||||||
|
// detectRemote: GLOBAL_KEY, // 是否使用远程语言检测 (移回setting)
|
||||||
|
// skipLangs: [], // 不翻译的语言 (移回setting)
|
||||||
|
// fixerSelector: "", // 修复函数选择器 (暂时作废)
|
||||||
|
// fixerFunc: GLOBAL_KEY, // 修复函数 (暂时作废)
|
||||||
|
transStartHook: "", // 钩子函数
|
||||||
|
transEndHook: "", // 钩子函数
|
||||||
|
// transRemoveHook: "", // 钩子函数 (暂时作废)
|
||||||
|
autoScan: GLOBAL_KEY, // 是否自动识别文本节点
|
||||||
|
hasRichText: GLOBAL_KEY, // 是否启用富文本翻译
|
||||||
|
hasShadowroot: GLOBAL_KEY, // 是否包含shadowroot
|
||||||
|
rootsSelector: "", // 翻译范围选择器
|
||||||
|
ignoreSelector: "", // 不翻译的选择器
|
||||||
|
splitParagraph: GLOBAL_KEY, // 切分段落
|
||||||
|
splitLength: 0, // 切分段落长度
|
||||||
|
highlightWords: GLOBAL_KEY, // 高亮词汇
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全局规则
|
||||||
|
export const GLOBLA_RULE = {
|
||||||
|
pattern: "*", // 匹配网址
|
||||||
|
selector: DEFAULT_SELECTOR, // 选择器
|
||||||
|
keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
|
||||||
|
terms: "", // 专业术语
|
||||||
|
aiTerms: "", // AI专业术语
|
||||||
|
apiSlug: OPT_TRANS_MICROSOFT, // 翻译服务
|
||||||
|
fromLang: "auto", // 源语言
|
||||||
|
toLang: "zh-CN", // 目标语言
|
||||||
|
textStyle: OPT_STYLE_NONE, // 译文样式
|
||||||
|
transOpen: "false", // 开启翻译
|
||||||
|
// bgColor: DEFAULT_COLOR, // 译文颜色 (作废)
|
||||||
|
// textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式 (作废)
|
||||||
|
textExtStyle: "", // 译文附加样式
|
||||||
|
termsStyle: "font-weight: bold;", // 专业术语样式
|
||||||
|
highlightStyle: "color: red;", // 高亮词汇样式
|
||||||
|
selectStyle: DEFAULT_SELECT_STYLE, // 选择器节点样式
|
||||||
|
parentStyle: "", // 选择器父节点样式
|
||||||
|
grandStyle: "", // 选择器祖节点样式
|
||||||
|
injectJs: "", // 注入JS
|
||||||
|
injectCss: "", // 注入CSS
|
||||||
|
transOnly: "false", // 是否仅显示译文
|
||||||
|
// transTiming: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译 (暂时作废)
|
||||||
|
transTag: DEFAULT_TRANS_TAG, // 译文元素标签
|
||||||
|
transTitle: "false", // 是否同时翻译页面标题
|
||||||
|
// transSelected: "true", // 是否启用划词翻译 (移回setting)
|
||||||
|
// detectRemote: "true", // 是否使用远程语言检测 (移回setting)
|
||||||
|
// skipLangs: [], // 不翻译的语言 (移回setting)
|
||||||
|
// fixerSelector: "", // 修复函数选择器 (暂时作废)
|
||||||
|
// fixerFunc: "-", // 修复函数 (暂时作废)
|
||||||
|
transStartHook: "", // 钩子函数
|
||||||
|
transEndHook: "", // 钩子函数
|
||||||
|
// transRemoveHook: "", // 钩子函数 (暂时作废)
|
||||||
|
autoScan: "true", // 是否自动识别文本节点
|
||||||
|
hasRichText: "true", // 是否启用富文本翻译
|
||||||
|
hasShadowroot: "false", // 是否包含shadowroot
|
||||||
|
rootsSelector: "body", // 翻译范围选择器
|
||||||
|
ignoreSelector: DEFAULT_IGNORE_SELECTOR, // 不翻译的选择器
|
||||||
|
splitParagraph: OPT_SPLIT_PARAGRAPH_DISABLE, // 切分段落
|
||||||
|
splitLength: 100, // 切分段落长度
|
||||||
|
highlightWords: OPT_HIGHLIGHT_WORDS_DISABLE, // 高亮词汇
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_RULES = [GLOBLA_RULE];
|
||||||
|
|
||||||
|
// todo: 校验几个内置规则
|
||||||
|
const RULES_MAP = {
|
||||||
|
// "www.google.com/search": {
|
||||||
|
// rootsSelector: `#rcnt`,
|
||||||
|
// },
|
||||||
|
"en.wikipedia.org": {
|
||||||
|
ignoreSelector: `.button, code, footer, form, mark, pre, .mwe-math-element, .mw-editsection`,
|
||||||
|
},
|
||||||
|
"news.ycombinator.com": {
|
||||||
|
selector: `p, .titleline, .commtext, .hn-item-title, .hn-comment-text, .hn-story-title`,
|
||||||
|
keepSelector: `code, img, svg, pre, .sitebit`,
|
||||||
|
ignoreSelector: `button, code, footer, form, header, mark, nav, pre, .reply`,
|
||||||
|
autoScan: `false`,
|
||||||
|
},
|
||||||
|
"twitter.com, https://x.com": {
|
||||||
|
selector: `[data-testid='tweetText'], [data-testid='twitter-article-title'], .public-DraftStyleDefault-block`,
|
||||||
|
keepSelector: `img, svg, a, span:has(a), div:has(a)`,
|
||||||
|
ignoreSelector: `button, [data-testid='videoPlayer'], [role='group']`,
|
||||||
|
autoScan: `false`,
|
||||||
|
},
|
||||||
|
"www.youtube.com/live_chat": {
|
||||||
|
rootsSelector: `div#items`,
|
||||||
|
selector: `span.yt-live-chat-text-message-renderer`,
|
||||||
|
autoScan: `false`,
|
||||||
|
},
|
||||||
|
"www.youtube.com": {
|
||||||
|
rootsSelector: `ytd-page-manager`,
|
||||||
|
ignoreSelector: `aside, button, footer, form, header, pre, mark, nav, #player, #container, .caption-window, .ytp-settings-menu`,
|
||||||
|
selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||||
|
parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||||
|
grandStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
|
||||||
|
},
|
||||||
|
"web.telegram.org": {
|
||||||
|
autoScan: `false`,
|
||||||
|
selector: ".text-content, .embedded-text-wrapper",
|
||||||
|
rootsSelector: ".Transition",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BUILTIN_RULES = Object.entries(RULES_MAP).map(
|
||||||
|
([pattern, rule]) => ({
|
||||||
|
// ...DEFAULT_RULE,
|
||||||
|
...rule,
|
||||||
|
pattern,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
190
src/config/setting.js
Normal file
190
src/config/setting.js
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { LogLevel } from "../libs/log";
|
||||||
|
import {
|
||||||
|
OPT_DICT_BING,
|
||||||
|
OPT_SUG_YOUDAO,
|
||||||
|
DEFAULT_HTTP_TIMEOUT,
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
|
DEFAULT_API_LIST,
|
||||||
|
} from "./api";
|
||||||
|
import { DEFAULT_CUSTOM_STYLES } from "./styles";
|
||||||
|
|
||||||
|
// 默认快捷键
|
||||||
|
export const OPT_SHORTCUT_TRANSLATE = "toggleTranslate";
|
||||||
|
export const OPT_SHORTCUT_STYLE = "toggleStyle";
|
||||||
|
export const OPT_SHORTCUT_POPUP = "togglePopup";
|
||||||
|
export const OPT_SHORTCUT_SETTING = "openSetting";
|
||||||
|
export const DEFAULT_SHORTCUTS = {
|
||||||
|
[OPT_SHORTCUT_TRANSLATE]: ["AltLeft", "KeyQ"],
|
||||||
|
[OPT_SHORTCUT_STYLE]: ["AltLeft", "KeyC"],
|
||||||
|
[OPT_SHORTCUT_POPUP]: ["AltLeft", "KeyK"],
|
||||||
|
[OPT_SHORTCUT_SETTING]: ["AltLeft", "KeyO"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TRANS_MIN_LENGTH = 2; // 最短翻译长度
|
||||||
|
export const TRANS_MAX_LENGTH = 100000; // 最长翻译长度
|
||||||
|
export const TRANS_NEWLINE_LENGTH = 20; // 换行字符数
|
||||||
|
export const DEFAULT_BLACKLIST = [
|
||||||
|
"https://fishjar.github.io/kiss-translator/options.html",
|
||||||
|
"https://translate.google.com",
|
||||||
|
"https://www.deepl.com/translator",
|
||||||
|
]; // 禁用翻译名单
|
||||||
|
export const DEFAULT_CSPLIST = []; // 禁用CSP名单
|
||||||
|
export const DEFAULT_ORILIST = ["https://dict.youdao.com"]; // 移除Origin名单
|
||||||
|
|
||||||
|
// 同步设置
|
||||||
|
export const OPT_SYNCTYPE_WORKER = "KISS-Worker";
|
||||||
|
export const OPT_SYNCTYPE_WEBDAV = "WebDAV";
|
||||||
|
export const OPT_SYNCTOKEN_PERFIX = "kt_";
|
||||||
|
export const OPT_SYNCTYPE_ALL = [OPT_SYNCTYPE_WORKER, OPT_SYNCTYPE_WEBDAV];
|
||||||
|
export const DEFAULT_SYNC = {
|
||||||
|
syncType: OPT_SYNCTYPE_WORKER, // 同步方式
|
||||||
|
syncUrl: "", // 数据同步接口
|
||||||
|
syncUser: "", // 数据同步用户名
|
||||||
|
syncKey: "", // 数据同步密钥
|
||||||
|
syncMeta: {}, // 数据更新及同步信息
|
||||||
|
subRulesSyncAt: 0, // 订阅规则同步时间
|
||||||
|
dataCaches: {}, // 缓存同步时间
|
||||||
|
};
|
||||||
|
|
||||||
|
// 输入框翻译
|
||||||
|
export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
|
||||||
|
export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
|
||||||
|
export const DEFAULT_INPUT_RULE = {
|
||||||
|
transOpen: true,
|
||||||
|
apiSlug: OPT_TRANS_MICROSOFT,
|
||||||
|
fromLang: "auto",
|
||||||
|
toLang: "en",
|
||||||
|
triggerShortcut: DEFAULT_INPUT_SHORTCUT,
|
||||||
|
triggerCount: 1,
|
||||||
|
triggerTime: 200,
|
||||||
|
transSign: OPT_INPUT_TRANS_SIGNS[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 划词翻译
|
||||||
|
export const PHONIC_MAP = {
|
||||||
|
en_phonic: ["英", "uk"],
|
||||||
|
us_phonic: ["美", "en"],
|
||||||
|
};
|
||||||
|
export const OPT_TRANBOX_TRIGGER_CLICK = "click";
|
||||||
|
export const OPT_TRANBOX_TRIGGER_HOVER = "hover";
|
||||||
|
export const OPT_TRANBOX_TRIGGER_SELECT = "select";
|
||||||
|
export const OPT_TRANBOX_TRIGGER_ALL = [
|
||||||
|
OPT_TRANBOX_TRIGGER_CLICK,
|
||||||
|
OPT_TRANBOX_TRIGGER_HOVER,
|
||||||
|
OPT_TRANBOX_TRIGGER_SELECT,
|
||||||
|
];
|
||||||
|
export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
|
||||||
|
export const DEFAULT_TRANBOX_SETTING = {
|
||||||
|
transOpen: true, // 是否启用划词翻译
|
||||||
|
apiSlugs: [OPT_TRANS_MICROSOFT],
|
||||||
|
fromLang: "auto",
|
||||||
|
toLang: "zh-CN",
|
||||||
|
toLang2: "en",
|
||||||
|
tranboxShortcut: DEFAULT_TRANBOX_SHORTCUT,
|
||||||
|
btnOffsetX: 10,
|
||||||
|
btnOffsetY: 10,
|
||||||
|
boxOffsetX: 0,
|
||||||
|
boxOffsetY: 10,
|
||||||
|
hideTranBtn: false, // 是否隐藏翻译按钮
|
||||||
|
hideClickAway: false, // 是否点击外部关闭弹窗
|
||||||
|
simpleStyle: false, // 是否简洁界面
|
||||||
|
followSelection: false, // 翻译框是否跟随选中文本
|
||||||
|
autoHeight: false, // 自适应高度
|
||||||
|
triggerMode: OPT_TRANBOX_TRIGGER_CLICK, // 触发翻译方式
|
||||||
|
// extStyles: "", // 附加样式
|
||||||
|
enDict: OPT_DICT_BING, // 英文词典
|
||||||
|
enSug: OPT_SUG_YOUDAO, // 英文建议
|
||||||
|
};
|
||||||
|
|
||||||
|
const SUBTITLE_WINDOW_STYLE = `padding: 0.5em 1em;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-shadow: 1px 1px 2px black;
|
||||||
|
display: inline-block`;
|
||||||
|
|
||||||
|
const SUBTITLE_ORIGIN_STYLE = `font-size: clamp(1rem, 2cqw, 3rem);`;
|
||||||
|
|
||||||
|
const SUBTITLE_TRANSLATION_STYLE = `font-size: clamp(1rem, 2cqw, 3rem);`;
|
||||||
|
|
||||||
|
export const DEFAULT_SUBTITLE_SETTING = {
|
||||||
|
enabled: true, // 是否开启
|
||||||
|
apiSlug: OPT_TRANS_MICROSOFT,
|
||||||
|
segSlug: "-", // AI智能断句
|
||||||
|
chunkLength: 1000, // AI处理切割长度
|
||||||
|
preTrans: 90, // 提前翻译时长
|
||||||
|
throttleTrans: 30, // 节流翻译间隔
|
||||||
|
// fromLang: "en",
|
||||||
|
toLang: "zh-CN",
|
||||||
|
isBilingual: true, // 是否双语显示
|
||||||
|
skipAd: false, // 是否快进广告
|
||||||
|
windowStyle: SUBTITLE_WINDOW_STYLE, // 背景样式
|
||||||
|
originStyle: SUBTITLE_ORIGIN_STYLE, // 原文样式
|
||||||
|
translationStyle: SUBTITLE_TRANSLATION_STYLE, // 译文样式
|
||||||
|
};
|
||||||
|
|
||||||
|
// 订阅列表
|
||||||
|
export const DEFAULT_SUBRULES_LIST = [
|
||||||
|
{
|
||||||
|
url: process.env.REACT_APP_RULESURL,
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: process.env.REACT_APP_RULESURL_ON,
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: process.env.REACT_APP_RULESURL_OFF,
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_MOUSEHOVER_KEY = ["ControlLeft"];
|
||||||
|
export const DEFAULT_MOUSE_HOVER_SETTING = {
|
||||||
|
useMouseHover: false, // 是否启用鼠标悬停翻译
|
||||||
|
mouseHoverKey: DEFAULT_MOUSEHOVER_KEY, // 鼠标悬停翻译组合键
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_SETTING = {
|
||||||
|
darkMode: "auto", // 深色模式
|
||||||
|
uiLang: "en", // 界面语言
|
||||||
|
// fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至rule,作废)
|
||||||
|
// fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至rule,作废)
|
||||||
|
minLength: TRANS_MIN_LENGTH,
|
||||||
|
maxLength: TRANS_MAX_LENGTH,
|
||||||
|
newlineLength: TRANS_NEWLINE_LENGTH,
|
||||||
|
httpTimeout: DEFAULT_HTTP_TIMEOUT,
|
||||||
|
clearCache: false, // 是否在浏览器下次启动时清除缓存
|
||||||
|
injectRules: true, // 是否注入订阅规则
|
||||||
|
fabClickAction: 0, // 悬浮按钮点击行为
|
||||||
|
// injectWebfix: true, // 是否注入修复补丁(作废)
|
||||||
|
// detectRemote: false, // 是否使用远程语言检测 (从rule移回)
|
||||||
|
// contextMenus: true, // 是否添加右键菜单(作废)
|
||||||
|
contextMenuType: 1, // 右键菜单类型(0不显示,1简单菜单,2多级菜单)
|
||||||
|
// transTag: DEFAULT_TRANS_TAG, // 译文元素标签(移至rule,作废)
|
||||||
|
// transOnly: false, // 是否仅显示译文(移至rule,作废)
|
||||||
|
// transTitle: false, // 是否同时翻译页面标题(移至rule,作废)
|
||||||
|
subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
|
||||||
|
// owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则 (作废)
|
||||||
|
transApis: DEFAULT_API_LIST, // 翻译接口 (v2.0 对象改为数组)
|
||||||
|
// mouseKey: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译(移至rule,作废)
|
||||||
|
shortcuts: DEFAULT_SHORTCUTS, // 快捷键
|
||||||
|
inputRule: DEFAULT_INPUT_RULE, // 输入框设置
|
||||||
|
tranboxSetting: DEFAULT_TRANBOX_SETTING, // 划词翻译设置
|
||||||
|
// touchTranslate: 2, // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击} (作废)
|
||||||
|
touchModes: [2], // 触屏翻译 {5:单指双击,6:单指三击,7:双指双击} (多选)
|
||||||
|
blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
|
||||||
|
csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
|
||||||
|
orilist: DEFAULT_ORILIST.join(",\n"), // 禁用CSP名单
|
||||||
|
// disableLangs: [], // 不翻译的语言(移至rule,作废)
|
||||||
|
skipLangs: [], // 不翻译的语言(从rule移回)
|
||||||
|
transInterval: 100, // 翻译等待时间
|
||||||
|
langDetector: "-", // 远程语言识别服务
|
||||||
|
mouseHoverSetting: DEFAULT_MOUSE_HOVER_SETTING, // 鼠标悬停翻译
|
||||||
|
preInit: true, // 是否预加载脚本
|
||||||
|
transAllnow: false, // 是否立即全部翻译
|
||||||
|
subtitleSetting: DEFAULT_SUBTITLE_SETTING, // 字幕设置
|
||||||
|
logLevel: LogLevel.INFO.value, // 日志级别
|
||||||
|
rootMargin: 500, // 提前触发翻译
|
||||||
|
customStyles: DEFAULT_CUSTOM_STYLES, // 自定义样式列表
|
||||||
|
};
|
||||||
23
src/config/storage.js
Normal file
23
src/config/storage.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { APP_NAME, APP_VERSION } from "./app";
|
||||||
|
|
||||||
|
export const KV_RULES_KEY = `kiss-rules_v${APP_VERSION[0]}.json`;
|
||||||
|
export const KV_WORDS_KEY = "kiss-words.json";
|
||||||
|
export const KV_RULES_SHARE_KEY = `kiss-rules-share_v${APP_VERSION[0]}.json`;
|
||||||
|
export const KV_SETTING_KEY = `kiss-setting_v${APP_VERSION[0]}.json`;
|
||||||
|
export const KV_SALT_SYNC = "KISS-Translator-SYNC";
|
||||||
|
export const KV_SALT_SHARE = "KISS-Translator-SHARE";
|
||||||
|
|
||||||
|
export const STOKEY_MSAUTH = `${APP_NAME}_msauth`;
|
||||||
|
export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`;
|
||||||
|
export const STOKEY_SETTING_OLD = `${APP_NAME}_setting`;
|
||||||
|
export const STOKEY_RULES_OLD = `${APP_NAME}_rules`;
|
||||||
|
export const STOKEY_SETTING = `${APP_NAME}_setting_v${APP_VERSION[0]}`;
|
||||||
|
export const STOKEY_RULES = `${APP_NAME}_rules_v${APP_VERSION[0]}`;
|
||||||
|
export const STOKEY_WORDS = `${APP_NAME}_words`;
|
||||||
|
export const STOKEY_SYNC = `${APP_NAME}_sync`;
|
||||||
|
export const STOKEY_FAB = `${APP_NAME}_fab`;
|
||||||
|
export const STOKEY_TRANBOX = `${APP_NAME}_tranbox`;
|
||||||
|
export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`;
|
||||||
|
|
||||||
|
export const CACHE_NAME = `${APP_NAME}_cache`;
|
||||||
|
export const DEFAULT_CACHE_TIMEOUT = 3600 * 24 * 7; // 缓存超时时间(7天)
|
||||||
46
src/config/styles.js
Normal file
46
src/config/styles.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
export const OPT_STYLE_NONE = "style_none"; // 无
|
||||||
|
export const OPT_STYLE_LINE = "under_line"; // 下划线
|
||||||
|
export const OPT_STYLE_DOTLINE = "dot_line"; // 点状线
|
||||||
|
export const OPT_STYLE_DASHLINE = "dash_line"; // 虚线
|
||||||
|
export const OPT_STYLE_DASHLINE_BOLD = "dash_line_bold"; // 虚线加粗
|
||||||
|
export const OPT_STYLE_DASHBOX = "dash_box"; // 虚线框
|
||||||
|
export const OPT_STYLE_DASHBOX_BOLD = "dash_box_bold"; // 虚线框加粗
|
||||||
|
export const OPT_STYLE_WAVYLINE = "wavy_line"; // 波浪线
|
||||||
|
export const OPT_STYLE_WAVYLINE_BOLD = "wavy_line_bold"; // 波浪线加粗
|
||||||
|
export const OPT_STYLE_MARKER = "marker"; // 马克笔
|
||||||
|
export const OPT_STYLE_GRADIENT_MARKER = "gradient_marker"; // 渐变马克笔
|
||||||
|
export const OPT_STYLE_FUZZY = "fuzzy"; // 模糊
|
||||||
|
export const OPT_STYLE_HIGHLIGHT = "highlight"; // 高亮
|
||||||
|
export const OPT_STYLE_BLOCKQUOTE = "blockquote"; // 引用
|
||||||
|
export const OPT_STYLE_GRADIENT = "gradient"; // 渐变
|
||||||
|
export const OPT_STYLE_BLINK = "blink"; // 闪现
|
||||||
|
export const OPT_STYLE_GLOW = "glow"; // 发光
|
||||||
|
export const OPT_STYLE_COLORFUL = "colorful"; // 多彩
|
||||||
|
export const OPT_STYLE_ALL = [
|
||||||
|
OPT_STYLE_NONE,
|
||||||
|
OPT_STYLE_LINE,
|
||||||
|
OPT_STYLE_DOTLINE,
|
||||||
|
OPT_STYLE_DASHLINE,
|
||||||
|
OPT_STYLE_DASHLINE_BOLD,
|
||||||
|
OPT_STYLE_WAVYLINE,
|
||||||
|
OPT_STYLE_WAVYLINE_BOLD,
|
||||||
|
OPT_STYLE_DASHBOX,
|
||||||
|
OPT_STYLE_DASHBOX_BOLD,
|
||||||
|
OPT_STYLE_MARKER,
|
||||||
|
OPT_STYLE_GRADIENT_MARKER,
|
||||||
|
OPT_STYLE_FUZZY,
|
||||||
|
OPT_STYLE_HIGHLIGHT,
|
||||||
|
OPT_STYLE_BLOCKQUOTE,
|
||||||
|
OPT_STYLE_GRADIENT,
|
||||||
|
OPT_STYLE_BLINK,
|
||||||
|
OPT_STYLE_GLOW,
|
||||||
|
OPT_STYLE_COLORFUL,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_CUSTOM_STYLES = [
|
||||||
|
{
|
||||||
|
styleSlug: "custom",
|
||||||
|
styleName: "Custom Style",
|
||||||
|
styleCode: `color: #209CEE;`,
|
||||||
|
},
|
||||||
|
];
|
||||||
14
src/config/url.js
Normal file
14
src/config/url.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { APP_LCNAME } from "./app";
|
||||||
|
|
||||||
|
export const URL_CACHE_TRAN = `https://${APP_LCNAME}/translate`;
|
||||||
|
export const URL_CACHE_SUBTITLE = `https://${APP_LCNAME}/subtitle`;
|
||||||
|
export const URL_CACHE_DELANG = `https://${APP_LCNAME}/detectlang`;
|
||||||
|
export const URL_CACHE_BINGDICT = `https://${APP_LCNAME}/bingdict`;
|
||||||
|
|
||||||
|
export const URL_KISS_WORKER = "https://github.com/fishjar/kiss-worker";
|
||||||
|
export const URL_KISS_PROXY = "https://github.com/fishjar/kiss-proxy";
|
||||||
|
export const URL_KISS_RULES = "https://github.com/fishjar/kiss-rules";
|
||||||
|
export const URL_KISS_RULES_NEW_ISSUE =
|
||||||
|
"https://github.com/fishjar/kiss-rules/issues/new";
|
||||||
|
export const URL_RAW_PREFIX =
|
||||||
|
"https://raw.githubusercontent.com/fishjar/kiss-translator/master";
|
||||||
@@ -1,81 +1,5 @@
|
|||||||
import { browser } from "./libs/browser";
|
import { run } from "./common";
|
||||||
import {
|
|
||||||
MSG_TRANS_TOGGLE,
|
|
||||||
MSG_TRANS_TOGGLE_STYLE,
|
|
||||||
MSG_TRANS_GETRULE,
|
|
||||||
MSG_TRANS_PUTRULE,
|
|
||||||
} from "./config";
|
|
||||||
import { getSettingWithDefault } from "./libs/storage";
|
|
||||||
import { isIframe, sendIframeMsg } from "./libs/iframe";
|
|
||||||
import { runWebfix } from "./libs/webfix";
|
|
||||||
import {
|
|
||||||
runIframe,
|
|
||||||
runTranslator,
|
|
||||||
showFab,
|
|
||||||
showTransbox,
|
|
||||||
windowListener,
|
|
||||||
showErr,
|
|
||||||
touchOperation,
|
|
||||||
} from "./common";
|
|
||||||
|
|
||||||
function runtimeListener(translator) {
|
globalThis.__KISS_CONTEXT__ = "content";
|
||||||
browser?.runtime.onMessage.addListener(async ({ action, args }) => {
|
|
||||||
switch (action) {
|
|
||||||
case MSG_TRANS_TOGGLE:
|
|
||||||
translator.toggle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE);
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_TOGGLE_STYLE:
|
|
||||||
translator.toggleStyle();
|
|
||||||
sendIframeMsg(MSG_TRANS_TOGGLE_STYLE);
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_GETRULE:
|
|
||||||
break;
|
|
||||||
case MSG_TRANS_PUTRULE:
|
|
||||||
translator.updateRule(args);
|
|
||||||
sendIframeMsg(MSG_TRANS_PUTRULE, args);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return { error: `message action is unavailable: ${action}` };
|
|
||||||
}
|
|
||||||
return { data: translator.rule };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
run();
|
||||||
* 入口函数
|
|
||||||
*/
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
// 读取设置信息
|
|
||||||
const setting = await getSettingWithDefault();
|
|
||||||
|
|
||||||
// 适配iframe
|
|
||||||
if (isIframe) {
|
|
||||||
runIframe(setting);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不规范网页修复
|
|
||||||
await runWebfix(setting);
|
|
||||||
|
|
||||||
// 翻译网页
|
|
||||||
const { translator, rule } = await runTranslator(setting);
|
|
||||||
|
|
||||||
// 监听消息
|
|
||||||
windowListener(rule);
|
|
||||||
runtimeListener(translator);
|
|
||||||
|
|
||||||
// 划词翻译
|
|
||||||
showTransbox(setting);
|
|
||||||
|
|
||||||
// 浮球按钮
|
|
||||||
await showFab(translator);
|
|
||||||
|
|
||||||
// 触屏操作
|
|
||||||
touchOperation(translator);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[KISS-Translator]", err);
|
|
||||||
showErr(err.message);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { createContext, useContext, useState, forwardRef } from "react";
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
import Snackbar from "@mui/material/Snackbar";
|
import Snackbar from "@mui/material/Snackbar";
|
||||||
import MuiAlert from "@mui/material/Alert";
|
import MuiAlert from "@mui/material/Alert";
|
||||||
|
|
||||||
@@ -18,36 +25,45 @@ export function AlertProvider({ children }) {
|
|||||||
const horizontal = "center";
|
const horizontal = "center";
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [severity, setSeverity] = useState("info");
|
const [severity, setSeverity] = useState("info");
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState(null);
|
||||||
|
|
||||||
const showAlert = (msg, type) => {
|
const showAlert = useCallback((msg, type) => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
setMessage(msg);
|
setMessage(msg);
|
||||||
setSeverity(type);
|
setSeverity(type);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleClose = (_, reason) => {
|
const handleClose = useCallback((_, reason) => {
|
||||||
if (reason === "clickaway") {
|
if (reason === "clickaway") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const error = (msg) => showAlert(msg, "error");
|
const value = useMemo(
|
||||||
const warning = (msg) => showAlert(msg, "warning");
|
() => ({
|
||||||
const info = (msg) => showAlert(msg, "info");
|
error: (msg) => showAlert(msg, "error"),
|
||||||
const success = (msg) => showAlert(msg, "success");
|
warning: (msg) => showAlert(msg, "warning"),
|
||||||
|
info: (msg) => showAlert(msg, "info"),
|
||||||
|
success: (msg) => showAlert(msg, "success"),
|
||||||
|
}),
|
||||||
|
[showAlert]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertContext.Provider value={{ error, warning, info, success }}>
|
<AlertContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={open}
|
open={open}
|
||||||
autoHideDuration={3000}
|
autoHideDuration={10000}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
anchorOrigin={{ vertical, horizontal }}
|
anchorOrigin={{ vertical, horizontal }}
|
||||||
>
|
>
|
||||||
<Alert onClose={handleClose} severity={severity} sx={{ width: "100%" }}>
|
<Alert
|
||||||
|
onClose={handleClose}
|
||||||
|
severity={severity}
|
||||||
|
sx={{ minWidth: "300px", maxWidth: "80%" }}
|
||||||
|
>
|
||||||
{message}
|
{message}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
|
|||||||
144
src/hooks/Api.js
144
src/hooks/Api.js
@@ -1,24 +1,136 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { DEFAULT_TRANS_APIS } from "../config";
|
import { DEFAULT_API_LIST, API_SPE_TYPES } from "../config";
|
||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
export function useApi(translator) {
|
function useApiState() {
|
||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateSetting } = useSetting();
|
||||||
const transApis = setting?.transApis || DEFAULT_TRANS_APIS;
|
const transApis = setting?.transApis || [];
|
||||||
|
|
||||||
const updateApi = useCallback(
|
return { transApis, updateSetting };
|
||||||
async (obj) => {
|
}
|
||||||
const api = transApis[translator] || {};
|
|
||||||
Object.assign(transApis, { [translator]: { ...api, ...obj } });
|
export function useApiList() {
|
||||||
await updateSetting({ transApis });
|
const { transApis, updateSetting } = useApiState();
|
||||||
},
|
|
||||||
[translator, transApis, updateSetting]
|
useEffect(() => {
|
||||||
|
const curSlugs = new Set(transApis.map((api) => api.apiSlug));
|
||||||
|
const missApis = DEFAULT_API_LIST.filter(
|
||||||
|
(api) => !curSlugs.has(api.apiSlug)
|
||||||
|
);
|
||||||
|
if (missApis.length > 0) {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transApis: [...(prev?.transApis || []), ...missApis],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [transApis, updateSetting]);
|
||||||
|
|
||||||
|
const userApis = useMemo(
|
||||||
|
() =>
|
||||||
|
transApis
|
||||||
|
.filter((api) => !API_SPE_TYPES.builtin.has(api.apiSlug))
|
||||||
|
.sort((a, b) => a.apiSlug.localeCompare(b.apiSlug)),
|
||||||
|
[transApis]
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetApi = useCallback(async () => {
|
const builtinApis = useMemo(
|
||||||
Object.assign(transApis, { [translator]: DEFAULT_TRANS_APIS[translator] });
|
() => transApis.filter((api) => API_SPE_TYPES.builtin.has(api.apiSlug)),
|
||||||
await updateSetting({ transApis });
|
[transApis]
|
||||||
}, [translator, transApis, updateSetting]);
|
);
|
||||||
|
|
||||||
return { api: transApis[translator] || {}, updateApi, resetApi };
|
const enabledApis = useMemo(
|
||||||
|
() => transApis.filter((api) => !api.isDisabled),
|
||||||
|
[transApis]
|
||||||
|
);
|
||||||
|
|
||||||
|
const aiEnabledApis = useMemo(
|
||||||
|
() => enabledApis.filter((api) => API_SPE_TYPES.ai.has(api.apiType)),
|
||||||
|
[enabledApis]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addApi = useCallback(
|
||||||
|
(apiType) => {
|
||||||
|
const defaultApiOpt =
|
||||||
|
DEFAULT_API_LIST.find((da) => da.apiType === apiType) || {};
|
||||||
|
const uuid = crypto.randomUUID();
|
||||||
|
const apiSlug = `${apiType}_${crypto.randomUUID()}`;
|
||||||
|
const apiName = `${apiType}_${uuid.slice(0, 8)}`;
|
||||||
|
const newApi = {
|
||||||
|
...defaultApiOpt,
|
||||||
|
apiSlug,
|
||||||
|
apiName,
|
||||||
|
apiType,
|
||||||
|
};
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transApis: [...(prev?.transApis || []), newApi],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteApi = useCallback(
|
||||||
|
(apiSlug) => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transApis: (prev?.transApis || []).filter(
|
||||||
|
(api) => api.apiSlug !== apiSlug
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transApis,
|
||||||
|
userApis,
|
||||||
|
builtinApis,
|
||||||
|
enabledApis,
|
||||||
|
aiEnabledApis,
|
||||||
|
addApi,
|
||||||
|
deleteApi,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApiItem(apiSlug) {
|
||||||
|
const { transApis, updateSetting } = useApiState();
|
||||||
|
|
||||||
|
const api = useMemo(
|
||||||
|
() => transApis.find((a) => a.apiSlug === apiSlug),
|
||||||
|
[transApis, apiSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
(updateData) => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transApis: (prev?.transApis || []).map((item) =>
|
||||||
|
item.apiSlug === apiSlug ? { ...item, ...updateData, apiSlug } : item
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[apiSlug, updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
transApis: (prev?.transApis || []).map((item) => {
|
||||||
|
if (item.apiSlug === apiSlug) {
|
||||||
|
const defaultApiOpt =
|
||||||
|
DEFAULT_API_LIST.find((da) => da.apiType === item.apiType) || {};
|
||||||
|
return {
|
||||||
|
...defaultApiOpt,
|
||||||
|
apiSlug: item.apiSlug,
|
||||||
|
apiName: item.apiName,
|
||||||
|
apiType: item.apiType,
|
||||||
|
key: item.key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}, [apiSlug, updateSetting]);
|
||||||
|
|
||||||
|
return { api, update, reset };
|
||||||
}
|
}
|
||||||
|
|||||||
108
src/hooks/Audio.js
Normal file
108
src/hooks/Audio.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { logger } from "../libs/log";
|
||||||
|
import { fetchData } from "../libs/fetch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 声音播放hook
|
||||||
|
* @param {*} src
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useAudio(src) {
|
||||||
|
const audioRef = useRef(null);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
const [playing, setPlaying] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const onPlay = useCallback(async () => {
|
||||||
|
if (!audioRef.current) return;
|
||||||
|
try {
|
||||||
|
await audioRef.current.play();
|
||||||
|
} catch (err) {
|
||||||
|
logger.info("Playback failed:", err);
|
||||||
|
setPlaying(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPause = useCallback(() => {
|
||||||
|
audioRef.current?.pause();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!src) return;
|
||||||
|
|
||||||
|
let ignore = false;
|
||||||
|
let objectUrl = null;
|
||||||
|
|
||||||
|
setReady(false);
|
||||||
|
setError(null);
|
||||||
|
setPlaying(false);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const audio = new Audio();
|
||||||
|
audioRef.current = audio;
|
||||||
|
|
||||||
|
const handleCanPlay = () => setReady(true);
|
||||||
|
const handlePlay = () => setPlaying(true);
|
||||||
|
const handlePause = () => setPlaying(false);
|
||||||
|
const handleEnded = () => setPlaying(false);
|
||||||
|
const handleError = (e) => {
|
||||||
|
if (!ignore) {
|
||||||
|
setError(audio.error || e);
|
||||||
|
setReady(false);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.addEventListener("canplaythrough", handleCanPlay);
|
||||||
|
audio.addEventListener("play", handlePlay);
|
||||||
|
audio.addEventListener("pause", handlePause);
|
||||||
|
audio.addEventListener("ended", handleEnded);
|
||||||
|
audio.addEventListener("error", handleError);
|
||||||
|
|
||||||
|
const loadAudio = async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchData(src, {}, { expect: "audio" });
|
||||||
|
if (ignore) return;
|
||||||
|
|
||||||
|
audio.src = data;
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
if (!ignore) {
|
||||||
|
logger.info("Audio fetch failed:", err);
|
||||||
|
setError(err);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadAudio();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ignore = true;
|
||||||
|
|
||||||
|
audio.pause();
|
||||||
|
audio.removeAttribute("src");
|
||||||
|
|
||||||
|
if (objectUrl) {
|
||||||
|
URL.revokeObjectURL(objectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
audio.removeEventListener("canplaythrough", handleCanPlay);
|
||||||
|
audio.removeEventListener("play", handlePlay);
|
||||||
|
audio.removeEventListener("pause", handlePause);
|
||||||
|
audio.removeEventListener("ended", handleEnded);
|
||||||
|
audio.removeEventListener("error", handleError);
|
||||||
|
};
|
||||||
|
}, [src]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
ready,
|
||||||
|
playing,
|
||||||
|
onPlay,
|
||||||
|
onPause,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,8 +11,13 @@ export function useDarkMode() {
|
|||||||
updateSetting,
|
updateSetting,
|
||||||
} = useSetting();
|
} = useSetting();
|
||||||
|
|
||||||
const toggleDarkMode = useCallback(async () => {
|
const toggleDarkMode = useCallback(() => {
|
||||||
await updateSetting({ darkMode: !darkMode });
|
const nextMode = {
|
||||||
|
light: "dark",
|
||||||
|
dark: "auto",
|
||||||
|
auto: "light",
|
||||||
|
};
|
||||||
|
updateSetting({ darkMode: nextMode[darkMode] || "light" });
|
||||||
}, [darkMode, updateSetting]);
|
}, [darkMode, updateSetting]);
|
||||||
|
|
||||||
return { darkMode, toggleDarkMode };
|
return { darkMode, toggleDarkMode };
|
||||||
|
|||||||
97
src/hooks/Confirm.js
Normal file
97
src/hooks/Confirm.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
|
import Dialog from "@mui/material/Dialog";
|
||||||
|
import DialogActions from "@mui/material/DialogActions";
|
||||||
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
|
import DialogContentText from "@mui/material/DialogContentText";
|
||||||
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import { useI18n } from "./I18n";
|
||||||
|
|
||||||
|
const ConfirmContext = createContext(null);
|
||||||
|
|
||||||
|
export function ConfirmProvider({ children }) {
|
||||||
|
const [dialogConfig, setDialogConfig] = useState(null);
|
||||||
|
const resolveRef = useRef(null);
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const translatedDefaults = useMemo(
|
||||||
|
() => ({
|
||||||
|
title: i18n("confirm_title", "Confirm"),
|
||||||
|
message: i18n("confirm_message", "Are you sure you want to proceed?"),
|
||||||
|
confirmText: i18n("confirm_action", "Confirm"),
|
||||||
|
cancelText: i18n("cancel_action", "Cancel"),
|
||||||
|
}),
|
||||||
|
[i18n]
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirm = useCallback(
|
||||||
|
(config) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setDialogConfig({ ...translatedDefaults, ...config });
|
||||||
|
resolveRef.current = resolve;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[translatedDefaults]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (resolveRef.current) {
|
||||||
|
resolveRef.current(false);
|
||||||
|
}
|
||||||
|
setDialogConfig(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (resolveRef.current) {
|
||||||
|
resolveRef.current(true);
|
||||||
|
}
|
||||||
|
setDialogConfig(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmContext.Provider value={confirm}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={!!dialogConfig}
|
||||||
|
onClose={handleClose}
|
||||||
|
aria-labelledby="confirm-dialog-title"
|
||||||
|
aria-describedby="confirm-dialog-description"
|
||||||
|
>
|
||||||
|
{dialogConfig && (
|
||||||
|
<>
|
||||||
|
<DialogTitle id="confirm-dialog-title">
|
||||||
|
{dialogConfig.title}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="confirm-dialog-description">
|
||||||
|
{dialogConfig.message}
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleClose}>{dialogConfig.cancelText}</Button>
|
||||||
|
<Button onClick={handleConfirm} color="primary" autoFocus>
|
||||||
|
{dialogConfig.confirmText}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</ConfirmContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfirm() {
|
||||||
|
const context = useContext(ConfirmContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useConfirm must be used within a ConfirmProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
84
src/hooks/CustomStyles.js
Normal file
84
src/hooks/CustomStyles.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useSetting } from "./Setting";
|
||||||
|
import { DEFAULT_CUSTOM_STYLES, OPT_STYLE_ALL } from "../config/styles";
|
||||||
|
import { builtinStylesMap } from "../libs/style";
|
||||||
|
import { useI18n } from "./I18n";
|
||||||
|
|
||||||
|
function useStyleState() {
|
||||||
|
const { setting, updateSetting } = useSetting();
|
||||||
|
const customStyles = setting?.customStyles || [];
|
||||||
|
|
||||||
|
return { customStyles, updateSetting };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStyleList() {
|
||||||
|
const { customStyles, updateSetting } = useStyleState();
|
||||||
|
|
||||||
|
const addStyle = useCallback(() => {
|
||||||
|
const defaultStyle = DEFAULT_CUSTOM_STYLES[0];
|
||||||
|
const uuid = crypto.randomUUID();
|
||||||
|
const styleSlug = `custom_${crypto.randomUUID()}`;
|
||||||
|
const styleName = `Style_${uuid.slice(0, 8)}`;
|
||||||
|
const newStyle = {
|
||||||
|
...defaultStyle,
|
||||||
|
styleSlug,
|
||||||
|
styleName,
|
||||||
|
};
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
customStyles: [...(prev?.customStyles || []), newStyle],
|
||||||
|
}));
|
||||||
|
}, [updateSetting]);
|
||||||
|
|
||||||
|
const deleteStyle = useCallback(
|
||||||
|
(styleSlug) => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
customStyles: (prev?.customStyles || []).filter(
|
||||||
|
(item) => item.styleSlug !== styleSlug
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateStyle = useCallback(
|
||||||
|
(styleSlug, updateData) => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
customStyles: (prev?.customStyles || []).map((item) =>
|
||||||
|
item.styleSlug === styleSlug ? { ...item, ...updateData } : item
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
customStyles,
|
||||||
|
addStyle,
|
||||||
|
deleteStyle,
|
||||||
|
updateStyle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAllTextStyles() {
|
||||||
|
const { customStyles } = useStyleList();
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const builtinStyles = useMemo(
|
||||||
|
() =>
|
||||||
|
OPT_STYLE_ALL.map((styleSlug) => ({
|
||||||
|
styleSlug,
|
||||||
|
styleName: i18n(styleSlug),
|
||||||
|
styleCode: builtinStylesMap[styleSlug] || "",
|
||||||
|
})),
|
||||||
|
[i18n]
|
||||||
|
);
|
||||||
|
|
||||||
|
const allTextStyles = useMemo(() => {
|
||||||
|
return [...builtinStyles, ...customStyles];
|
||||||
|
}, [builtinStyles, customStyles]);
|
||||||
|
|
||||||
|
return { builtinStyles, customStyles, allTextStyles };
|
||||||
|
}
|
||||||
23
src/hooks/DebouncedCallback.js
Normal file
23
src/hooks/DebouncedCallback.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useMemo, useEffect, useRef } from "react";
|
||||||
|
import { debounce } from "../libs/utils";
|
||||||
|
|
||||||
|
export function useDebouncedCallback(callback, delay) {
|
||||||
|
const callbackRef = useRef(callback);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
callbackRef.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
const debouncedCallback = useMemo(
|
||||||
|
() => debounce((...args) => callbackRef.current(...args), delay),
|
||||||
|
[delay]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
debouncedCallback.cancel();
|
||||||
|
};
|
||||||
|
}, [debouncedCallback]);
|
||||||
|
|
||||||
|
return debouncedCallback;
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { STOKEY_FAB } from "../config";
|
import { STOKEY_FAB } from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
|
|
||||||
|
const DEFAULT_FAB = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fab hook
|
* fab hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useFab() {
|
export function useFab() {
|
||||||
const { data, update } = useStorage(STOKEY_FAB);
|
const { data, update } = useStorage(STOKEY_FAB, DEFAULT_FAB);
|
||||||
return { fab: data, updateFab: update };
|
return { fab: data, updateFab: update };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,64 @@
|
|||||||
import { KV_WORDS_KEY } from "../config";
|
import { STOKEY_WORDS, KV_WORDS_KEY } from "../config";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { trySyncWords } from "../libs/sync";
|
import { useStorage } from "./Storage";
|
||||||
import { getWordsWithDefault, setWords } from "../libs/storage";
|
import { debounceSyncMeta } from "../libs/storage";
|
||||||
import { useSyncMeta } from "./Sync";
|
|
||||||
|
const DEFAULT_FAVWORDS = {};
|
||||||
|
|
||||||
export function useFavWords() {
|
export function useFavWords() {
|
||||||
const [loading, setLoading] = useState(false);
|
const { data: favWords, save: saveWords } = useStorage(
|
||||||
const [favWords, setFavWords] = useState({});
|
STOKEY_WORDS,
|
||||||
const { updateSyncMeta } = useSyncMeta();
|
DEFAULT_FAVWORDS,
|
||||||
|
KV_WORDS_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
const save = useCallback(
|
||||||
|
(objOrFn) => {
|
||||||
|
saveWords(objOrFn);
|
||||||
|
debounceSyncMeta(KV_WORDS_KEY);
|
||||||
|
},
|
||||||
|
[saveWords]
|
||||||
|
);
|
||||||
|
|
||||||
const toggleFav = useCallback(
|
const toggleFav = useCallback(
|
||||||
async (word) => {
|
(word) => {
|
||||||
const favs = { ...favWords };
|
save((prev) => {
|
||||||
if (favs[word]) {
|
if (!prev[word]) {
|
||||||
|
return { ...prev, [word]: { createdAt: Date.now() } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const favs = { ...prev };
|
||||||
delete favs[word];
|
delete favs[word];
|
||||||
} else {
|
return favs;
|
||||||
favs[word] = { createdAt: Date.now() };
|
});
|
||||||
}
|
|
||||||
await setWords(favs);
|
|
||||||
await updateSyncMeta(KV_WORDS_KEY);
|
|
||||||
await trySyncWords();
|
|
||||||
setFavWords(favs);
|
|
||||||
},
|
},
|
||||||
[updateSyncMeta, favWords]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergeWords = useCallback(
|
const mergeWords = useCallback(
|
||||||
async (newWords) => {
|
(words) => {
|
||||||
const favs = { ...favWords };
|
save((prev) => ({
|
||||||
newWords.forEach((word) => {
|
...words.reduce((acc, key) => {
|
||||||
if (!favs[word]) {
|
acc[key] = { createdAt: Date.now() };
|
||||||
favs[word] = { createdAt: Date.now() };
|
return acc;
|
||||||
}
|
}, {}),
|
||||||
});
|
...prev,
|
||||||
await setWords(favs);
|
}));
|
||||||
await updateSyncMeta(KV_WORDS_KEY);
|
|
||||||
await trySyncWords();
|
|
||||||
setFavWords(favs);
|
|
||||||
},
|
},
|
||||||
[updateSyncMeta, favWords]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearWords = useCallback(async () => {
|
const clearWords = useCallback(() => {
|
||||||
await setWords({});
|
save({});
|
||||||
await updateSyncMeta(KV_WORDS_KEY);
|
}, [save]);
|
||||||
await trySyncWords();
|
|
||||||
setFavWords({});
|
|
||||||
}, [updateSyncMeta]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const favList = useMemo(
|
||||||
(async () => {
|
() =>
|
||||||
try {
|
Object.entries(favWords || {}).sort((a, b) => a[0].localeCompare(b[0])),
|
||||||
setLoading(true);
|
[favWords]
|
||||||
await trySyncWords();
|
);
|
||||||
const favWords = await getWordsWithDefault();
|
|
||||||
setFavWords(favWords);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[query fav]", err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { loading, favWords, toggleFav, mergeWords, clearWords };
|
const wordList = useMemo(() => favList.map(([word]) => word), [favList]);
|
||||||
|
|
||||||
|
return { favWords, favList, wordList, toggleFav, mergeWords, clearWords };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,152 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
|
||||||
/**
|
export const useAsync = () => {
|
||||||
* fetch data hook
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const useFetch = (url) => {
|
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
const execute = useCallback(async (fn, ...args) => {
|
||||||
if (!url) {
|
if (!fn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
setLoading(true);
|
||||||
setLoading(true);
|
setError(null);
|
||||||
try {
|
|
||||||
const res = await fetch(url);
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`[${res.status}] ${res.statusText}`);
|
|
||||||
}
|
|
||||||
let data;
|
|
||||||
if (res.headers.get("Content-Type")?.includes("json")) {
|
|
||||||
data = await res.json();
|
|
||||||
} else {
|
|
||||||
data = await res.text();
|
|
||||||
}
|
|
||||||
setData(data);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
return [data, loading, error];
|
try {
|
||||||
|
const res = await fn(...args);
|
||||||
|
setData(res);
|
||||||
|
setLoading(false);
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
setError(err?.message || "An unknown error occurred");
|
||||||
|
setLoading(false);
|
||||||
|
// throw err;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setData(null);
|
||||||
|
setLoading(false);
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { data, loading, error, execute, reset };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAsyncNow = (fn, arg) => {
|
||||||
|
const { execute, ...asyncState } = useAsync();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fn) {
|
||||||
|
execute(fn, arg);
|
||||||
|
}
|
||||||
|
}, [execute, fn, arg]);
|
||||||
|
|
||||||
|
return { ...asyncState };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFetch = () => {
|
||||||
|
const { execute, ...asyncState } = useAsync();
|
||||||
|
|
||||||
|
const requester = useCallback(async (url, options) => {
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorInfo = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`Request failed: ${response.status} ${response.statusText} - ${errorInfo}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (response.status === 204) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.headers.get("Content-Type")?.includes("json")) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const get = useCallback(
|
||||||
|
async (url, options = {}) => {
|
||||||
|
try {
|
||||||
|
const result = await execute(requester, url, {
|
||||||
|
...options,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[execute, requester]
|
||||||
|
);
|
||||||
|
|
||||||
|
const post = useCallback(
|
||||||
|
async (url, body, options = {}) => {
|
||||||
|
try {
|
||||||
|
const result = await execute(requester, url, {
|
||||||
|
...options,
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", ...options.headers },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[execute, requester]
|
||||||
|
);
|
||||||
|
|
||||||
|
const put = useCallback(
|
||||||
|
async (url, body, options = {}) => {
|
||||||
|
try {
|
||||||
|
const result = await execute(requester, url, {
|
||||||
|
...options,
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json", ...options.headers },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[execute, requester]
|
||||||
|
);
|
||||||
|
|
||||||
|
const del = useCallback(
|
||||||
|
async (url, options = {}) => {
|
||||||
|
try {
|
||||||
|
const result = await execute(requester, url, {
|
||||||
|
...options,
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[execute, requester]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...asyncState,
|
||||||
|
get,
|
||||||
|
post,
|
||||||
|
put,
|
||||||
|
del,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGet = (url) => {
|
||||||
|
const { get, ...fetchState } = useFetch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (url) get(url);
|
||||||
|
}, [url, get]);
|
||||||
|
|
||||||
|
return { ...fetchState };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
import { I18N, URL_RAW_PREFIX } from "../config";
|
import { I18N, URL_RAW_PREFIX } from "../config";
|
||||||
import { useFetch } from "./Fetch";
|
import { useGet } from "./Fetch";
|
||||||
|
|
||||||
|
export const getI18n = (uiLang, key, defaultText = "") => {
|
||||||
|
return I18N?.[key]?.[uiLang] ?? defaultText;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLangMap = (uiLang) => {
|
||||||
|
return (key, defaultText = "") => getI18n(uiLang, key, defaultText);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 多语言 hook
|
* 多语言 hook
|
||||||
@@ -10,12 +18,12 @@ export const useI18n = () => {
|
|||||||
const {
|
const {
|
||||||
setting: { uiLang },
|
setting: { uiLang },
|
||||||
} = useSetting();
|
} = useSetting();
|
||||||
return (key, defaultText = "") => I18N?.[key]?.[uiLang] ?? defaultText;
|
return useLangMap(uiLang);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useI18nMd = (key) => {
|
export const useI18nMd = (key) => {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const fileName = i18n(key);
|
const fileName = i18n(key);
|
||||||
const url = fileName ? `${URL_RAW_PREFIX}/${fileName}` : "";
|
const url = fileName ? `${URL_RAW_PREFIX}/${fileName}` : "";
|
||||||
return useFetch(url);
|
return useGet(url);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { DEFAULT_INPUT_RULE } from "../config";
|
import { DEFAULT_INPUT_RULE } from "../config";
|
||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
export function useInputRule() {
|
export function useInputRule() {
|
||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateChild } = useSetting();
|
||||||
const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
|
const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
|
||||||
|
const updateInputRule = updateChild("inputRule");
|
||||||
const updateInputRule = useCallback(
|
|
||||||
async (obj) => {
|
|
||||||
Object.assign(inputRule, obj);
|
|
||||||
await updateSetting({ inputRule });
|
|
||||||
},
|
|
||||||
[inputRule, updateSetting]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { inputRule, updateInputRule };
|
return { inputRule, updateInputRule };
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/hooks/Loading.js
Normal file
16
src/hooks/Loading.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<center>
|
||||||
|
<Divider>
|
||||||
|
<Link
|
||||||
|
href={process.env.REACT_APP_HOMEPAGE}
|
||||||
|
>{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
|
||||||
|
</Divider>
|
||||||
|
<CircularProgress />
|
||||||
|
</center>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/hooks/MouseHover.js
Normal file
11
src/hooks/MouseHover.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { DEFAULT_MOUSE_HOVER_SETTING } from "../config";
|
||||||
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
|
export function useMouseHoverSetting() {
|
||||||
|
const { setting, updateChild } = useSetting();
|
||||||
|
const mouseHoverSetting =
|
||||||
|
setting?.mouseHoverSetting || DEFAULT_MOUSE_HOVER_SETTING;
|
||||||
|
const updateMouseHoverSetting = updateChild("mouseHoverSetting");
|
||||||
|
|
||||||
|
return { mouseHoverSetting, updateMouseHoverSetting };
|
||||||
|
}
|
||||||
@@ -1,90 +1,104 @@
|
|||||||
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
import { trySyncRules } from "../libs/sync";
|
|
||||||
import { checkRules } from "../libs/rules";
|
import { checkRules } from "../libs/rules";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useSyncMeta } from "./Sync";
|
import { debounceSyncMeta } from "../libs/storage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 规则 hook
|
* 规则 hook
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useRules() {
|
export function useRules() {
|
||||||
const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
|
const { data: list = [], save: saveRules } = useStorage(
|
||||||
const { updateSyncMeta } = useSyncMeta();
|
STOKEY_RULES,
|
||||||
|
DEFAULT_RULES,
|
||||||
|
KV_RULES_KEY
|
||||||
|
);
|
||||||
|
|
||||||
const updateRules = useCallback(
|
const save = useCallback(
|
||||||
async (rules) => {
|
(objOrFn) => {
|
||||||
await save(rules);
|
saveRules(objOrFn);
|
||||||
await updateSyncMeta(KV_RULES_KEY);
|
debounceSyncMeta(KV_RULES_KEY);
|
||||||
trySyncRules();
|
|
||||||
},
|
},
|
||||||
[save, updateSyncMeta]
|
[saveRules]
|
||||||
);
|
);
|
||||||
|
|
||||||
const add = useCallback(
|
const add = useCallback(
|
||||||
async (rule) => {
|
(rule) => {
|
||||||
const rules = [...list];
|
save((prev) => {
|
||||||
if (rule.pattern === "*") {
|
if (
|
||||||
return;
|
rule.pattern === "*" ||
|
||||||
}
|
prev.some((item) => item.pattern === rule.pattern)
|
||||||
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
) {
|
||||||
return;
|
return prev;
|
||||||
}
|
}
|
||||||
rules.unshift(rule);
|
return [rule, ...prev];
|
||||||
await updateRules(rules);
|
});
|
||||||
},
|
},
|
||||||
[list, updateRules]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const del = useCallback(
|
const del = useCallback(
|
||||||
async (pattern) => {
|
(pattern) => {
|
||||||
let rules = [...list];
|
save((prev) => {
|
||||||
if (pattern === "*") {
|
if (pattern === "*") {
|
||||||
return;
|
return prev;
|
||||||
}
|
}
|
||||||
rules = rules.filter((item) => item.pattern !== pattern);
|
return prev.filter((item) => item.pattern !== pattern);
|
||||||
await updateRules(rules);
|
});
|
||||||
},
|
},
|
||||||
[list, updateRules]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const clear = useCallback(async () => {
|
const clear = useCallback(() => {
|
||||||
let rules = [...list];
|
save((prev) => prev.filter((item) => item.pattern === "*"));
|
||||||
rules = rules.filter((item) => item.pattern === "*");
|
}, [save]);
|
||||||
await updateRules(rules);
|
|
||||||
}, [list, updateRules]);
|
|
||||||
|
|
||||||
const put = useCallback(
|
const put = useCallback(
|
||||||
async (pattern, obj) => {
|
(pattern, obj) => {
|
||||||
const rules = [...list];
|
save((prev) => {
|
||||||
if (pattern === "*") {
|
// if (pattern !== obj.pattern) {
|
||||||
obj.pattern = "*";
|
// return prev;
|
||||||
}
|
// }
|
||||||
const rule = rules.find((r) => r.pattern === pattern);
|
return prev.map((item) =>
|
||||||
rule && Object.assign(rule, obj);
|
item.pattern === pattern ? { ...item, ...obj } : item
|
||||||
await updateRules(rules);
|
);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[list, updateRules]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
const merge = useCallback(
|
const merge = useCallback(
|
||||||
async (newRules) => {
|
(rules) => {
|
||||||
const rules = [...list];
|
save((prev) => {
|
||||||
newRules = checkRules(newRules);
|
const adds = checkRules(rules);
|
||||||
newRules.forEach((newRule) => {
|
if (adds.length === 0) {
|
||||||
const rule = rules.find(
|
return prev;
|
||||||
(oldRule) => oldRule.pattern === newRule.pattern
|
|
||||||
);
|
|
||||||
if (rule) {
|
|
||||||
Object.assign(rule, newRule);
|
|
||||||
} else {
|
|
||||||
rules.unshift(newRule);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const map = new Map();
|
||||||
|
// // 不进行深度合并
|
||||||
|
// // [...prev, ...adds].forEach((item) => {
|
||||||
|
// // const k = item.pattern;
|
||||||
|
// // map.set(k, { ...(map.get(k) || {}), ...item });
|
||||||
|
// // });
|
||||||
|
// prev.forEach((item) => map.set(item.pattern, item));
|
||||||
|
// adds.forEach((item) => map.set(item.pattern, item));
|
||||||
|
// return [...map.values()];
|
||||||
|
|
||||||
|
const addsMap = new Map(adds.map((item) => [item.pattern, item]));
|
||||||
|
const prevPatterns = new Set(prev.map((item) => item.pattern));
|
||||||
|
const updatedPrev = prev.map(
|
||||||
|
(prevItem) => addsMap.get(prevItem.pattern) || prevItem
|
||||||
|
);
|
||||||
|
const newItems = adds.filter(
|
||||||
|
(addItem) => !prevPatterns.has(addItem.pattern)
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...newItems, ...updatedPrev];
|
||||||
});
|
});
|
||||||
await updateRules(rules);
|
|
||||||
},
|
},
|
||||||
[list, updateRules]
|
[save]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { list, add, del, clear, put, merge };
|
return { list, add, del, clear, put, merge };
|
||||||
|
|||||||
@@ -1,51 +1,113 @@
|
|||||||
import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import {
|
||||||
|
STOKEY_SETTING,
|
||||||
|
DEFAULT_SETTING,
|
||||||
|
KV_SETTING_KEY,
|
||||||
|
MSG_SET_LOGLEVEL,
|
||||||
|
} from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
import { trySyncSetting } from "../libs/sync";
|
import { debounceSyncMeta } from "../libs/storage";
|
||||||
import { createContext, useCallback, useContext, useMemo } from "react";
|
import Loading from "./Loading";
|
||||||
import { debounce } from "../libs/utils";
|
import { logger } from "../libs/log";
|
||||||
import { useSyncMeta } from "./Sync";
|
import { sendBgMsg } from "../libs/msg";
|
||||||
|
import { isExt } from "../libs/client";
|
||||||
|
|
||||||
const SettingContext = createContext({
|
const SettingContext = createContext({
|
||||||
setting: null,
|
setting: DEFAULT_SETTING,
|
||||||
updateSetting: async () => {},
|
updateSetting: () => {},
|
||||||
reloadSetting: async () => {},
|
reloadSetting: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export function SettingProvider({ children }) {
|
export function SettingProvider({ children, context }) {
|
||||||
const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
|
const isOptionsPage = useMemo(() => context === "options", [context]);
|
||||||
const { updateSyncMeta } = useSyncMeta();
|
|
||||||
|
|
||||||
const syncSetting = useMemo(
|
const {
|
||||||
() =>
|
data: setting,
|
||||||
debounce(() => {
|
isLoading,
|
||||||
trySyncSetting();
|
update,
|
||||||
}, [2000]),
|
reload,
|
||||||
[]
|
} = useStorage(STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY);
|
||||||
);
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof setting?.darkMode === "boolean") {
|
||||||
|
update((currentSetting) => ({
|
||||||
|
...currentSetting,
|
||||||
|
darkMode: currentSetting.darkMode ? "dark" : "light",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [setting?.darkMode, update]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOptionsPage) return;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
logger.setLevel(setting?.logLevel);
|
||||||
|
if (isExt) {
|
||||||
|
await sendBgMsg(MSG_SET_LOGLEVEL, setting?.logLevel);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to fetch log level, using default.", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [isOptionsPage, setting?.logLevel]);
|
||||||
|
|
||||||
const updateSetting = useCallback(
|
const updateSetting = useCallback(
|
||||||
async (obj) => {
|
(objOrFn) => {
|
||||||
await update(obj);
|
update(objOrFn);
|
||||||
await updateSyncMeta(KV_SETTING_KEY);
|
debounceSyncMeta(KV_SETTING_KEY);
|
||||||
syncSetting();
|
|
||||||
},
|
},
|
||||||
[update, syncSetting, updateSyncMeta]
|
[update]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data) {
|
const updateChild = useCallback(
|
||||||
return;
|
(key) => async (obj) => {
|
||||||
|
updateSetting((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: { ...(prev?.[key] || {}), ...obj },
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[updateSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
context,
|
||||||
|
setting,
|
||||||
|
updateSetting,
|
||||||
|
updateChild,
|
||||||
|
reloadSetting: reload,
|
||||||
|
}),
|
||||||
|
[context, setting, updateSetting, updateChild, reload]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return isOptionsPage ? <Loading /> : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setting) {
|
||||||
|
return isOptionsPage ? (
|
||||||
|
<center>
|
||||||
|
<Alert severity="error" sx={{ maxWidth: 600, margin: "60px auto" }}>
|
||||||
|
<p>数据加载出错,请刷新页面或卸载后重新安装。</p>
|
||||||
|
<p>
|
||||||
|
Data loading error, please refresh the page or uninstall and
|
||||||
|
reinstall.
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
</center>
|
||||||
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContext.Provider
|
<SettingContext.Provider value={value}>{children}</SettingContext.Provider>
|
||||||
value={{
|
|
||||||
setting: data,
|
|
||||||
updateSetting,
|
|
||||||
reloadSetting: reload,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SettingContext.Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ export function useShortcut(action) {
|
|||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateSetting } = useSetting();
|
||||||
const shortcuts = setting?.shortcuts || DEFAULT_SHORTCUTS;
|
const shortcuts = setting?.shortcuts || DEFAULT_SHORTCUTS;
|
||||||
const shortcut = shortcuts[action] || [];
|
const shortcut = shortcuts[action] || [];
|
||||||
|
|
||||||
const setShortcut = useCallback(
|
const setShortcut = useCallback(
|
||||||
async (val) => {
|
(val) => {
|
||||||
Object.assign(shortcuts, { [action]: val });
|
updateSetting((prev) => ({
|
||||||
await updateSetting({ shortcuts });
|
...prev,
|
||||||
|
shortcuts: { ...(prev?.shortcuts || {}), [action]: val },
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
[action, shortcuts, updateSetting]
|
[action, updateSetting]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { shortcut, setShortcut };
|
return { shortcut, setShortcut };
|
||||||
|
|||||||
@@ -1,69 +1,145 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { storage } from "../libs/storage";
|
import { storage } from "../libs/storage";
|
||||||
|
import { kissLog } from "../libs/log";
|
||||||
|
import { syncData } from "../libs/sync";
|
||||||
|
import { useDebouncedCallback } from "./DebouncedCallback";
|
||||||
|
import { isOptions } from "../libs/browser";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 用于将组件状态与 Storage 同步
|
||||||
*
|
*
|
||||||
* @param {*} key
|
* @param {string} key 用于在 Storage 中存取值的键
|
||||||
* @param {*} defaultVal 需为调用hook外的常量
|
* @param {*} defaultVal 默认值。建议在组件外定义为常量。
|
||||||
* @returns
|
* @param {string} [syncKey=""] 用于远端同步的可选键名
|
||||||
|
* @returns {{
|
||||||
|
* data: *,
|
||||||
|
* save: (valueOrFn: any | ((prevData: any) => any)) => void,
|
||||||
|
* update: (partialDataOrFn: object | ((prevData: object) => object)) => void,
|
||||||
|
* remove: () => Promise<void>,
|
||||||
|
* reload: () => Promise<void>
|
||||||
|
* }}
|
||||||
*/
|
*/
|
||||||
export function useStorage(key, defaultVal) {
|
export function useStorage(key, defaultVal = null, syncKey = "") {
|
||||||
const [loading, setLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(defaultVal);
|
||||||
|
|
||||||
const save = useCallback(
|
// 首次加载数据
|
||||||
async (val) => {
|
useEffect(() => {
|
||||||
setData(val);
|
let isMounted = true;
|
||||||
await storage.setObj(key, val);
|
|
||||||
},
|
|
||||||
[key]
|
|
||||||
);
|
|
||||||
|
|
||||||
const update = useCallback(
|
const loadInitialData = async () => {
|
||||||
async (obj) => {
|
try {
|
||||||
setData((pre = {}) => ({ ...pre, ...obj }));
|
const storedVal = await storage.getObj(key);
|
||||||
await storage.putObj(key, obj);
|
if (storedVal === undefined || storedVal === null) {
|
||||||
},
|
await storage.setObj(key, defaultVal);
|
||||||
[key]
|
} else if (isMounted) {
|
||||||
);
|
setData(storedVal);
|
||||||
|
}
|
||||||
const remove = useCallback(async () => {
|
} catch (err) {
|
||||||
setData(null);
|
kissLog(`storage load error for key: ${key}`, err);
|
||||||
await storage.del(key);
|
} finally {
|
||||||
}, [key]);
|
if (isMounted) {
|
||||||
|
setIsLoading(false);
|
||||||
const reload = useCallback(async () => {
|
}
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const val = await storage.getObj(key);
|
|
||||||
if (val) {
|
|
||||||
setData(val);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadInitialData();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [key, defaultVal]);
|
||||||
|
|
||||||
|
// 远端同步
|
||||||
|
const runSync = useCallback(async (keyToSync, valueToSync) => {
|
||||||
|
try {
|
||||||
|
const res = await syncData(keyToSync, valueToSync);
|
||||||
|
if (res?.isNew) {
|
||||||
|
setData(res.value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
kissLog("Sync failed", keyToSync);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const debouncedSync = useDebouncedCallback(runSync, 3000);
|
||||||
|
|
||||||
|
// 持久化
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.setObj(key, data).catch((err) => {
|
||||||
|
kissLog(`storage save error for key: ${key}`, err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 触发远端同步
|
||||||
|
if (syncKey && isOptions()) {
|
||||||
|
debouncedSync(syncKey, data);
|
||||||
|
}
|
||||||
|
}, [key, syncKey, isLoading, data, debouncedSync]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全量替换状态值
|
||||||
|
* @param {any | ((prevData: any) => any)} valueOrFn 新的值或一个返回新值的函数。
|
||||||
|
*/
|
||||||
|
const save = useCallback((valueOrFn) => {
|
||||||
|
// kissLog("save storage:", valueOrFn);
|
||||||
|
setData((prevData) =>
|
||||||
|
typeof valueOrFn === "function" ? valueOrFn(prevData) : valueOrFn
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并对象到当前状态(假设状态是一个对象)。
|
||||||
|
* @param {object | ((prevData: object) => object)} partialDataOrFn 要合并的对象或一个返回该对象的函数。
|
||||||
|
*/
|
||||||
|
const update = useCallback((partialDataOrFn) => {
|
||||||
|
// kissLog("update storage:", partialDataOrFn);
|
||||||
|
setData((prevData) => {
|
||||||
|
const partialData =
|
||||||
|
typeof partialDataOrFn === "function"
|
||||||
|
? partialDataOrFn(prevData)
|
||||||
|
: partialDataOrFn;
|
||||||
|
// 确保 preData 是一个对象,避免展开 null 或 undefined
|
||||||
|
const baseObj =
|
||||||
|
typeof prevData === "object" && prevData !== null ? prevData : {};
|
||||||
|
return { ...baseObj, ...partialData };
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Storage 中删除该值,并将状态重置为 null。
|
||||||
|
*/
|
||||||
|
const remove = useCallback(async () => {
|
||||||
|
// kissLog("remove storage:");
|
||||||
|
try {
|
||||||
|
await storage.del(key);
|
||||||
|
setData(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[storage reload]", err.message);
|
kissLog(`storage remove error for key: ${key}`, err);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
}, [key]);
|
}, [key]);
|
||||||
|
|
||||||
useEffect(() => {
|
/**
|
||||||
(async () => {
|
* 从 Storage 重新加载数据以覆盖当前状态。
|
||||||
try {
|
*/
|
||||||
setLoading(true);
|
const reload = useCallback(async () => {
|
||||||
const val = await storage.getObj(key);
|
// kissLog("reload storage:");
|
||||||
if (val) {
|
try {
|
||||||
setData(val);
|
const storedVal = await storage.getObj(key);
|
||||||
} else if (defaultVal) {
|
setData(storedVal ?? defaultVal);
|
||||||
setData(defaultVal);
|
} catch (err) {
|
||||||
await storage.setObj(key, defaultVal);
|
kissLog(`storage reload error for key: ${key}`, err);
|
||||||
}
|
// setData(defaultVal);
|
||||||
} catch (err) {
|
}
|
||||||
console.log("[storage load]", err.message);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [key, defaultVal]);
|
}, [key, defaultVal]);
|
||||||
|
|
||||||
return { data, save, update, remove, reload, loading };
|
return { data, save, update, remove, reload, isLoading };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
|
import { DEFAULT_SUBRULES_LIST } from "../config";
|
||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { loadOrFetchSubRules } from "../libs/subRules";
|
import { loadOrFetchSubRules } from "../libs/subRules";
|
||||||
import { delSubRules } from "../libs/storage";
|
import { kissLog } from "../libs/log";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订阅规则
|
* 订阅规则
|
||||||
@@ -18,50 +18,36 @@ export function useSubRules() {
|
|||||||
const selectedUrl = selectedSub.url;
|
const selectedUrl = selectedSub.url;
|
||||||
|
|
||||||
const selectSub = useCallback(
|
const selectSub = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
const subrulesList = [...list];
|
updateSetting((prev) => ({
|
||||||
subrulesList.forEach((item) => {
|
...prev,
|
||||||
if (item.url === url) {
|
subrulesList: prev.subrulesList.map((item) => ({
|
||||||
item.selected = true;
|
...item,
|
||||||
} else {
|
selected: item.url === url,
|
||||||
item.selected = false;
|
})),
|
||||||
}
|
}));
|
||||||
});
|
|
||||||
await updateSetting({ subrulesList });
|
|
||||||
},
|
},
|
||||||
[list, updateSetting]
|
[updateSetting]
|
||||||
);
|
|
||||||
|
|
||||||
const updateSub = useCallback(
|
|
||||||
async (url, obj) => {
|
|
||||||
const subrulesList = [...list];
|
|
||||||
subrulesList.forEach((item) => {
|
|
||||||
if (item.url === url) {
|
|
||||||
Object.assign(item, obj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await updateSetting({ subrulesList });
|
|
||||||
},
|
|
||||||
[list, updateSetting]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const addSub = useCallback(
|
const addSub = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
const subrulesList = [...list];
|
updateSetting((prev) => ({
|
||||||
subrulesList.push({ url, selected: false });
|
...prev,
|
||||||
await updateSetting({ subrulesList });
|
subrulesList: [...prev.subrulesList, { url, selected: false }],
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
[list, updateSetting]
|
[updateSetting]
|
||||||
);
|
);
|
||||||
|
|
||||||
const delSub = useCallback(
|
const delSub = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
let subrulesList = [...list];
|
updateSetting((prev) => ({
|
||||||
subrulesList = subrulesList.filter((item) => item.url !== url);
|
...prev,
|
||||||
await updateSetting({ subrulesList });
|
subrulesList: prev.subrulesList.filter((item) => item.url !== url),
|
||||||
await delSubRules(url);
|
}));
|
||||||
},
|
},
|
||||||
[list, updateSetting]
|
[updateSetting]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -72,7 +58,7 @@ export function useSubRules() {
|
|||||||
const rules = await loadOrFetchSubRules(selectedUrl);
|
const rules = await loadOrFetchSubRules(selectedUrl);
|
||||||
setSelectedRules(rules);
|
setSelectedRules(rules);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[loadOrFetchSubRules]", err);
|
kissLog("loadOrFetchSubRules", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -83,7 +69,6 @@ export function useSubRules() {
|
|||||||
return {
|
return {
|
||||||
subList: list,
|
subList: list,
|
||||||
selectSub,
|
selectSub,
|
||||||
updateSub,
|
|
||||||
addSub,
|
addSub,
|
||||||
delSub,
|
delSub,
|
||||||
selectedSub,
|
selectedSub,
|
||||||
@@ -93,21 +78,3 @@ export function useSubRules() {
|
|||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 覆写订阅规则
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function useOwSubRule() {
|
|
||||||
const { setting, updateSetting } = useSetting();
|
|
||||||
const { owSubrule = DEFAULT_OW_RULE } = setting;
|
|
||||||
|
|
||||||
const updateOwSubrule = useCallback(
|
|
||||||
async (obj) => {
|
|
||||||
await updateSetting({ owSubrule: { ...owSubrule, ...obj } });
|
|
||||||
},
|
|
||||||
[owSubrule, updateSetting]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { owSubrule, updateOwSubrule };
|
|
||||||
}
|
|
||||||
|
|||||||
10
src/hooks/Subtitle.js
Normal file
10
src/hooks/Subtitle.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { DEFAULT_SUBTITLE_SETTING } from "../config";
|
||||||
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
|
export function useSubtitle() {
|
||||||
|
const { setting, updateChild } = useSetting();
|
||||||
|
const subtitleSetting = setting?.subtitleSetting || DEFAULT_SUBTITLE_SETTING;
|
||||||
|
const updateSubtitle = updateChild("subtitleSetting");
|
||||||
|
|
||||||
|
return { subtitleSetting, updateSubtitle };
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
|
||||||
import { useStorage } from "./Storage";
|
import { useStorage } from "./Storage";
|
||||||
|
|
||||||
@@ -16,15 +16,24 @@ export function useSync() {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function useSyncMeta() {
|
export function useSyncMeta() {
|
||||||
const { sync, updateSync } = useSync();
|
const { updateSync } = useSync();
|
||||||
|
|
||||||
const updateSyncMeta = useCallback(
|
const updateSyncMeta = useCallback(
|
||||||
async (key) => {
|
(key) => {
|
||||||
const syncMeta = sync?.syncMeta || {};
|
updateSync((prevSync) => {
|
||||||
syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
|
const newSyncMeta = {
|
||||||
await updateSync({ syncMeta });
|
...(prevSync?.syncMeta || {}),
|
||||||
|
[key]: {
|
||||||
|
...(prevSync?.syncMeta?.[key] || {}),
|
||||||
|
updateAt: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return { syncMeta: newSyncMeta };
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[sync?.syncMeta, updateSync]
|
[updateSync]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { updateSyncMeta };
|
return { updateSyncMeta };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,25 +46,32 @@ export function useSyncCaches() {
|
|||||||
const { sync, updateSync, reloadSync } = useSync();
|
const { sync, updateSync, reloadSync } = useSync();
|
||||||
|
|
||||||
const updateDataCache = useCallback(
|
const updateDataCache = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
const dataCaches = sync?.dataCaches || {};
|
updateSync((prevSync) => ({
|
||||||
dataCaches[url] = Date.now();
|
dataCaches: {
|
||||||
await updateSync({ dataCaches });
|
...(prevSync?.dataCaches || {}),
|
||||||
|
[url]: Date.now(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
[sync, updateSync]
|
[updateSync]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteDataCache = useCallback(
|
const deleteDataCache = useCallback(
|
||||||
async (url) => {
|
(url) => {
|
||||||
const dataCaches = sync?.dataCaches || {};
|
updateSync((prevSync) => {
|
||||||
delete dataCaches[url];
|
const newDataCaches = { ...(prevSync?.dataCaches || {}) };
|
||||||
await updateSync({ dataCaches });
|
delete newDataCaches[url];
|
||||||
|
return { dataCaches: newDataCaches };
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[sync, updateSync]
|
[updateSync]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dataCaches = useMemo(() => sync?.dataCaches || {}, [sync?.dataCaches]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataCaches: sync?.dataCaches || {},
|
dataCaches,
|
||||||
updateDataCache,
|
updateDataCache,
|
||||||
deleteDataCache,
|
deleteDataCache,
|
||||||
reloadSync,
|
reloadSync,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||||
import { useDarkMode } from "./ColorMode";
|
import { useDarkMode } from "./ColorMode";
|
||||||
import { THEME_DARK, THEME_LIGHT } from "../config";
|
import { THEME_DARK, THEME_LIGHT } from "../config";
|
||||||
|
|
||||||
@@ -9,21 +9,51 @@ import { THEME_DARK, THEME_LIGHT } from "../config";
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function Theme({ children, options }) {
|
export default function Theme({ children, options = {}, styles = {} }) {
|
||||||
const { darkMode } = useDarkMode();
|
const { darkMode } = useDarkMode();
|
||||||
|
const [systemMode, setSystemMode] = useState(THEME_LIGHT);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window.matchMedia !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const handleChange = () => {
|
||||||
|
setSystemMode(mediaQuery.matches ? THEME_DARK : THEME_LIGHT);
|
||||||
|
};
|
||||||
|
handleChange(); // Set initial value
|
||||||
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
|
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => {
|
||||||
|
let htmlFontSize = 16;
|
||||||
|
try {
|
||||||
|
const s = window.getComputedStyle(document.documentElement).fontSize;
|
||||||
|
htmlFontSize = parseInt(s.replace("px", ""));
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDarkMode =
|
||||||
|
darkMode === "dark" || (darkMode === "auto" && systemMode === THEME_DARK);
|
||||||
|
|
||||||
return createTheme({
|
return createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
mode: darkMode ? THEME_DARK : THEME_LIGHT,
|
mode: isDarkMode ? THEME_DARK : THEME_LIGHT,
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
htmlFontSize,
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}, [darkMode, options]);
|
}, [darkMode, options, systemMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
<GlobalStyles styles={styles} />
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { DEFAULT_TRANBOX_SETTING } from "../config";
|
import { DEFAULT_TRANBOX_SETTING } from "../config";
|
||||||
import { useSetting } from "./Setting";
|
import { useSetting } from "./Setting";
|
||||||
|
|
||||||
export function useTranbox() {
|
export function useTranbox() {
|
||||||
const { setting, updateSetting } = useSetting();
|
const { setting, updateChild } = useSetting();
|
||||||
const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
|
const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
|
||||||
|
const updateTranbox = updateChild("tranboxSetting");
|
||||||
const updateTranbox = useCallback(
|
|
||||||
async (obj) => {
|
|
||||||
Object.assign(tranboxSetting, obj);
|
|
||||||
await updateSetting({ tranboxSetting });
|
|
||||||
},
|
|
||||||
[tranboxSetting, updateSetting]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { tranboxSetting, updateTranbox };
|
return { tranboxSetting, updateTranbox };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { tryDetectLang } from "../libs";
|
|
||||||
import { apiTranslate } from "../apis";
|
|
||||||
import { DEFAULT_TRANS_APIS } from "../config";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 翻译hook
|
|
||||||
* @param {*} q
|
|
||||||
* @param {*} rule
|
|
||||||
* @param {*} setting
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function useTranslate(q, rule, setting) {
|
|
||||||
const [text, setText] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [sameLang, setSamelang] = useState(false);
|
|
||||||
|
|
||||||
const { translator, fromLang, toLang } = rule;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const deLang = await tryDetectLang(q, setting.detectRemote);
|
|
||||||
if (deLang && toLang.includes(deLang)) {
|
|
||||||
setSamelang(true);
|
|
||||||
} else {
|
|
||||||
const [trText, isSame] = await apiTranslate({
|
|
||||||
translator,
|
|
||||||
text: q,
|
|
||||||
fromLang,
|
|
||||||
toLang,
|
|
||||||
apiSetting:
|
|
||||||
setting.transApis?.[translator] || DEFAULT_TRANS_APIS[translator],
|
|
||||||
});
|
|
||||||
setText(trText);
|
|
||||||
setSamelang(isSame);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[translate]", err);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [q, translator, fromLang, toLang, setting]);
|
|
||||||
|
|
||||||
return { text, sameLang, loading };
|
|
||||||
}
|
|
||||||
61
src/hooks/ValidationInput.js
Normal file
61
src/hooks/ValidationInput.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { limitNumber, limitFloat } from "../libs/utils";
|
||||||
|
|
||||||
|
function ValidationInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
name,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
isFloat = false,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const [localValue, setLocalValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalValue(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleLocalChange = (e) => {
|
||||||
|
setLocalValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
const numValue = Number(localValue);
|
||||||
|
|
||||||
|
if (isNaN(numValue)) {
|
||||||
|
setLocalValue(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatedValue = isFloat
|
||||||
|
? limitFloat(numValue, min, max)
|
||||||
|
: limitNumber(numValue, min, max);
|
||||||
|
|
||||||
|
if (validatedValue !== numValue) {
|
||||||
|
setLocalValue(validatedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
target: {
|
||||||
|
name: name,
|
||||||
|
value: validatedValue,
|
||||||
|
},
|
||||||
|
preventDefault: () => {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
{...props}
|
||||||
|
type="number"
|
||||||
|
name={name}
|
||||||
|
value={localValue}
|
||||||
|
onChange={handleLocalChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ValidationInput;
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { STOKEY_WFRULES, KV_WFRULES_KEY } from "../config";
|
|
||||||
import { useStorage } from "./Storage";
|
|
||||||
import { trySyncWebfixRules } from "../libs/sync";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { useSyncMeta } from "./Sync";
|
|
||||||
|
|
||||||
const DEFAULT_WFRULES = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修复规则 hook
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function useWebfixRules() {
|
|
||||||
const { data: list, save } = useStorage(STOKEY_WFRULES, DEFAULT_WFRULES);
|
|
||||||
const { updateSyncMeta } = useSyncMeta();
|
|
||||||
|
|
||||||
const updateRules = useCallback(
|
|
||||||
async (rules) => {
|
|
||||||
await save(rules);
|
|
||||||
await updateSyncMeta(KV_WFRULES_KEY);
|
|
||||||
trySyncWebfixRules();
|
|
||||||
},
|
|
||||||
[save, updateSyncMeta]
|
|
||||||
);
|
|
||||||
|
|
||||||
const add = useCallback(
|
|
||||||
async (rule) => {
|
|
||||||
const rules = [...list];
|
|
||||||
if (rules.map((item) => item.pattern).includes(rule.pattern)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rules.unshift(rule);
|
|
||||||
await updateRules(rules);
|
|
||||||
},
|
|
||||||
[list, updateRules]
|
|
||||||
);
|
|
||||||
|
|
||||||
const del = useCallback(
|
|
||||||
async (pattern) => {
|
|
||||||
let rules = [...list];
|
|
||||||
rules = rules.filter((item) => item.pattern !== pattern);
|
|
||||||
await updateRules(rules);
|
|
||||||
},
|
|
||||||
[list, updateRules]
|
|
||||||
);
|
|
||||||
|
|
||||||
const put = useCallback(
|
|
||||||
async (pattern, obj) => {
|
|
||||||
const rules = [...list];
|
|
||||||
const rule = rules.find((r) => r.pattern === pattern);
|
|
||||||
rule && Object.assign(rule, obj);
|
|
||||||
await updateRules(rules);
|
|
||||||
},
|
|
||||||
[list, updateRules]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { list, add, del, put };
|
|
||||||
}
|
|
||||||
29
src/hooks/WindowSize.js
Normal file
29
src/hooks/WindowSize.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useDebouncedCallback } from "./DebouncedCallback";
|
||||||
|
|
||||||
|
function useWindowSize() {
|
||||||
|
const [windowSize, setWindowSize] = useState({
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
const debounceWindowResize = useDebouncedCallback(() => {
|
||||||
|
setWindowSize({
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debounceWindowResize();
|
||||||
|
|
||||||
|
window.addEventListener("resize", debounceWindowResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", debounceWindowResize);
|
||||||
|
};
|
||||||
|
}, [debounceWindowResize]);
|
||||||
|
|
||||||
|
return windowSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useWindowSize;
|
||||||
16
src/index.js
16
src/index.js
@@ -7,14 +7,15 @@ import Paper from "@mui/material/Paper";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import { useFetch } from "./hooks/Fetch";
|
import { useGet } from "./hooks/Fetch";
|
||||||
import { I18N, URL_RAW_PREFIX } from "./config";
|
import { I18N, URL_RAW_PREFIX } from "./config";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [lang, setLang] = useState("zh");
|
const [lang, setLang] = useState("zh");
|
||||||
const [data, loading, error] = useFetch(
|
const { data, loading, error } = useGet(
|
||||||
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
|
`${URL_RAW_PREFIX}/${I18N?.["about_md"]?.[lang]}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ padding: 2, margin: 2 }}>
|
<Paper sx={{ padding: 2, margin: 2 }}>
|
||||||
<Stack spacing={2} direction="row" justifyContent="flex-end">
|
<Stack spacing={2} direction="row" justifyContent="flex-end">
|
||||||
@@ -36,19 +37,10 @@ function App() {
|
|||||||
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
<Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL}>
|
||||||
Install/Update Userscript for Tampermonkey/Violentmonkey
|
Install/Update Userscript for Tampermonkey/Violentmonkey
|
||||||
</Link>
|
</Link>
|
||||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_DOWNLOADURL2}>
|
|
||||||
Install/Update Userscript for Tampermonkey/Violentmonkey 2
|
|
||||||
</Link> */}
|
|
||||||
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
<Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL}>
|
||||||
Install/Update Userscript for iOS Safari
|
Install/Update Userscript for iOS Safari
|
||||||
</Link>
|
</Link>
|
||||||
{/* <Link href={process.env.REACT_APP_USERSCRIPT_IOS_DOWNLOADURL2}>
|
|
||||||
Install/Update Userscript for iOS Safari 2
|
|
||||||
</Link> */}
|
|
||||||
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
|
<Link href={process.env.REACT_APP_OPTIONSPAGE}>Open Options Page</Link>
|
||||||
{/* <Link href={process.env.REACT_APP_OPTIONSPAGE2}>
|
|
||||||
Open Options Page 2
|
|
||||||
</Link> */}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -56,7 +48,7 @@ function App() {
|
|||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</center>
|
</center>
|
||||||
) : (
|
) : (
|
||||||
<ReactMarkdown children={error ? error.message : data} />
|
<ReactMarkdown children={error || data} />
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|||||||
3
src/injector-shadowroot.js
Normal file
3
src/injector-shadowroot.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { shadowRootInjector } from "./injectors/shadowroot";
|
||||||
|
|
||||||
|
shadowRootInjector();
|
||||||
3
src/injector-subtitle.js
Normal file
3
src/injector-subtitle.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { XMLHttpRequestInjector } from "./injectors/xmlhttp";
|
||||||
|
|
||||||
|
XMLHttpRequestInjector();
|
||||||
27
src/injectors/index.js
Normal file
27
src/injectors/index.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { browser } from "../libs/browser";
|
||||||
|
import { isExt } from "../libs/client";
|
||||||
|
import { injectExternalJs, injectInlineJs } from "../libs/injector";
|
||||||
|
import { shadowRootInjector } from "./shadowroot";
|
||||||
|
import { XMLHttpRequestInjector } from "./xmlhttp";
|
||||||
|
|
||||||
|
export const INJECTOR = {
|
||||||
|
subtitle: "injector-subtitle.js",
|
||||||
|
shadowroot: "injector-shadowroot.js",
|
||||||
|
};
|
||||||
|
|
||||||
|
const injectorMap = {
|
||||||
|
[INJECTOR.subtitle]: XMLHttpRequestInjector,
|
||||||
|
[INJECTOR.shadowroot]: shadowRootInjector,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function injectJs(name, id = "kiss-translator-inject-js") {
|
||||||
|
const injector = injectorMap[name];
|
||||||
|
if (!injector) return;
|
||||||
|
|
||||||
|
if (isExt) {
|
||||||
|
const src = browser.runtime.getURL(name);
|
||||||
|
injectExternalJs(src, id);
|
||||||
|
} else {
|
||||||
|
injectInlineJs(`(${injector})()`, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/injectors/shadowroot.js
Normal file
12
src/injectors/shadowroot.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const shadowRootInjector = () => {
|
||||||
|
try {
|
||||||
|
const orig = Element.prototype.attachShadow;
|
||||||
|
Element.prototype.attachShadow = function (...args) {
|
||||||
|
const root = orig.apply(this, args);
|
||||||
|
window.postMessage({ type: "KISS_SHADOW_ROOT_CREATED" }, "*");
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log("shadowRootInjector", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
23
src/injectors/xmlhttp.js
Normal file
23
src/injectors/xmlhttp.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export const XMLHttpRequestInjector = () => {
|
||||||
|
try {
|
||||||
|
const originalOpen = XMLHttpRequest.prototype.open;
|
||||||
|
XMLHttpRequest.prototype.open = function (...args) {
|
||||||
|
const url = args[1];
|
||||||
|
if (typeof url === "string" && url.includes("timedtext")) {
|
||||||
|
this.addEventListener("load", function () {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: "KISS_XHR_DATA_YOUTUBE",
|
||||||
|
url: this.responseURL,
|
||||||
|
response: this.responseText,
|
||||||
|
},
|
||||||
|
window.location.origin
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return originalOpen.apply(this, args);
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log("XMLHttpRequestInjector", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { getMsauth, setMsauth } from "./storage";
|
import { getMsauth, setMsauth } from "./storage";
|
||||||
import { URL_MICROSOFT_AUTH } from "../config";
|
import { kissLog } from "./log";
|
||||||
import { fetchData } from "./fetch";
|
import { apiMsAuth } from "../apis";
|
||||||
|
|
||||||
const parseMSToken = (token) => {
|
const parseMSToken = (token) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(atob(token.split(".")[1])).exp;
|
return JSON.parse(atob(token.split(".")[1])).exp;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("[parseMSToken]", err);
|
kissLog("parseMSToken", err);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
@@ -16,28 +16,55 @@ const parseMSToken = (token) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const _msAuth = () => {
|
const _msAuth = () => {
|
||||||
let { token, exp } = {};
|
let tokenPromise = null;
|
||||||
|
const EXPIRATION_MS = 1000;
|
||||||
|
|
||||||
|
const fetchNewToken = async () => {
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 1. 查询storage缓存
|
||||||
|
const storageToken = await getMsauth();
|
||||||
|
if (storageToken) {
|
||||||
|
const storageExp = parseMSToken(storageToken);
|
||||||
|
const storageExpiresAt = storageExp * 1000;
|
||||||
|
if (storageExpiresAt > now + EXPIRATION_MS) {
|
||||||
|
return { token: storageToken, expiresAt: storageExpiresAt };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 缓存没有或失效,查询接口
|
||||||
|
const apiToken = await apiMsAuth();
|
||||||
|
if (!apiToken) {
|
||||||
|
throw new Error("Failed to fetch ms token");
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiExp = parseMSToken(apiToken);
|
||||||
|
const apiExpiresAt = apiExp * 1000;
|
||||||
|
await setMsauth(apiToken);
|
||||||
|
return { token: apiToken, expiresAt: apiExpiresAt };
|
||||||
|
} catch (error) {
|
||||||
|
kissLog("get msauth failed", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
// 查询内存缓存
|
// 检查是否有缓存的 Promise
|
||||||
const now = Date.now();
|
if (tokenPromise) {
|
||||||
if (token && exp * 1000 > now + 1000) {
|
try {
|
||||||
return [token, exp];
|
const cachedResult = await tokenPromise;
|
||||||
|
if (cachedResult.expiresAt > Date.now() + EXPIRATION_MS) {
|
||||||
|
return cachedResult.token;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询storage缓存
|
tokenPromise = fetchNewToken();
|
||||||
const res = await getMsauth();
|
const result = await tokenPromise;
|
||||||
token = res?.token;
|
return result.token;
|
||||||
exp = res?.exp;
|
|
||||||
if (token && exp * 1000 > now + 1000) {
|
|
||||||
return [token, exp];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存没有或失效,查询接口
|
|
||||||
token = await fetchData(URL_MICROSOFT_AUTH);
|
|
||||||
exp = parseMSToken(token);
|
|
||||||
await setMsauth({ token, exp });
|
|
||||||
return [token, exp];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
152
src/libs/batchQueue.js
Normal file
152
src/libs/batchQueue.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_BATCH_INTERVAL,
|
||||||
|
DEFAULT_BATCH_SIZE,
|
||||||
|
DEFAULT_BATCH_LENGTH,
|
||||||
|
} from "../config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批处理队列
|
||||||
|
* @param {*} args
|
||||||
|
* @param {*} param1
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const BatchQueue = (
|
||||||
|
taskFn,
|
||||||
|
{
|
||||||
|
batchInterval = DEFAULT_BATCH_INTERVAL,
|
||||||
|
batchSize = DEFAULT_BATCH_SIZE,
|
||||||
|
batchLength = DEFAULT_BATCH_LENGTH,
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
|
const queue = [];
|
||||||
|
let isProcessing = false;
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
const sendBatchRequest = async (payloads, batchArgs) => {
|
||||||
|
return taskFn(payloads, batchArgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processQueue = async () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queue.length === 0 || isProcessing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing = true;
|
||||||
|
|
||||||
|
let tasksToProcess = [];
|
||||||
|
let currentBatchLength = 0;
|
||||||
|
let endIndex = 0;
|
||||||
|
|
||||||
|
for (const task of queue) {
|
||||||
|
const textLength = task.payload?.length || 0;
|
||||||
|
if (
|
||||||
|
endIndex >= batchSize ||
|
||||||
|
(currentBatchLength + textLength > batchLength && endIndex > 0)
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentBatchLength += textLength;
|
||||||
|
endIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex > 0) {
|
||||||
|
tasksToProcess = queue.splice(0, endIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasksToProcess.length === 0) {
|
||||||
|
isProcessing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payloads = tasksToProcess.map((item) => item.payload);
|
||||||
|
const batchArgs = tasksToProcess[0].args;
|
||||||
|
const responses = await sendBatchRequest(payloads, batchArgs);
|
||||||
|
if (!Array.isArray(responses)) {
|
||||||
|
throw new Error("responses format error");
|
||||||
|
}
|
||||||
|
|
||||||
|
tasksToProcess.forEach((taskItem, index) => {
|
||||||
|
const response = responses[index];
|
||||||
|
if (response) {
|
||||||
|
taskItem.resolve(response);
|
||||||
|
} else {
|
||||||
|
taskItem.reject(new Error(`No response for item at index ${index}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
tasksToProcess.forEach((taskItem) => taskItem.reject(error));
|
||||||
|
} finally {
|
||||||
|
isProcessing = false;
|
||||||
|
if (queue.length > 0) {
|
||||||
|
if (queue.length >= batchSize) {
|
||||||
|
setTimeout(processQueue, 0);
|
||||||
|
} else {
|
||||||
|
scheduleProcessing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleProcessing = () => {
|
||||||
|
if (!isProcessing && !timer && queue.length > 0) {
|
||||||
|
timer = setTimeout(processQueue, batchInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addTask = (data, args) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const payload = data;
|
||||||
|
queue.push({ payload, resolve, reject, args });
|
||||||
|
|
||||||
|
if (queue.length >= batchSize) {
|
||||||
|
processQueue();
|
||||||
|
} else {
|
||||||
|
scheduleProcessing();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
queue.forEach((task) =>
|
||||||
|
task.reject(new Error("Queue instance was destroyed."))
|
||||||
|
);
|
||||||
|
queue.length = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { addTask, destroy };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 实例字典
|
||||||
|
const queueMap = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取批处理实例
|
||||||
|
*/
|
||||||
|
export const getBatchQueue = (key, taskFn, options) => {
|
||||||
|
if (queueMap.has(key)) {
|
||||||
|
return queueMap.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = BatchQueue(taskFn, options);
|
||||||
|
queueMap.set(key, queue);
|
||||||
|
return queue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有任务
|
||||||
|
*/
|
||||||
|
export const clearAllBatchQueue = () => {
|
||||||
|
for (const queue of queueMap.values()) {
|
||||||
|
queue.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
10
src/libs/blacklist.js
Normal file
10
src/libs/blacklist.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { isMatch } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否在黑名单中
|
||||||
|
* @param {*} href
|
||||||
|
* @param {*} param1
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const isInBlacklist = (href, { blacklist }) =>
|
||||||
|
blacklist.split(/\n|,/).some((url) => isMatch(href, url.trim()));
|
||||||
@@ -8,10 +8,36 @@ function _browser() {
|
|||||||
try {
|
try {
|
||||||
return require("webextension-polyfill");
|
return require("webextension-polyfill");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.log("[browser]", err.message);
|
// kissLog("browser", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const browser = _browser();
|
export const browser = _browser();
|
||||||
|
|
||||||
export const isBg = () => globalThis?.ContextType === "BACKGROUND";
|
export const getContext = () => {
|
||||||
|
const context = globalThis.__KISS_CONTEXT__;
|
||||||
|
if (context) return context;
|
||||||
|
|
||||||
|
// if (typeof window === "undefined" || typeof document === "undefined") {
|
||||||
|
// return "background";
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const extensionOrigin = browser.runtime.getURL("");
|
||||||
|
// if (!window.location.href.startsWith(extensionOrigin)) {
|
||||||
|
// return "content";
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const pathname = window.location.pathname;
|
||||||
|
// if (pathname.includes("popup")) return "popup";
|
||||||
|
// if (pathname.includes("options")) return "options";
|
||||||
|
// if (pathname.includes("sidepanel")) return "sidepanel";
|
||||||
|
// if (pathname.includes("background")) return "background";
|
||||||
|
|
||||||
|
return "undefined";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isBg = () => getContext() === "background";
|
||||||
|
export const isOptions = () => getContext() === "options";
|
||||||
|
|
||||||
|
export const isBuiltinAIAvailable =
|
||||||
|
"LanguageDetector" in globalThis && "Translator" in globalThis;
|
||||||
|
|||||||
168
src/libs/builtinAI.js
Normal file
168
src/libs/builtinAI.js
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { kissLog, logger } from "./log";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chrome 浏览器内置翻译
|
||||||
|
*/
|
||||||
|
class ChromeTranslator {
|
||||||
|
#translatorMap = new Map();
|
||||||
|
#detectorPromise = null;
|
||||||
|
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.onProgress = options.onProgress || this.#defaultProgressHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
#defaultProgressHandler(type, progress) {
|
||||||
|
kissLog(`Downloading ${type} model: ${progress}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
#getDetectorPromise() {
|
||||||
|
if (!this.#detectorPromise) {
|
||||||
|
this.#detectorPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const availability = await LanguageDetector.availability();
|
||||||
|
if (availability === "unavailable") {
|
||||||
|
throw new Error("LanguageDetector unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await LanguageDetector.create({
|
||||||
|
monitor: (m) => this._monitorProgress(m, "detector"),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.#detectorPromise = null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#detectorPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createTranslator(sourceLanguage, targetLanguage) {
|
||||||
|
const key = `${sourceLanguage}_${targetLanguage}`;
|
||||||
|
if (this.#translatorMap.has(key)) {
|
||||||
|
return this.#translatorMap.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const translatorPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const avail = await Translator.availability({
|
||||||
|
sourceLanguage,
|
||||||
|
targetLanguage,
|
||||||
|
});
|
||||||
|
if (avail === "unavailable") {
|
||||||
|
throw new Error(
|
||||||
|
`Translator ${sourceLanguage}_${targetLanguage} unavailable`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const translator = await Translator.create({
|
||||||
|
sourceLanguage,
|
||||||
|
targetLanguage,
|
||||||
|
monitor: (m) => this._monitorProgress(m, `translator (${key})`),
|
||||||
|
});
|
||||||
|
this.#translatorMap.set(key, translator);
|
||||||
|
|
||||||
|
return translator;
|
||||||
|
} catch (error) {
|
||||||
|
this.#translatorMap.delete(key);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
this.#translatorMap.set(key, translatorPromise);
|
||||||
|
return translatorPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
_monitorProgress(monitorable, type) {
|
||||||
|
monitorable.addEventListener("downloadprogress", (e) => {
|
||||||
|
const progress = e.total > 0 ? Math.round((e.loaded / e.total) * 100) : 0;
|
||||||
|
this.onProgress(type, progress);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async detectLanguage(text, confidenceThreshold = 0.4) {
|
||||||
|
if (!text) {
|
||||||
|
return ["", "Input text cannot be empty."];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detector = await this.#getDetectorPromise();
|
||||||
|
const results = await detector.detect(text);
|
||||||
|
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
return ["", "No language could be detected."];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { detectedLanguage, confidence } = results[0];
|
||||||
|
if (confidence < confidenceThreshold) {
|
||||||
|
return [
|
||||||
|
"",
|
||||||
|
`Confidence of test results (${detectedLanguage} ${confidence.toFixed(
|
||||||
|
2
|
||||||
|
)}) below the set threshold ${confidenceThreshold}。`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [detectedLanguage, ""];
|
||||||
|
} catch (error) {
|
||||||
|
kissLog("detectLanguage", error, `(${text})`);
|
||||||
|
return ["", error.message];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async translateText(text, targetLanguage, sourceLanguage = "auto") {
|
||||||
|
if (!text || !targetLanguage || typeof text !== "string") {
|
||||||
|
return ["", sourceLanguage, "Input text cannot be empty."];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let finalSourceLanguage = sourceLanguage;
|
||||||
|
if (sourceLanguage === "auto") {
|
||||||
|
const [detectedLanguage, detectionError] =
|
||||||
|
await this.detectLanguage(text);
|
||||||
|
if (detectionError || !detectedLanguage) {
|
||||||
|
const reason =
|
||||||
|
detectionError || "Unable to determine source language.";
|
||||||
|
return [
|
||||||
|
"",
|
||||||
|
finalSourceLanguage,
|
||||||
|
`Automatic detection of source language failed: ${reason}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
finalSourceLanguage = detectedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalSourceLanguage === targetLanguage) {
|
||||||
|
return ["", finalSourceLanguage, "Same lang"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const translator = await this.#createTranslator(
|
||||||
|
finalSourceLanguage,
|
||||||
|
targetLanguage
|
||||||
|
);
|
||||||
|
const translatedText = await translator.translate(text);
|
||||||
|
|
||||||
|
return [translatedText, finalSourceLanguage, ""];
|
||||||
|
} catch (error) {
|
||||||
|
kissLog("translateText", error, `(${text})`);
|
||||||
|
|
||||||
|
if (
|
||||||
|
error &&
|
||||||
|
error.message &&
|
||||||
|
error.message.includes("Other generic failures occurred")
|
||||||
|
) {
|
||||||
|
logger.info("Generic failure detected, resetting translator cache.");
|
||||||
|
this.#translatorMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ["", sourceLanguage, error.message];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chromeTranslator = new ChromeTranslator();
|
||||||
|
|
||||||
|
export const chromeDetect = (args) =>
|
||||||
|
chromeTranslator.detectLanguage(args.text);
|
||||||
|
export const chromeTranslate = (args) =>
|
||||||
|
chromeTranslator.translateText(args.text, args.to, args.from);
|
||||||
183
src/libs/cache.js
Normal file
183
src/libs/cache.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import {
|
||||||
|
CACHE_NAME,
|
||||||
|
DEFAULT_CACHE_TIMEOUT,
|
||||||
|
MSG_CLEAR_CACHES,
|
||||||
|
MSG_GET_HTTPCACHE,
|
||||||
|
MSG_PUT_HTTPCACHE,
|
||||||
|
} from "../config";
|
||||||
|
import { kissLog } from "./log";
|
||||||
|
import { isExt } from "./client";
|
||||||
|
import { isBg } from "./browser";
|
||||||
|
import { sendBgMsg } from "./msg";
|
||||||
|
import { blobToBase64 } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存数据
|
||||||
|
*/
|
||||||
|
export const tryClearCaches = async () => {
|
||||||
|
try {
|
||||||
|
if (isExt && !isBg()) {
|
||||||
|
await sendBgMsg(MSG_CLEAR_CACHES);
|
||||||
|
} else {
|
||||||
|
await caches.delete(CACHE_NAME);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("clean caches", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造缓存 request
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} init
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const newCacheReq = async (input, init) => {
|
||||||
|
let request = new Request(input, init);
|
||||||
|
if (request.method !== "GET") {
|
||||||
|
const body = await request.text();
|
||||||
|
const cacheUrl = new URL(request.url);
|
||||||
|
cacheUrl.pathname += body;
|
||||||
|
request = new Request(cacheUrl.toString(), { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 caches
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} init
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getHttpCache = async ({ input, init, expect }) => {
|
||||||
|
try {
|
||||||
|
const request = await newCacheReq(input, init);
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const response = await cache.match(request);
|
||||||
|
if (response) {
|
||||||
|
const res = await parseResponse(response, expect);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("get cache", err);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入 caches
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} init
|
||||||
|
* @param {*} data
|
||||||
|
*/
|
||||||
|
export const putHttpCache = async ({
|
||||||
|
input,
|
||||||
|
init,
|
||||||
|
data,
|
||||||
|
maxAge = DEFAULT_CACHE_TIMEOUT, // todo: 从设置里面读取最大缓存时间
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const req = await newCacheReq(input, init);
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const res = new Response(JSON.stringify(data), {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": `max-age=${maxAge}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// res.headers.set("Cache-Control", `max-age=${maxAge}`);
|
||||||
|
await cache.put(req, res);
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("put cache", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 response
|
||||||
|
* @param {*} res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const parseResponse = async (res, expect = null) => {
|
||||||
|
if (!res) {
|
||||||
|
throw new Error("Response object does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg = {
|
||||||
|
url: res.url,
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errorText = await res.clone().text();
|
||||||
|
try {
|
||||||
|
msg.response = JSON.parse(errorText);
|
||||||
|
} catch {
|
||||||
|
msg.response = errorText;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
msg.response = "Unable to read error body";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = res.headers.get("Content-Type") || "";
|
||||||
|
if (expect === "blob") return res.blob();
|
||||||
|
if (expect === "text") return res.text();
|
||||||
|
if (expect === "json") return res.json();
|
||||||
|
if (
|
||||||
|
expect === "audio" ||
|
||||||
|
contentType.includes("audio") ||
|
||||||
|
contentType.includes("image") ||
|
||||||
|
contentType.includes("video")
|
||||||
|
) {
|
||||||
|
const blob = await res.blob();
|
||||||
|
return blobToBase64(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
if (!text) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch (err) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getHttpCache 兼容性封装
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} init
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getHttpCachePolyfill = (input, init) => {
|
||||||
|
// 插件
|
||||||
|
if (isExt && !isBg()) {
|
||||||
|
return sendBgMsg(MSG_GET_HTTPCACHE, { input, init });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 油猴/网页/BackgroundPage
|
||||||
|
return getHttpCache({ input, init });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* putHttpCache 兼容性封装
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} init
|
||||||
|
* @param {*} data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const putHttpCachePolyfill = (input, init, data) => {
|
||||||
|
// 插件
|
||||||
|
if (isExt && !isBg()) {
|
||||||
|
return sendBgMsg(MSG_PUT_HTTPCACHE, { input, init, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 油猴/网页/BackgroundPage
|
||||||
|
return putHttpCache({ input, init, data });
|
||||||
|
};
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import { CLIENT_EXTS, CLIENT_USERSCRIPT, CLIENT_WEB } from "../config";
|
import {
|
||||||
|
CLIENT_EXTS,
|
||||||
|
CLIENT_USERSCRIPT,
|
||||||
|
CLIENT_WEB,
|
||||||
|
CLIENT_FIREFOX,
|
||||||
|
} from "../config";
|
||||||
|
|
||||||
export const client = process.env.REACT_APP_CLIENT;
|
export const client = process.env.REACT_APP_CLIENT;
|
||||||
export const isExt = CLIENT_EXTS.includes(client);
|
export const isExt = CLIENT_EXTS.includes(client);
|
||||||
export const isGm = client === CLIENT_USERSCRIPT;
|
export const isGm = client === CLIENT_USERSCRIPT;
|
||||||
export const isWeb = client === CLIENT_WEB;
|
export const isWeb = client === CLIENT_WEB;
|
||||||
|
export const isFirefox = client === CLIENT_FIREFOX;
|
||||||
|
|||||||
65
src/libs/detect.js
Normal file
65
src/libs/detect.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
OPT_TRANS_GOOGLE,
|
||||||
|
OPT_TRANS_MICROSOFT,
|
||||||
|
OPT_TRANS_BAIDU,
|
||||||
|
OPT_TRANS_TENCENT,
|
||||||
|
OPT_LANGS_TO_CODE,
|
||||||
|
OPT_LANGS_MAP,
|
||||||
|
OPT_TRANS_BUILTINAI,
|
||||||
|
OPT_LANGDETECTOR_MAP,
|
||||||
|
} from "../config";
|
||||||
|
import { browser } from "./browser";
|
||||||
|
import {
|
||||||
|
apiGoogleLangdetect,
|
||||||
|
apiMicrosoftLangdetect,
|
||||||
|
apiBaiduLangdetect,
|
||||||
|
apiTencentLangdetect,
|
||||||
|
apiBuiltinAIDetect,
|
||||||
|
} from "../apis";
|
||||||
|
import { kissLog } from "./log";
|
||||||
|
|
||||||
|
const langdetectFns = {
|
||||||
|
[OPT_TRANS_GOOGLE]: apiGoogleLangdetect,
|
||||||
|
[OPT_TRANS_MICROSOFT]: apiMicrosoftLangdetect,
|
||||||
|
[OPT_TRANS_BAIDU]: apiBaiduLangdetect,
|
||||||
|
[OPT_TRANS_TENCENT]: apiTencentLangdetect,
|
||||||
|
[OPT_TRANS_BUILTINAI]: apiBuiltinAIDetect,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语言识别
|
||||||
|
* @param {*} text
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const tryDetectLang = async (text, langDetector = "-") => {
|
||||||
|
let deLang = "";
|
||||||
|
|
||||||
|
// 内置AI/远程识别
|
||||||
|
if (OPT_LANGDETECTOR_MAP.has(langDetector)) {
|
||||||
|
try {
|
||||||
|
const lang = await langdetectFns[langDetector](text);
|
||||||
|
if (lang) {
|
||||||
|
deLang = OPT_LANGS_TO_CODE[langDetector].get(lang) || "";
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("detect lang remote", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地识别
|
||||||
|
if (!deLang) {
|
||||||
|
try {
|
||||||
|
const res = await browser?.i18n?.detectLanguage(text);
|
||||||
|
const lang = res?.languages?.[0]?.language;
|
||||||
|
if (lang && OPT_LANGS_MAP.has(lang)) {
|
||||||
|
deLang = lang;
|
||||||
|
} else if (lang?.startsWith("zh")) {
|
||||||
|
deLang = "zh-CN";
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("detect lang local", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deLang;
|
||||||
|
};
|
||||||
18
src/libs/fabManager.js
Normal file
18
src/libs/fabManager.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import ShadowDomManager from "./shadowDomManager";
|
||||||
|
import { APP_CONSTS } from "../config";
|
||||||
|
import ContentFab from "../views/Action/ContentFab";
|
||||||
|
|
||||||
|
export class FabManager extends ShadowDomManager {
|
||||||
|
constructor({ processActions, fabConfig }) {
|
||||||
|
super({
|
||||||
|
id: APP_CONSTS.fabID,
|
||||||
|
className: "notranslate",
|
||||||
|
reactComponent: ContentFab,
|
||||||
|
props: { processActions, fabConfig },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fabConfig?.isHide) {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
import { isExt, isGm } from "./client";
|
import { isExt, isGm } from "./client";
|
||||||
import { sendBgMsg } from "./msg";
|
import { sendBgMsg } from "./msg";
|
||||||
import { taskPool } from "./pool";
|
import { getSettingWithDefault } from "./storage";
|
||||||
import {
|
import { MSG_FETCH, DEFAULT_HTTP_TIMEOUT } from "../config";
|
||||||
MSG_FETCH,
|
|
||||||
MSG_FETCH_LIMIT,
|
|
||||||
MSG_FETCH_CLEAR,
|
|
||||||
CACHE_NAME,
|
|
||||||
DEFAULT_FETCH_INTERVAL,
|
|
||||||
DEFAULT_FETCH_LIMIT,
|
|
||||||
} from "../config";
|
|
||||||
import { isBg } from "./browser";
|
import { isBg } from "./browser";
|
||||||
import { newCacheReq, newTransReq } from "./req";
|
import { kissLog } from "./log";
|
||||||
|
import { getFetchPool } from "./pool";
|
||||||
|
import { getHttpCachePolyfill, parseResponse } from "./cache";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 油猴脚本的请求封装
|
* 油猴脚本的请求封装
|
||||||
@@ -18,7 +13,10 @@ import { newCacheReq, newTransReq } from "./req";
|
|||||||
* @param {*} init
|
* @param {*} init
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
export const fetchGM = async (
|
||||||
|
input,
|
||||||
|
{ method = "GET", headers, body, timeout } = {}
|
||||||
|
) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
GM.xmlHttpRequest({
|
GM.xmlHttpRequest({
|
||||||
method,
|
method,
|
||||||
@@ -26,7 +24,8 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
|||||||
headers,
|
headers,
|
||||||
data: body,
|
data: body,
|
||||||
// withCredentials: true,
|
// withCredentials: true,
|
||||||
onload: ({ response, responseHeaders, status, statusText, ...opts }) => {
|
timeout,
|
||||||
|
onload: ({ response, responseHeaders, status, statusText }) => {
|
||||||
const headers = {};
|
const headers = {};
|
||||||
responseHeaders.split("\n").forEach((line) => {
|
responseHeaders.split("\n").forEach((line) => {
|
||||||
const [name, value] = line.split(":").map((item) => item.trim());
|
const [name, value] = line.split(":").map((item) => item.trim());
|
||||||
@@ -42,172 +41,112 @@ export const fetchGM = async (input, { method = "GET", headers, body } = {}) =>
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onerror: reject,
|
onerror: reject,
|
||||||
|
onabort: () => {
|
||||||
|
reject(new Error("GM request onabort."));
|
||||||
|
},
|
||||||
|
ontimeout: () => {
|
||||||
|
reject(new Error("GM request timeout."));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发起请求
|
* 发起请求
|
||||||
* @param {*} param0
|
* @param {*} input
|
||||||
|
* @param {*} init
|
||||||
|
* @param {*} opts
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchApi = async ({ input, init, transOpts, apiSetting }) => {
|
export const fetchPatcher = async (input, init = {}, opts) => {
|
||||||
if (transOpts?.translator) {
|
let timeout = opts?.httpTimeout;
|
||||||
[input, init] = await newTransReq(transOpts, apiSetting);
|
if (!timeout) {
|
||||||
|
try {
|
||||||
|
timeout = (await getSettingWithDefault()).httpTimeout;
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("getSettingWithDefault", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (!timeout) {
|
||||||
if (!input) {
|
timeout = DEFAULT_HTTP_TIMEOUT;
|
||||||
throw new Error("url is empty");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGm) {
|
if (isGm) {
|
||||||
let info;
|
// todo: 自定义接口 init 可能包含了 signal
|
||||||
if (window.KISS_GM) {
|
Object.assign(init, { timeout });
|
||||||
info = await window.KISS_GM.getInfo();
|
|
||||||
} else {
|
|
||||||
info = GM.info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tampermonkey --> .connects
|
const { body, headers, status, statusText } = window.KISS_GM
|
||||||
// Violentmonkey --> .connect
|
? await window.KISS_GM.fetch(input, init)
|
||||||
const connects = info?.script?.connects || info?.script?.connect || [];
|
: await fetchGM(input, init);
|
||||||
const url = new URL(input);
|
|
||||||
const isSafe = connects.find((item) => url.hostname.endsWith(item));
|
|
||||||
|
|
||||||
if (isSafe) {
|
return new Response(body, {
|
||||||
const { body, headers, status, statusText } = window.KISS_GM
|
headers: new Headers(headers),
|
||||||
? await window.KISS_GM.fetch(input, init)
|
status,
|
||||||
: await fetchGM(input, init);
|
statusText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return new Response(body, {
|
if (AbortSignal?.timeout && !init.signal) {
|
||||||
headers: new Headers(headers),
|
Object.assign(init, { signal: AbortSignal.timeout(timeout) });
|
||||||
status,
|
|
||||||
statusText,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(input, init);
|
return fetch(input, init);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求池实例
|
* 处理请求
|
||||||
*/
|
* @param {*} param0
|
||||||
export const fetchPool = taskPool(
|
|
||||||
fetchApi,
|
|
||||||
null,
|
|
||||||
DEFAULT_FETCH_INTERVAL,
|
|
||||||
DEFAULT_FETCH_LIMIT
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 请求数据统一接口
|
|
||||||
* @param {*} input
|
|
||||||
* @param {*} opts
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchData = async (
|
export const fetchHandle = async ({ input, init, opts }) => {
|
||||||
input,
|
const res = await fetchPatcher(input, init, opts);
|
||||||
{ useCache, usePool, transOpts, apiSetting, ...init } = {}
|
return parseResponse(res, opts.expect);
|
||||||
) => {
|
|
||||||
const cacheReq = await newCacheReq(input, init);
|
|
||||||
let res;
|
|
||||||
|
|
||||||
// 查询缓存
|
|
||||||
if (useCache) {
|
|
||||||
try {
|
|
||||||
const cache = await caches.open(CACHE_NAME);
|
|
||||||
res = await cache.match(cacheReq);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[cache match]", err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
// 发送请求
|
|
||||||
if (usePool) {
|
|
||||||
res = await fetchPool.push({ input, init, transOpts, apiSetting });
|
|
||||||
} else {
|
|
||||||
res = await fetchApi({ input, init, transOpts, apiSetting });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res?.ok) {
|
|
||||||
const cause = {
|
|
||||||
status: res.status,
|
|
||||||
};
|
|
||||||
if (res.headers.get("Content-Type")?.includes("json")) {
|
|
||||||
cause.body = await res.json();
|
|
||||||
}
|
|
||||||
throw new Error(`response: [${res.status}] ${res.statusText}`, { cause });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 插入缓存
|
|
||||||
if (useCache) {
|
|
||||||
try {
|
|
||||||
const cache = await caches.open(CACHE_NAME);
|
|
||||||
await cache.put(cacheReq, res.clone());
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[cache put]", err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = res.headers.get("Content-Type");
|
|
||||||
if (contentType?.includes("json")) {
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
return await res.text();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fetch 兼容性封装
|
* fetch 兼容性封装
|
||||||
* @param {*} input
|
* @param {*} args
|
||||||
* @param {*} opts
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const fetchPolyfill = async (input, opts) => {
|
export const fnPolyfill = ({ fn, msg = MSG_FETCH, ...args }) => {
|
||||||
|
// 插件
|
||||||
|
if (isExt && !isBg()) {
|
||||||
|
return sendBgMsg(msg, { ...args });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 油猴/网页/BackgroundPage
|
||||||
|
return fn({ ...args });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据请求
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} init
|
||||||
|
* @param {*} param1
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const fetchData = async (
|
||||||
|
input,
|
||||||
|
init,
|
||||||
|
{ useCache, usePool, fetchInterval, fetchLimit, ...opts } = {}
|
||||||
|
) => {
|
||||||
if (!input?.trim()) {
|
if (!input?.trim()) {
|
||||||
throw new Error("URL is empty");
|
throw new Error("URL is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 插件
|
// 使用缓存数据
|
||||||
if (isExt && !isBg()) {
|
if (useCache) {
|
||||||
const res = await sendBgMsg(MSG_FETCH, { input, opts });
|
const resCache = await getHttpCachePolyfill(input, init);
|
||||||
if (res.error) {
|
if (resCache) {
|
||||||
throw new Error(res.error, { cause: res.cause });
|
return resCache;
|
||||||
}
|
}
|
||||||
return res.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 油猴/网页/BackgroundPage
|
// 通过任务池发送请求
|
||||||
return await fetchData(input, opts);
|
if (usePool) {
|
||||||
};
|
const fetchPool = getFetchPool(fetchInterval, fetchLimit);
|
||||||
|
return fetchPool.push(fnPolyfill, { fn: fetchHandle, input, init, opts });
|
||||||
/**
|
|
||||||
* 更新 fetch pool 参数
|
|
||||||
* @param {*} interval
|
|
||||||
* @param {*} limit
|
|
||||||
*/
|
|
||||||
export const updateFetchPool = async (interval, limit) => {
|
|
||||||
if (isExt) {
|
|
||||||
const res = await sendBgMsg(MSG_FETCH_LIMIT, { interval, limit });
|
|
||||||
if (res.error) {
|
|
||||||
throw new Error(res.error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fetchPool.update(interval, limit);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
// 直接请求
|
||||||
* 清空任务池
|
return fnPolyfill({ fn: fetchHandle, input, init, opts });
|
||||||
*/
|
|
||||||
export const clearFetchPool = async () => {
|
|
||||||
if (isExt) {
|
|
||||||
const res = await sendBgMsg(MSG_FETCH_CLEAR);
|
|
||||||
if (res.error) {
|
|
||||||
throw new Error(res.error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fetchPool.clear();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { CACHE_NAME } from "../config";
|
|
||||||
import { browser } from "./browser";
|
|
||||||
import { apiBaiduLangdetect } from "../apis";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除缓存数据
|
|
||||||
*/
|
|
||||||
export const tryClearCaches = async () => {
|
|
||||||
try {
|
|
||||||
caches.delete(CACHE_NAME);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[clean caches]", err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 语言识别
|
|
||||||
* @param {*} q
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const tryDetectLang = async (q, useRemote = false) => {
|
|
||||||
let lang = "";
|
|
||||||
|
|
||||||
if (useRemote) {
|
|
||||||
try {
|
|
||||||
lang = await apiBaiduLangdetect(q);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[detect lang remote]", err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lang) {
|
|
||||||
try {
|
|
||||||
const res = await browser?.i18n?.detectLanguage(q);
|
|
||||||
lang = res?.languages?.[0]?.language;
|
|
||||||
} catch (err) {
|
|
||||||
console.log("[detect lang local]", err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lang;
|
|
||||||
};
|
|
||||||
60
src/libs/injector.js
Normal file
60
src/libs/injector.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { trustedTypesHelper } from "./trustedTypes";
|
||||||
|
|
||||||
|
// Function to inject inline JavaScript code
|
||||||
|
export const injectInlineJs = (code, id = "kiss-translator-inline-js") => {
|
||||||
|
if (document.getElementById(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.createElement("script");
|
||||||
|
el.setAttribute("data-source", "kiss-inject injectInlineJs");
|
||||||
|
el.type = "text/javascript";
|
||||||
|
el.id = id;
|
||||||
|
el.textContent = trustedTypesHelper.createScript(code);
|
||||||
|
(document.head || document.documentElement).appendChild(el);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const injectInlineJsBg = (code, id = "kiss-translator-inline-js") => {
|
||||||
|
if (document.getElementById(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.createElement("script");
|
||||||
|
el.setAttribute("data-source", "kiss-inject injectInlineJsBg");
|
||||||
|
el.type = "text/javascript";
|
||||||
|
el.id = id;
|
||||||
|
el.textContent = code;
|
||||||
|
(document.head || document.documentElement).appendChild(el);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to inject external JavaScript file
|
||||||
|
export const injectExternalJs = (src, id = "kiss-translator-external-js") => {
|
||||||
|
if (document.getElementById(id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.createElement("script");
|
||||||
|
el.setAttribute("data-source", "kiss-inject injectExternalJs");
|
||||||
|
el.type = "text/javascript";
|
||||||
|
el.id = id;
|
||||||
|
el.src = trustedTypesHelper.createScriptURL(src);
|
||||||
|
(document.head || document.documentElement).appendChild(el);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to inject internal CSS code
|
||||||
|
export const injectInternalCss = (styles) => {
|
||||||
|
const el = document.createElement("style");
|
||||||
|
el.setAttribute("data-source", "kiss-inject injectInternalCss");
|
||||||
|
el.textContent = styles;
|
||||||
|
document.head?.appendChild(el);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to inject external CSS file
|
||||||
|
export const injectExternalCss = (href) => {
|
||||||
|
const el = document.createElement("link");
|
||||||
|
el.setAttribute("data-source", "kiss-inject injectExternalCss");
|
||||||
|
el.setAttribute("rel", "stylesheet");
|
||||||
|
el.setAttribute("type", "text/css");
|
||||||
|
el.setAttribute("href", href);
|
||||||
|
document.head?.appendChild(el);
|
||||||
|
};
|
||||||
327
src/libs/inputTranslate.js
Normal file
327
src/libs/inputTranslate.js
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_INPUT_RULE,
|
||||||
|
DEFAULT_INPUT_SHORTCUT,
|
||||||
|
OPT_LANGS_LIST,
|
||||||
|
DEFAULT_API_SETTING,
|
||||||
|
} from "../config";
|
||||||
|
import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
|
||||||
|
import { stepShortcutRegister } from "./shortcut";
|
||||||
|
import { apiTranslate } from "../apis";
|
||||||
|
import { createLoadingSVG } from "./svg";
|
||||||
|
import { logger } from "./log";
|
||||||
|
|
||||||
|
function isInputNode(node) {
|
||||||
|
return (
|
||||||
|
node.nodeName?.toUpperCase() === "INPUT" ||
|
||||||
|
node.nodeName?.toUpperCase() === "TEXTAREA"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditAbleNode(node) {
|
||||||
|
return node.hasAttribute("contenteditable");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replaceContentEditableText(node, newText) {
|
||||||
|
try {
|
||||||
|
logger.debug("try replace editable 1: pasteEvent");
|
||||||
|
|
||||||
|
node.focus();
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection) throw new Error("window.getSelection() is not available.");
|
||||||
|
|
||||||
|
const targetNode = node.querySelector("p") || node;
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(targetNode);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.setData("text/plain", newText);
|
||||||
|
|
||||||
|
const pasteEvent = new ClipboardEvent("paste", {
|
||||||
|
clipboardData: dataTransfer,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
node.dispatchEvent(pasteEvent);
|
||||||
|
|
||||||
|
await sleep(50);
|
||||||
|
if (node.innerText.trim() === newText) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Strategy 1 failed to replace text correctly.");
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug("Strategy 1 Failed:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("try replace editable 2: execCommand");
|
||||||
|
|
||||||
|
node.focus();
|
||||||
|
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection) throw new Error("window.getSelection() is not available.");
|
||||||
|
|
||||||
|
const targetNode = node.querySelector("p") || node;
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(targetNode);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
document.execCommand("insertText", false, newText);
|
||||||
|
|
||||||
|
await sleep(50);
|
||||||
|
if (node.innerText.trim() === newText) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Strategy 2 failed to replace text correctly.");
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug("Strategy 2 Failed:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("try replace editable 3: textContent");
|
||||||
|
|
||||||
|
node.focus();
|
||||||
|
|
||||||
|
const targetNode = node.querySelector("p") || node;
|
||||||
|
const textSpan = targetNode.querySelector('span[data-lexical-text="true"]');
|
||||||
|
|
||||||
|
if (textSpan) {
|
||||||
|
textSpan.textContent = newText;
|
||||||
|
} else {
|
||||||
|
targetNode.textContent = newText;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||||
|
|
||||||
|
await sleep(50);
|
||||||
|
if (node.innerText.trim() === newText) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Strategy 3 failed to replace text correctly.");
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug("Strategy 3 Failed:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeText(node) {
|
||||||
|
if (isInputNode(node)) {
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
return node.innerText || node.textContent || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLoading(node, loadingId) {
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.id = loadingId;
|
||||||
|
div.appendChild(createLoadingSVG());
|
||||||
|
div.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
left: ${rect.left}px;
|
||||||
|
top: ${rect.top}px;
|
||||||
|
width: ${rect.width}px;
|
||||||
|
height: ${rect.height}px;
|
||||||
|
line-height: ${rect.height}px;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 2147483647;
|
||||||
|
pointer-events: none; /* 允许点击穿透 */
|
||||||
|
`;
|
||||||
|
document.body.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoading(loadingId) {
|
||||||
|
const div = document.getElementById(loadingId);
|
||||||
|
if (div) div.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入框翻译
|
||||||
|
*/
|
||||||
|
export class InputTranslator {
|
||||||
|
#config;
|
||||||
|
#unregisterShortcut = null;
|
||||||
|
#isEnabled = false;
|
||||||
|
#triggerShortcut; // 用于缓存快捷键
|
||||||
|
|
||||||
|
constructor({ inputRule = DEFAULT_INPUT_RULE, transApis = [] } = {}) {
|
||||||
|
this.#config = { inputRule, transApis };
|
||||||
|
|
||||||
|
const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule;
|
||||||
|
if (initialTriggerShortcut && initialTriggerShortcut.length > 0) {
|
||||||
|
this.#triggerShortcut = initialTriggerShortcut;
|
||||||
|
} else {
|
||||||
|
this.#triggerShortcut = DEFAULT_INPUT_SHORTCUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#config.inputRule.transOpen) {
|
||||||
|
this.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用输入翻译功能
|
||||||
|
*/
|
||||||
|
enable() {
|
||||||
|
if (this.#isEnabled || !this.#config.inputRule.transOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { triggerCount, triggerTime } = this.#config.inputRule;
|
||||||
|
this.#unregisterShortcut = stepShortcutRegister(
|
||||||
|
this.#triggerShortcut,
|
||||||
|
this.#handleTranslate.bind(this),
|
||||||
|
triggerCount,
|
||||||
|
triggerTime
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#isEnabled = true;
|
||||||
|
logger.info("Input Translator enabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用输入翻译功能
|
||||||
|
*/
|
||||||
|
disable() {
|
||||||
|
if (!this.#isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.#unregisterShortcut) {
|
||||||
|
this.#unregisterShortcut();
|
||||||
|
this.#unregisterShortcut = null;
|
||||||
|
}
|
||||||
|
this.#isEnabled = false;
|
||||||
|
logger.info("Input Translator disabled.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换启用/禁用状态
|
||||||
|
*/
|
||||||
|
toggle() {
|
||||||
|
if (this.#isEnabled) {
|
||||||
|
this.disable();
|
||||||
|
} else {
|
||||||
|
this.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 翻译核心逻辑
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async #handleTranslate() {
|
||||||
|
let node = document.activeElement;
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
while (node.shadowRoot && node.shadowRoot.activeElement) {
|
||||||
|
node = node.shadowRoot.activeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInputNode(node) && !isEditAbleNode(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiSlug, transSign, triggerCount } = this.#config.inputRule;
|
||||||
|
let { fromLang, toLang } = this.#config.inputRule;
|
||||||
|
|
||||||
|
let initText = getNodeText(node);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.#triggerShortcut.length === 1 &&
|
||||||
|
this.#triggerShortcut[0].length === 1
|
||||||
|
) {
|
||||||
|
initText = removeEndchar(
|
||||||
|
initText,
|
||||||
|
this.#triggerShortcut[0],
|
||||||
|
triggerCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initText.trim()) return;
|
||||||
|
|
||||||
|
let text = initText;
|
||||||
|
if (transSign) {
|
||||||
|
const res = matchInputStr(text, transSign);
|
||||||
|
if (res) {
|
||||||
|
let lang = res[1];
|
||||||
|
if (lang === "zh" || lang === "cn") lang = "zh-CN";
|
||||||
|
else if (lang === "tw" || lang === "hk") lang = "zh-TW";
|
||||||
|
|
||||||
|
if (lang && OPT_LANGS_LIST.includes(lang)) {
|
||||||
|
toLang = lang;
|
||||||
|
}
|
||||||
|
text = res[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiSetting =
|
||||||
|
this.#config.transApis.find((api) => api.apiSlug === apiSlug) ||
|
||||||
|
DEFAULT_API_SETTING;
|
||||||
|
const loadingId = "kiss-loading-" + genEventName();
|
||||||
|
|
||||||
|
try {
|
||||||
|
addLoading(node, loadingId);
|
||||||
|
|
||||||
|
const { trText, isSame } = await apiTranslate({
|
||||||
|
text,
|
||||||
|
fromLang,
|
||||||
|
toLang,
|
||||||
|
apiSetting,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newText = trText?.trim() || "";
|
||||||
|
if (!newText || isSame) return;
|
||||||
|
|
||||||
|
if (isInputNode(node)) {
|
||||||
|
node.value = newText;
|
||||||
|
node.dispatchEvent(
|
||||||
|
new Event("input", { bubbles: true, cancelable: true })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const success = await replaceContentEditableText(node, newText);
|
||||||
|
if (!success) {
|
||||||
|
// todo: 提示可以黏贴
|
||||||
|
logger.info("Replace editable text failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.info("Translate input error:", err);
|
||||||
|
} finally {
|
||||||
|
removeLoading(loadingId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新配置
|
||||||
|
*/
|
||||||
|
updateConfig({ inputRule, transApis }) {
|
||||||
|
const wasEnabled = this.#isEnabled;
|
||||||
|
if (wasEnabled) {
|
||||||
|
this.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputRule) {
|
||||||
|
this.#config.inputRule = inputRule;
|
||||||
|
}
|
||||||
|
if (transApis) {
|
||||||
|
this.#config.transApis = transApis;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { triggerShortcut: initialTriggerShortcut } = this.#config.inputRule;
|
||||||
|
this.#triggerShortcut =
|
||||||
|
initialTriggerShortcut && initialTriggerShortcut.length > 0
|
||||||
|
? initialTriggerShortcut
|
||||||
|
: DEFAULT_INPUT_SHORTCUT;
|
||||||
|
|
||||||
|
if (wasEnabled) {
|
||||||
|
this.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/libs/interpreter.js
Normal file
14
src/libs/interpreter.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Sval from "sval";
|
||||||
|
|
||||||
|
export const interpreter = new Sval({
|
||||||
|
// ECMA Version of the code
|
||||||
|
// 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15
|
||||||
|
// or 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024
|
||||||
|
// or "latest"
|
||||||
|
ecmaVer: "latest",
|
||||||
|
// Code source type
|
||||||
|
// "script" or "module"
|
||||||
|
sourceType: "script",
|
||||||
|
// Whether the code runs in a sandbox
|
||||||
|
sandBox: true,
|
||||||
|
});
|
||||||
163
src/libs/log.js
Normal file
163
src/libs/log.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// 定义日志级别
|
||||||
|
export const LogLevel = {
|
||||||
|
DEBUG: { value: 0, name: "DEBUG", color: "#6495ED" }, // 宝蓝色
|
||||||
|
INFO: { value: 1, name: "INFO", color: "#4CAF50" }, // 绿色
|
||||||
|
WARN: { value: 2, name: "WARN", color: "#FFC107" }, // 琥珀色
|
||||||
|
ERROR: { value: 3, name: "ERROR", color: "#F44336" }, // 红色
|
||||||
|
SILENT: { value: 4, name: "SILENT" }, // 特殊级别,用于关闭所有日志
|
||||||
|
};
|
||||||
|
|
||||||
|
function findLogLevelByValue(value) {
|
||||||
|
return Object.values(LogLevel).find((level) => level.value === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLogLevelByName(name) {
|
||||||
|
if (typeof name !== "string" || name.length === 0) return undefined;
|
||||||
|
const upperCaseName = name.toUpperCase();
|
||||||
|
return Object.values(LogLevel).find((level) => level.name === upperCaseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
/**
|
||||||
|
* @param {object} [options={}] 配置选项
|
||||||
|
* @param {LogLevel} [options.level=LogLevel.INFO] 要显示的最低日志级别
|
||||||
|
* @param {string} [options.prefix='App'] 日志前缀,用于区分模块
|
||||||
|
*/
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.config = {
|
||||||
|
level: options.level || LogLevel.INFO,
|
||||||
|
prefix: options.prefix || "KISS-Translator",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态设置日志级别
|
||||||
|
* @param {LogLevel} level - 新的日志级别
|
||||||
|
*/
|
||||||
|
setLevel(level) {
|
||||||
|
let newLevelObject;
|
||||||
|
|
||||||
|
if (typeof level === "string") {
|
||||||
|
newLevelObject = findLogLevelByName(level);
|
||||||
|
if (!newLevelObject) {
|
||||||
|
this.warn(
|
||||||
|
`Invalid log level name provided: "${level}". Keeping current level.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (typeof level === "number") {
|
||||||
|
newLevelObject = findLogLevelByValue(level);
|
||||||
|
if (!newLevelObject) {
|
||||||
|
this.warn(
|
||||||
|
`Invalid log level value provided: ${level}. Keeping current level.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (level && typeof level.value === "number") {
|
||||||
|
newLevelObject = level;
|
||||||
|
} else {
|
||||||
|
this.warn(
|
||||||
|
"Invalid argument passed to setLevel. Must be a LogLevel object, number, or string."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.level.value !== newLevelObject.value) {
|
||||||
|
this.config.level = newLevelObject;
|
||||||
|
console.log(
|
||||||
|
`[${this.config.prefix}] Log level dynamically set to ${this.config.level.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 核心日志记录方法
|
||||||
|
* @private
|
||||||
|
* @param {LogLevel} level - 当前消息的日志级别
|
||||||
|
* @param {...any} args - 要记录的多个参数,可以是任何类型
|
||||||
|
*/
|
||||||
|
_log(level, ...args) {
|
||||||
|
// 如果当前级别低于配置的最低级别,则不打印
|
||||||
|
if (level.value < this.config.level.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const prefixStr = `[${this.config.prefix}]`;
|
||||||
|
const levelStr = `[${level.name}]`;
|
||||||
|
|
||||||
|
// 判断是否在浏览器环境并且浏览器支持 console 样式
|
||||||
|
const isBrowser =
|
||||||
|
typeof window !== "undefined" && typeof window.document !== "undefined";
|
||||||
|
|
||||||
|
if (isBrowser) {
|
||||||
|
// 在浏览器中使用颜色高亮
|
||||||
|
const consoleMethod = this._getConsoleMethod(level);
|
||||||
|
consoleMethod(
|
||||||
|
`%c${timestamp} %c${prefixStr} %c${levelStr}`,
|
||||||
|
"color: gray; font-weight: lighter;", // 时间戳样式
|
||||||
|
"color: #7c57e0; font-weight: bold;", // 前缀样式 (紫色)
|
||||||
|
`color: ${level.color}; font-weight: bold;`, // 日志级别样式
|
||||||
|
...args
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 在 Node.js 或不支持样式的环境中,输出纯文本
|
||||||
|
const consoleMethod = this._getConsoleMethod(level);
|
||||||
|
consoleMethod(timestamp, prefixStr, levelStr, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据日志级别获取对应的 console 方法
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getConsoleMethod(level) {
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.ERROR:
|
||||||
|
return console.error;
|
||||||
|
case LogLevel.WARN:
|
||||||
|
return console.warn;
|
||||||
|
case LogLevel.INFO:
|
||||||
|
return console.info;
|
||||||
|
default:
|
||||||
|
return console.log;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 DEBUG 级别的日志
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
debug(...args) {
|
||||||
|
this._log(LogLevel.DEBUG, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 INFO 级别的日志
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
info(...args) {
|
||||||
|
this._log(LogLevel.INFO, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 WARN 级别的日志
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
warn(...args) {
|
||||||
|
this._log(LogLevel.WARN, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 ERROR 级别的日志
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
error(...args) {
|
||||||
|
this._log(LogLevel.ERROR, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger = new Logger();
|
||||||
|
export const kissLog = logger.info.bind(logger);
|
||||||
|
|
||||||
|
// todo:debug日志埋点
|
||||||
@@ -1,5 +1,22 @@
|
|||||||
import { browser } from "./browser";
|
import { browser } from "./browser";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前tab信息
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getCurTab = async () => {
|
||||||
|
const [tab] = await browser.tabs.query({
|
||||||
|
active: true,
|
||||||
|
lastFocusedWindow: true,
|
||||||
|
});
|
||||||
|
return tab;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCurTabId = async () => {
|
||||||
|
const tab = await getCurTab();
|
||||||
|
return tab.id;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送消息给background
|
* 发送消息给background
|
||||||
* @param {*} action
|
* @param {*} action
|
||||||
@@ -16,15 +33,6 @@ export const sendBgMsg = (action, args) =>
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const sendTabMsg = async (action, args) => {
|
export const sendTabMsg = async (action, args) => {
|
||||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
const tabId = await getCurTabId();
|
||||||
return browser.tabs.sendMessage(tabs[0].id, { action, args });
|
return browser.tabs.sendMessage(tabId, { action, args });
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前tab信息
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getTabInfo = async () => {
|
|
||||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
|
||||||
return tabs[0];
|
|
||||||
};
|
};
|
||||||
|
|||||||
226
src/libs/pool.js
226
src/libs/pool.js
@@ -1,78 +1,170 @@
|
|||||||
|
import { DEFAULT_FETCH_INTERVAL, DEFAULT_FETCH_LIMIT } from "../config";
|
||||||
|
import { kissLog } from "./log";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 任务池
|
* 任务池
|
||||||
* @param {*} fn
|
|
||||||
* @param {*} preFn
|
|
||||||
* @param {*} _interval
|
|
||||||
* @param {*} _limit
|
|
||||||
* @returns
|
|
||||||
*/
|
*/
|
||||||
export const taskPool = (
|
class TaskPool {
|
||||||
fn,
|
#pool = [];
|
||||||
preFn,
|
|
||||||
_interval = 100,
|
|
||||||
_limit = 100,
|
|
||||||
_retryInteral = 1000
|
|
||||||
) => {
|
|
||||||
const pool = [];
|
|
||||||
const maxRetry = 2; // 最大重试次数
|
|
||||||
let maxCount = _limit; // 最大数量
|
|
||||||
let curCount = 0; // 当前数量
|
|
||||||
let interval = _interval; // 间隔时间
|
|
||||||
let timer = null;
|
|
||||||
|
|
||||||
const run = async () => {
|
#maxRetry = 2; // 最大重试次数
|
||||||
// console.log("timer", timer);
|
#retryInterval = 1000; // 重试间隔时间
|
||||||
timer && clearTimeout(timer);
|
#limit; // 最大并发数
|
||||||
timer = setTimeout(run, interval);
|
#interval; // 任务最小启动间隔
|
||||||
|
|
||||||
if (curCount < maxCount) {
|
#currentConcurrent = 0; // 当前正在执行的任务数
|
||||||
const item = pool.shift();
|
#lastExecutionTime = 0; // 上一个任务的启动时间
|
||||||
if (item) {
|
#schedulerTimer = null; // 用于调度下一个任务的定时器
|
||||||
curCount++;
|
|
||||||
const { args, resolve, reject, retry } = item;
|
constructor(
|
||||||
try {
|
interval = DEFAULT_FETCH_INTERVAL,
|
||||||
const preArgs = preFn ? await preFn(item.args) : {};
|
limit = DEFAULT_FETCH_LIMIT,
|
||||||
const res = await fn({ ...args, ...preArgs });
|
retryInterval = 1000
|
||||||
resolve(res);
|
) {
|
||||||
} catch (err) {
|
this.#interval = interval;
|
||||||
console.log("[task]", retry, err);
|
this.#limit = limit;
|
||||||
if (retry < maxRetry) {
|
this.#retryInterval = retryInterval;
|
||||||
const retryTimer = setTimeout(() => {
|
}
|
||||||
clearTimeout(retryTimer);
|
|
||||||
pool.push({ args, resolve, reject, retry: retry + 1 });
|
/**
|
||||||
}, _retryInteral);
|
* 调度器
|
||||||
} else {
|
*/
|
||||||
reject(err);
|
#scheduleNext() {
|
||||||
}
|
if (this.#schedulerTimer) {
|
||||||
} finally {
|
return;
|
||||||
curCount--;
|
}
|
||||||
|
|
||||||
|
if (this.#currentConcurrent >= this.#limit || this.#pool.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLast = now - this.#lastExecutionTime;
|
||||||
|
const delay = Math.max(0, this.#interval - timeSinceLast);
|
||||||
|
|
||||||
|
this.#schedulerTimer = setTimeout(() => {
|
||||||
|
this.#schedulerTimer = null;
|
||||||
|
if (this.#currentConcurrent < this.#limit && this.#pool.length > 0) {
|
||||||
|
const task = this.#pool.shift();
|
||||||
|
if (task) {
|
||||||
|
this.#lastExecutionTime = Date.now();
|
||||||
|
this.#execute(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
if (this.#pool.length > 0) {
|
||||||
push: async (args) => {
|
this.#scheduleNext();
|
||||||
if (!timer) {
|
|
||||||
run();
|
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
}, delay);
|
||||||
pool.push({ args, resolve, reject, retry: 0 });
|
}
|
||||||
});
|
|
||||||
},
|
/**
|
||||||
update: (_interval = 100, _limit = 100) => {
|
* 执行单个任务
|
||||||
if (_interval >= 0 && _interval <= 5000 && _interval !== interval) {
|
* @param {object} task - 任务对象
|
||||||
interval = _interval;
|
*/
|
||||||
|
async #execute(task) {
|
||||||
|
this.#currentConcurrent++;
|
||||||
|
const { fn, args, resolve, reject, retry } = task;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fn(args);
|
||||||
|
resolve(res);
|
||||||
|
} catch (err) {
|
||||||
|
kissLog("task pool", err);
|
||||||
|
if (retry < this.#maxRetry) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.#pool.unshift({ ...task, retry: retry + 1 }); // unshift 保证重试任务优先
|
||||||
|
this.#scheduleNext();
|
||||||
|
}, this.#retryInterval);
|
||||||
|
} else {
|
||||||
|
reject(err);
|
||||||
}
|
}
|
||||||
if (_limit >= 1 && _limit <= 100 && _limit !== maxCount) {
|
} finally {
|
||||||
maxCount = _limit;
|
this.#currentConcurrent--;
|
||||||
}
|
this.#scheduleNext();
|
||||||
},
|
}
|
||||||
clear: () => {
|
}
|
||||||
pool.length = 0;
|
|
||||||
curCount = 0;
|
/**
|
||||||
timer && clearTimeout(timer);
|
* 向任务池中添加一个新任务
|
||||||
timer = null;
|
* @param {Function} fn - 要执行的异步函数
|
||||||
},
|
* @param {*} args - 函数的参数
|
||||||
};
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
push(fn, args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.#pool.push({ fn, args, resolve, reject, retry: 0 });
|
||||||
|
this.#scheduleNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新任务池的配置
|
||||||
|
* @param {number} interval - 新的最小任务间隔
|
||||||
|
* @param {number} limit - 新的最大并发数
|
||||||
|
*/
|
||||||
|
update(interval, limit) {
|
||||||
|
if (interval >= 0) {
|
||||||
|
this.#interval = interval;
|
||||||
|
}
|
||||||
|
if (limit >= 1) {
|
||||||
|
this.#limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#scheduleNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空任务池
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
for (const task of this.#pool) {
|
||||||
|
task.reject("the task pool was cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#pool.length = 0;
|
||||||
|
if (this.#schedulerTimer) {
|
||||||
|
clearTimeout(this.#schedulerTimer);
|
||||||
|
this.#schedulerTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求池实例
|
||||||
|
*/
|
||||||
|
let fetchPool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求池实例
|
||||||
|
* @param interval
|
||||||
|
* @param limit
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getFetchPool = (interval, limit) => {
|
||||||
|
if (!fetchPool) {
|
||||||
|
fetchPool = new TaskPool(
|
||||||
|
interval ?? DEFAULT_FETCH_INTERVAL,
|
||||||
|
limit ?? DEFAULT_FETCH_LIMIT
|
||||||
|
);
|
||||||
|
} else if (interval && limit) {
|
||||||
|
updateFetchPool(interval, limit);
|
||||||
|
}
|
||||||
|
return fetchPool;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新请求池参数
|
||||||
|
* @param {*} interval
|
||||||
|
* @param {*} limit
|
||||||
|
*/
|
||||||
|
export const updateFetchPool = (interval, limit) => {
|
||||||
|
fetchPool?.update(interval, limit);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空请求池
|
||||||
|
*/
|
||||||
|
export const clearFetchPool = () => {
|
||||||
|
fetchPool?.clear();
|
||||||
};
|
};
|
||||||
|
|||||||
26
src/libs/popupManager.js
Normal file
26
src/libs/popupManager.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import ShadowDomManager from "./shadowDomManager";
|
||||||
|
import { APP_CONSTS, EVENT_KISS, MSG_POPUP_TOGGLE } from "../config";
|
||||||
|
import Action from "../views/Action";
|
||||||
|
|
||||||
|
export class PopupManager extends ShadowDomManager {
|
||||||
|
constructor({ translator, processActions }) {
|
||||||
|
super({
|
||||||
|
id: APP_CONSTS.popupID,
|
||||||
|
className: "notranslate",
|
||||||
|
reactComponent: Action,
|
||||||
|
props: { translator, processActions },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(props) {
|
||||||
|
if (this.isVisible) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(EVENT_KISS, {
|
||||||
|
detail: { action: MSG_POPUP_TOGGLE },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.show(props || this._props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
src/libs/req.js
251
src/libs/req.js
@@ -1,251 +0,0 @@
|
|||||||
import queryString from "query-string";
|
|
||||||
import {
|
|
||||||
OPT_TRANS_GOOGLE,
|
|
||||||
OPT_TRANS_MICROSOFT,
|
|
||||||
OPT_TRANS_DEEPL,
|
|
||||||
OPT_TRANS_DEEPLFREE,
|
|
||||||
OPT_TRANS_DEEPLX,
|
|
||||||
OPT_TRANS_BAIDU,
|
|
||||||
OPT_TRANS_TENCENT,
|
|
||||||
OPT_TRANS_OPENAI,
|
|
||||||
OPT_TRANS_CLOUDFLAREAI,
|
|
||||||
OPT_TRANS_CUSTOMIZE,
|
|
||||||
URL_MICROSOFT_TRAN,
|
|
||||||
URL_TENCENT_TRANSMART,
|
|
||||||
PROMPT_PLACE_FROM,
|
|
||||||
PROMPT_PLACE_TO,
|
|
||||||
} from "../config";
|
|
||||||
import { msAuth } from "./auth";
|
|
||||||
import { genDeeplFree } from "../apis/deepl";
|
|
||||||
import { genBaidu } from "../apis/baidu";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造缓存 request
|
|
||||||
* @param {*} request
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const newCacheReq = async (input, init) => {
|
|
||||||
let request = new Request(input, init);
|
|
||||||
if (request.method !== "GET") {
|
|
||||||
const body = await request.text();
|
|
||||||
const cacheUrl = new URL(request.url);
|
|
||||||
cacheUrl.pathname += body;
|
|
||||||
request = new Request(cacheUrl.toString(), { method: "GET" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return request;
|
|
||||||
};
|
|
||||||
|
|
||||||
const genGoogle = ({ text, from, to, url, key }) => {
|
|
||||||
const params = {
|
|
||||||
client: "gtx",
|
|
||||||
dt: "t",
|
|
||||||
dj: 1,
|
|
||||||
ie: "UTF-8",
|
|
||||||
sl: from,
|
|
||||||
tl: to,
|
|
||||||
q: text,
|
|
||||||
};
|
|
||||||
const input = `${url}?${queryString.stringify(params)}`;
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (key) {
|
|
||||||
init.headers.Authorization = `Bearer ${key}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [input, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genMicrosoft = async ({ text, from, to }) => {
|
|
||||||
const [token] = await msAuth();
|
|
||||||
const params = {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
"api-version": "3.0",
|
|
||||||
};
|
|
||||||
const input = `${URL_MICROSOFT_TRAN}?${queryString.stringify(params)}`;
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify([{ Text: text }]),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [input, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genDeepl = ({ text, from, to, url, key }) => {
|
|
||||||
const data = {
|
|
||||||
text: [text],
|
|
||||||
target_lang: to,
|
|
||||||
source_lang: from,
|
|
||||||
// split_sentences: "0",
|
|
||||||
};
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
Authorization: `DeepL-Auth-Key ${key}`,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genDeeplX = ({ text, from, to, url, key }) => {
|
|
||||||
const data = {
|
|
||||||
text,
|
|
||||||
target_lang: to,
|
|
||||||
source_lang: from,
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
if (key) {
|
|
||||||
init.headers.Authorization = `Bearer ${key}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genTencent = ({ text, from, to }) => {
|
|
||||||
const data = {
|
|
||||||
header: {
|
|
||||||
fn: "auto_translation_block",
|
|
||||||
},
|
|
||||||
source: {
|
|
||||||
text_block: text,
|
|
||||||
lang: from,
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
lang: to,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [URL_TENCENT_TRANSMART, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genOpenAI = ({ text, from, to, url, key, prompt, model }) => {
|
|
||||||
prompt = prompt
|
|
||||||
.replaceAll(PROMPT_PLACE_FROM, from)
|
|
||||||
.replaceAll(PROMPT_PLACE_TO, to);
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
model,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: prompt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: text,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
temperature: 0,
|
|
||||||
max_tokens: 256,
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
Authorization: `Bearer ${key}`, // OpenAI
|
|
||||||
"api-key": key, // Azure OpenAI
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genCloudflareAI = ({ text, from, to, url, key }) => {
|
|
||||||
const data = {
|
|
||||||
text,
|
|
||||||
source_lang: from,
|
|
||||||
target_lang: to,
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
Authorization: `Bearer ${key}`,
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
const genCustom = ({ text, from, to, url, key }) => {
|
|
||||||
const data = {
|
|
||||||
text,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
};
|
|
||||||
const init = {
|
|
||||||
headers: {
|
|
||||||
"Content-type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
};
|
|
||||||
if (key) {
|
|
||||||
init.headers.Authorization = `Bearer ${key}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [url, init];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造翻译接口 request
|
|
||||||
* @param {*}
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const newTransReq = ({ translator, text, from, to }, apiSetting) => {
|
|
||||||
const args = { text, from, to, ...apiSetting };
|
|
||||||
switch (translator) {
|
|
||||||
case OPT_TRANS_GOOGLE:
|
|
||||||
return genGoogle(args);
|
|
||||||
case OPT_TRANS_MICROSOFT:
|
|
||||||
return genMicrosoft(args);
|
|
||||||
case OPT_TRANS_DEEPL:
|
|
||||||
return genDeepl(args);
|
|
||||||
case OPT_TRANS_DEEPLFREE:
|
|
||||||
return genDeeplFree(args);
|
|
||||||
case OPT_TRANS_DEEPLX:
|
|
||||||
return genDeeplX(args);
|
|
||||||
case OPT_TRANS_BAIDU:
|
|
||||||
return genBaidu(args);
|
|
||||||
case OPT_TRANS_TENCENT:
|
|
||||||
return genTencent(args);
|
|
||||||
case OPT_TRANS_OPENAI:
|
|
||||||
return genOpenAI(args);
|
|
||||||
case OPT_TRANS_CLOUDFLAREAI:
|
|
||||||
return genCloudflareAI(args);
|
|
||||||
case OPT_TRANS_CUSTOMIZE:
|
|
||||||
return genCustom(args);
|
|
||||||
default:
|
|
||||||
throw new Error(`[trans] translator: ${translator} not support`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user