Compare commits
802 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f858eed150 | ||
|
|
bbdd712b01 | ||
|
|
c0875971e9 | ||
|
|
0199ebb6c3 | ||
|
|
c5763e2f8f | ||
|
|
5338ec0c34 | ||
|
|
8b5735f521 | ||
|
|
3d1a1cd033 | ||
|
|
b1b5eeb0e0 | ||
|
|
49e37587f9 | ||
|
|
01102ae973 | ||
|
|
e7931bf360 | ||
|
|
d095e4b35a | ||
|
|
b8e254dab6 | ||
|
|
4059160d90 | ||
|
|
e0f242fe22 | ||
|
|
05453364ff | ||
|
|
c3aedd935d | ||
|
|
99a7f72448 | ||
|
|
56ae1eadbc | ||
|
|
4828c03bbf | ||
|
|
cfc07764b4 | ||
|
|
91938cc3b9 | ||
|
|
c62a84a9ea | ||
|
|
0b16b6bb86 | ||
|
|
6a8f7f0a40 | ||
|
|
42ca0967b6 | ||
|
|
deb29f0e88 | ||
|
|
714af986b0 | ||
|
|
4ff26366a5 | ||
|
|
9c628a8f53 | ||
|
|
4a40f2b8f7 | ||
|
|
9a2dda626c | ||
|
|
a9ff491da0 | ||
|
|
5c5a7d20de | ||
|
|
05ae4eb529 | ||
|
|
15f93b198c | ||
|
|
0a99dacb6b | ||
|
|
00f6c04611 | ||
|
|
d9b899b53f | ||
|
|
d96f8da8fd | ||
|
|
ababcf7850 | ||
|
|
f23bfaf694 | ||
|
|
cac05dee0b | ||
|
|
155c93d371 | ||
|
|
9a61ee7530 | ||
|
|
4bea1c5e5c | ||
|
|
9ccc26b0b0 | ||
|
|
5cd3787d6f | ||
|
|
807b1f62a1 | ||
|
|
c15db54d5a | ||
|
|
aa7b078121 | ||
|
|
99130d0181 | ||
|
|
90e2036cbe | ||
|
|
c2f3e42867 | ||
|
|
bd33369a41 | ||
|
|
4f625d8ed5 | ||
|
|
866fe56dd2 | ||
|
|
5f37dbca4c | ||
|
|
c49e617dfe | ||
|
|
e763ffd4cf | ||
|
|
20ab7dd3e1 | ||
|
|
55741c6332 | ||
|
|
42d85336a8 | ||
|
|
639b82f494 | ||
|
|
5003c176a2 | ||
|
|
10bfbbec17 | ||
|
|
3da900db7f | ||
|
|
9f421ec3b0 | ||
|
|
69fb11eee0 | ||
|
|
ffbb85df43 | ||
|
|
a4e78c4e0d | ||
|
|
274c5ae165 | ||
|
|
39c4012a1a | ||
|
|
6d4b0cbdef | ||
|
|
ea4b120a85 | ||
|
|
5c2454c331 | ||
|
|
4ff46965c4 | ||
|
|
33e3f7ea3c | ||
|
|
347fc4f2c8 | ||
|
|
2b4ff4a8a5 | ||
|
|
f7d34983e0 | ||
|
|
3271d69fcb | ||
|
|
7ea24b21f8 | ||
|
|
b2b608e8c3 | ||
|
|
e44ea5bc96 | ||
|
|
fa58b1e53f | ||
|
|
9466bc544c | ||
|
|
9e65f5726c | ||
|
|
fc2c3740a0 | ||
|
|
2095a6512b | ||
|
|
a461a72224 | ||
|
|
f9e7653901 | ||
|
|
a11e528b8d | ||
|
|
c0937aa473 | ||
|
|
fecd6451d5 | ||
|
|
fb96631351 | ||
|
|
a16450be5d | ||
|
|
02628b5886 | ||
|
|
60e37deae8 | ||
|
|
21922265e4 | ||
|
|
42c740eaff | ||
|
|
d187c23a77 | ||
|
|
9252042c99 | ||
|
|
66c5a471e3 | ||
|
|
91c48bedd3 | ||
|
|
c882ad5399 | ||
|
|
b3700dc09e | ||
|
|
b06187ddf7 | ||
|
|
cf69bb2013 | ||
|
|
81b284ad94 | ||
|
|
f838f877fa | ||
|
|
d7c6f8eb52 | ||
|
|
6f49f1fe01 | ||
|
|
e75c4554a5 | ||
|
|
58852502dc | ||
|
|
a151646850 | ||
|
|
97d290de9d | ||
|
|
438b255708 | ||
|
|
754ac166e0 | ||
|
|
0b18334236 | ||
|
|
90d2ad6b19 | ||
|
|
21fcae52b2 | ||
|
|
d72c9ba247 | ||
|
|
27f80148cb | ||
|
|
1daf57a4bd | ||
|
|
3999532e77 | ||
|
|
126a5e3bbc | ||
|
|
a1fb5871d1 | ||
|
|
4c18ebf61a | ||
|
|
8bc6a2adcc | ||
|
|
475c0673a0 | ||
|
|
f81491fb32 | ||
|
|
1f2a265c54 | ||
|
|
fbfe16e784 | ||
|
|
c6439fe020 | ||
|
|
7e605e5cda | ||
|
|
973fe56cc8 | ||
|
|
91bc7fa4b0 | ||
|
|
051fa37949 | ||
|
|
243aaac3da | ||
|
|
a8db632c4a | ||
|
|
11f5b22cb4 | ||
|
|
5967706daa | ||
|
|
9c02eba0dc | ||
|
|
e2340c2e98 | ||
|
|
a8e818f97f | ||
|
|
6f26c54b62 | ||
|
|
448feedace | ||
|
|
eefc1ee0d7 | ||
|
|
d2eac62273 | ||
|
|
ee89b34ab8 | ||
|
|
2d8584b72d | ||
|
|
e803ce13eb | ||
|
|
4e5fd18eea | ||
|
|
9ec62bc1de | ||
|
|
906acb217a | ||
|
|
6c6cc8d85b | ||
|
|
5cb09bc4c6 | ||
|
|
198d9fb17e | ||
|
|
33b87312f4 | ||
|
|
ece9b993e0 | ||
|
|
04894f118b | ||
|
|
ac7b6eeb21 | ||
|
|
4c4868a2b6 | ||
|
|
a75f726111 | ||
|
|
d34c0c8652 | ||
|
|
c0bd7d0610 | ||
|
|
155a66b913 | ||
|
|
c7216ef0a6 | ||
|
|
c692a8d8f3 | ||
|
|
54e6bc3154 | ||
|
|
2e24d32cc2 | ||
|
|
1c7e31a464 | ||
|
|
94bf8338cd | ||
|
|
152f0bd727 | ||
|
|
6ffdc7b07d | ||
|
|
fe87566668 | ||
|
|
c36dd47afd | ||
|
|
b6a9b17410 | ||
|
|
c78fdf87b8 | ||
|
|
55a1ccc849 | ||
|
|
d97f42ff2d | ||
|
|
9ab52aeaf2 | ||
|
|
a0190143fe | ||
|
|
a48135a60d | ||
|
|
09eec3235d | ||
|
|
d21e5dfee4 | ||
|
|
899a8d746a | ||
|
|
9bbfc2de3f | ||
|
|
d82bb22341 | ||
|
|
0fd55c6635 | ||
|
|
4b346dd2e1 | ||
|
|
13a0516cce | ||
|
|
5fcd7ccb58 | ||
|
|
b0aef46c99 | ||
|
|
ec50530284 | ||
|
|
cbc4ebe7b3 | ||
|
|
f5339db646 | ||
|
|
c573e70e8b | ||
|
|
16b3049839 | ||
|
|
57ff8e9d22 | ||
|
|
5c6ea23e0f | ||
|
|
5a2aa7cd4b | ||
|
|
3df53b582a | ||
|
|
eb53fc472c | ||
|
|
c4e9178efb | ||
|
|
822f41bc40 | ||
|
|
1558c0a62f | ||
|
|
3977bb2a0b | ||
|
|
26df3a1d1d | ||
|
|
a77a860e0c | ||
|
|
78b637c83b | ||
|
|
b132178228 | ||
|
|
4021389a4d | ||
|
|
089be99287 | ||
|
|
b3dd6acfe6 | ||
|
|
ec3645a1c9 | ||
|
|
1c49873da1 | ||
|
|
8818bd90e0 | ||
|
|
4fb95799f8 | ||
|
|
2ee9084b91 | ||
|
|
3f3ef27d6b | ||
|
|
e01e59b72c | ||
|
|
0a97f04257 | ||
|
|
0b3888a8ae | ||
|
|
c6601e5bbf | ||
|
|
5f7c5d25de | ||
|
|
99d80df76c | ||
|
|
22beeabb9b | ||
|
|
8e1aad655a | ||
|
|
942447c62f | ||
|
|
d8a4da7ec8 | ||
|
|
56c50eacfe | ||
|
|
eec6f7d168 | ||
|
|
b45219a595 | ||
|
|
d7858f17a1 | ||
|
|
000a99c53e | ||
|
|
e592d26f8b | ||
|
|
015a60f998 | ||
|
|
caba43bb5b | ||
|
|
056425bd8a | ||
|
|
4aca62c042 | ||
|
|
b597c655cd | ||
|
|
9b276009e2 | ||
|
|
c1dac2e064 | ||
|
|
f707993188 | ||
|
|
ea612d9d53 | ||
|
|
b44e737448 | ||
|
|
bb429afd95 | ||
|
|
475a8f8a28 | ||
|
|
c7ba5ca894 | ||
|
|
3023f0a7cc | ||
|
|
ddaefbc952 | ||
|
|
0b3a0fb3ed | ||
|
|
7f40a430fd | ||
|
|
05f5d3b25c | ||
|
|
c3ca0b18b3 | ||
|
|
696e0b1fa7 | ||
|
|
201f7dbd3e | ||
|
|
0bfd3e906c | ||
|
|
71ac2bfc45 | ||
|
|
5370db7c5e | ||
|
|
bcc30e40ba | ||
|
|
2f70f654f7 | ||
|
|
b64115dcbd | ||
|
|
c9c71d8582 | ||
|
|
689bc19296 | ||
|
|
27498ab649 | ||
|
|
678a11f998 | ||
|
|
e9ef98716f | ||
|
|
b3ce43eaf7 | ||
|
|
72083b7e87 | ||
|
|
0cc94c2033 | ||
|
|
1d6296b400 | ||
|
|
7ad5da2a9e | ||
|
|
a7665a9994 | ||
|
|
a4cd3f26e8 | ||
|
|
fcdb33b64b | ||
|
|
7fd6119bcf | ||
|
|
b4d4b2473c | ||
|
|
91f715c3c3 | ||
|
|
ea5fccfe5f | ||
|
|
86835eec73 | ||
|
|
2bccee2333 | ||
|
|
2d01b0d714 | ||
|
|
44bf37b05a | ||
|
|
cf617f0a64 | ||
|
|
eeeaffd883 | ||
|
|
d178302d34 | ||
|
|
83a5364903 | ||
|
|
aef76db664 | ||
|
|
c3b3240191 | ||
|
|
f381974955 | ||
|
|
bd16dd98c4 | ||
|
|
2fca6132a0 | ||
|
|
137eba33c9 | ||
|
|
143699c0a4 | ||
|
|
0485403fff | ||
|
|
489fcb9666 | ||
|
|
7cc3b84ebc | ||
|
|
cb254f87d4 | ||
|
|
d4db98fd64 | ||
|
|
d14a6d8311 | ||
|
|
286c115167 | ||
|
|
6038b9e052 | ||
|
|
552082a36a | ||
|
|
5cea92d96d | ||
|
|
02b7b89b94 | ||
|
|
93697cf1f5 | ||
|
|
8daaee28c3 | ||
|
|
c32f608ec5 | ||
|
|
7b09029c5b | ||
|
|
6e1c414c84 | ||
|
|
e57976be99 | ||
|
|
a37e6a3f4c | ||
|
|
2dbe4064b2 | ||
|
|
2b0c0d467a | ||
|
|
40fa4516df | ||
|
|
5201c0cd14 | ||
|
|
61039dcd7e | ||
|
|
039ff4ee41 | ||
|
|
b40349805f | ||
|
|
d709d119ac | ||
|
|
8d2b6bdc12 | ||
|
|
ff78af2d56 | ||
|
|
ada53dba3b | ||
|
|
ba2f6c0f66 | ||
|
|
268869345c | ||
|
|
4b556bd3a9 | ||
|
|
6f10d35a4c | ||
|
|
33167fcdce | ||
|
|
e9c85b0e77 | ||
|
|
e521254600 | ||
|
|
a773d98400 | ||
|
|
ae066d3cd9 | ||
|
|
b5726fc0f3 | ||
|
|
4a056a0d27 | ||
|
|
7817431bce | ||
|
|
c02d2745c3 | ||
|
|
ee610ec800 | ||
|
|
6c0d585fef | ||
|
|
29417005b0 | ||
|
|
cf87fd8340 | ||
|
|
f1b85b0dde | ||
|
|
abef73d384 | ||
|
|
535f947f88 | ||
|
|
f27e243cc4 | ||
|
|
6a699ed5f1 | ||
|
|
9c1f5efab5 | ||
|
|
6b7ce56f6b | ||
|
|
b76ee4a2d0 | ||
|
|
b444a74a44 | ||
|
|
d43820cc82 | ||
|
|
e74e8fe1c2 | ||
|
|
9eb6e8ec27 | ||
|
|
fae94d3696 | ||
|
|
68e5ed64c9 | ||
|
|
f912d3b8bd | ||
|
|
fc03d2ee91 | ||
|
|
523b2b8db4 | ||
|
|
d547e9b6d7 | ||
|
|
71efc9f854 | ||
|
|
4f289f7467 | ||
|
|
02ef8bee71 | ||
|
|
ff5c1b00d7 | ||
|
|
30264be311 | ||
|
|
8ea44ab8c7 | ||
|
|
1b8ff7ca61 | ||
|
|
f00a066c22 | ||
|
|
859cf468aa | ||
|
|
5b486a917b | ||
|
|
9ace6b70f0 | ||
|
|
447029ae70 | ||
|
|
83f26cde53 | ||
|
|
8ac52690fd | ||
|
|
6934b2bd27 | ||
|
|
6647e4fcd4 | ||
|
|
21710f55f3 | ||
|
|
27bd9a7489 | ||
|
|
630d37125c | ||
|
|
9424237534 | ||
|
|
cba3fbeb5f | ||
|
|
58778ccf43 | ||
|
|
6c61d47d78 | ||
|
|
35e02f9d98 | ||
|
|
58c1650863 | ||
|
|
9b14ffa14c | ||
|
|
96c09bf4cd | ||
|
|
737cec744a | ||
|
|
13ed92bb94 | ||
|
|
076594c78e | ||
|
|
b6b1b4ebbe | ||
|
|
4007f37492 | ||
|
|
532d671feb | ||
|
|
fed7a1ac84 | ||
|
|
ddfd170ea8 | ||
|
|
bae5c67dfa | ||
|
|
84f51603fb | ||
|
|
f73ddc03e9 | ||
|
|
a16d9877cc | ||
|
|
c24e9e083c | ||
|
|
101602c6f6 | ||
|
|
18a7bd1fd1 | ||
|
|
dfbd556bb8 | ||
|
|
040cdde8ba | ||
|
|
06373480ae | ||
|
|
5713a78f2e | ||
|
|
b9f2f17a24 | ||
|
|
9adc993472 | ||
|
|
dcd5f3d529 | ||
|
|
18e70a0e6b | ||
|
|
5ad57d1608 | ||
|
|
74eaf48ceb | ||
|
|
30bb0cb291 | ||
|
|
b50e6b93bd | ||
|
|
a0b5a1462d | ||
|
|
4910f93c94 | ||
|
|
4a52bd0cb7 | ||
|
|
b0bfb73952 | ||
|
|
69d049a69a | ||
|
|
7d75153362 | ||
|
|
748bfa31ae | ||
|
|
e7d995edbc | ||
|
|
a144fb2e48 | ||
|
|
7521013e11 | ||
|
|
c6321fc6b2 | ||
|
|
7d92d5d096 | ||
|
|
ab201d5016 | ||
|
|
efa38d5ee9 | ||
|
|
e8769d09a8 | ||
|
|
a216444825 | ||
|
|
fee3e10e6b | ||
|
|
4d71a8f3c2 | ||
|
|
fc104b0b01 | ||
|
|
3dcb351b36 | ||
|
|
600d05d08f | ||
|
|
6b6ff70ad3 | ||
|
|
891f660738 | ||
|
|
6901b9b728 | ||
|
|
c7f211a7f8 | ||
|
|
c48ea1152c | ||
|
|
f5d0eb94b4 | ||
|
|
cebeef04a0 | ||
|
|
3e77a83ca6 | ||
|
|
c872b335e7 | ||
|
|
cc1e173552 | ||
|
|
35e0567705 | ||
|
|
fb2add305e | ||
|
|
74d4c18c4c | ||
|
|
da3ce07485 | ||
|
|
c7ab179a9e | ||
|
|
6fd11fcd56 | ||
|
|
3966cf165b | ||
|
|
0b2ada5d1c | ||
|
|
4278101bbe | ||
|
|
8b43af49fc | ||
|
|
6e29e8426b | ||
|
|
af11d3c771 | ||
|
|
e5c5af4d57 | ||
|
|
3dbdf5adf2 | ||
|
|
4d7a030b70 | ||
|
|
3351262dd7 | ||
|
|
5ec4377502 | ||
|
|
9c8402c3a5 | ||
|
|
928a45e48e | ||
|
|
1d088c5eae | ||
|
|
cdcf81ab7c | ||
|
|
9f196bafe9 | ||
|
|
5c9e1406a1 | ||
|
|
0b42e00b29 | ||
|
|
88b98a138f | ||
|
|
136c37885d | ||
|
|
812988b31a | ||
|
|
191680a01b | ||
|
|
467d1a754d | ||
|
|
d1973922cd | ||
|
|
3b7689975d | ||
|
|
3386a71c5e | ||
|
|
7bb65a5e76 | ||
|
|
f3a9c8e0e2 | ||
|
|
22861ca8d0 | ||
|
|
19118ea241 | ||
|
|
4a9dc7249f | ||
|
|
5dad9c2eb8 | ||
|
|
d6b35b00b9 | ||
|
|
fda8ab500b | ||
|
|
66df421de2 | ||
|
|
33c62f08ca | ||
|
|
b660602809 | ||
|
|
6dfce2ca30 | ||
|
|
655e20e99e | ||
|
|
f2b80bdc08 | ||
|
|
10af873fa5 | ||
|
|
d87a5b14f8 | ||
|
|
b87a18b993 | ||
|
|
c4185034e4 | ||
|
|
9d64426b00 | ||
|
|
c81cc8bea4 | ||
|
|
90e680d6be | ||
|
|
04c0833111 | ||
|
|
06151eab3b | ||
|
|
3dcb8590f6 | ||
|
|
a9b313aa4a | ||
|
|
1f2e35060b | ||
|
|
a96862fffa | ||
|
|
68cb8e194d | ||
|
|
c164926c54 | ||
|
|
de7516116d | ||
|
|
fccfe5b088 | ||
|
|
23aa5fa0a3 | ||
|
|
d384c0a141 | ||
|
|
18058c2a36 | ||
|
|
71727202f3 | ||
|
|
eee0b949de | ||
|
|
3cbbb67b0c | ||
|
|
7879f66e78 | ||
|
|
c14ac37495 | ||
|
|
73a77183aa | ||
|
|
09cfa21091 | ||
|
|
c193571ece | ||
|
|
04bc92b071 | ||
|
|
94e58a449c | ||
|
|
9d044195aa | ||
|
|
caff34cc3b | ||
|
|
34c5c0b1f7 | ||
|
|
906801e13c | ||
|
|
dad4c6b866 | ||
|
|
090462022f | ||
|
|
cbf9f65fb4 | ||
|
|
5a493cd55d | ||
|
|
dfc204ef05 | ||
|
|
56c6e2d29c | ||
|
|
db03dd12a0 | ||
|
|
6c67e6363a | ||
|
|
e2888beb4c | ||
|
|
bba9166885 | ||
|
|
504e4eab3e | ||
|
|
2e475c35cc | ||
|
|
ccf18758fb | ||
|
|
68f9852790 | ||
|
|
d0150de003 | ||
|
|
e2b792335b | ||
|
|
ece38c9e59 | ||
|
|
a19b5090bf | ||
|
|
e4b3c35892 | ||
|
|
4b229a759a | ||
|
|
1e9e42ac48 | ||
|
|
245a48f66e | ||
|
|
e6d8397550 | ||
|
|
d59bd43846 | ||
|
|
c1579c83c7 | ||
|
|
4d782e60ad | ||
|
|
c702f47927 | ||
|
|
9110cfd923 | ||
|
|
e40dd14bbf | ||
|
|
90aaae9959 | ||
|
|
e81dda0fa8 | ||
|
|
f93796d036 | ||
|
|
d06359cb81 | ||
|
|
8b68fb578f | ||
|
|
cca300e419 | ||
|
|
77c3ec0bbe | ||
|
|
ed81fc576a | ||
|
|
435fcb9669 | ||
|
|
9020d95b62 | ||
|
|
84d7a501d4 | ||
|
|
e65dd49d69 | ||
|
|
a705cbe6c2 | ||
|
|
60b8af3860 | ||
|
|
9ac4187aa8 | ||
|
|
6419d29489 | ||
|
|
4684e43f42 | ||
|
|
a477c9fa6d | ||
|
|
d1be331f99 | ||
|
|
cbc792d406 | ||
|
|
0313c5c560 | ||
|
|
18aa2fcd92 | ||
|
|
10461941d7 | ||
|
|
e6050219bc | ||
|
|
81481c37fe | ||
|
|
5ea92a7d18 | ||
|
|
f40630aced | ||
|
|
81850acdfe | ||
|
|
6819d5aa8b | ||
|
|
2aef4e5d05 | ||
|
|
6d4d2c3e7e | ||
|
|
87bcaa4731 | ||
|
|
5d2378f291 | ||
|
|
253507d14b | ||
|
|
548fb7099b | ||
|
|
0dd7c777ee | ||
|
|
6812bf2388 | ||
|
|
12bcbfa9f7 | ||
|
|
b5dfd371d9 | ||
|
|
e09d7fb103 | ||
|
|
0fe3afe254 | ||
|
|
db50d50c19 | ||
|
|
691bdb1512 | ||
|
|
d50b712bca | ||
|
|
3b68e4f32b | ||
|
|
259b9a90dd | ||
|
|
f4c5fd7eb4 | ||
|
|
3cd42d03f0 | ||
|
|
3497b82e8c | ||
|
|
15a24e4e75 | ||
|
|
96837f908e | ||
|
|
4ea5ebbf9e | ||
|
|
281e015376 | ||
|
|
5825a16aff | ||
|
|
2586a8c433 | ||
|
|
9f7c9c3428 | ||
|
|
9790ba735b | ||
|
|
e3dbcac9fb | ||
|
|
1c99929429 | ||
|
|
9b2cdbbb18 | ||
|
|
928cf1220e | ||
|
|
c0557856a3 | ||
|
|
97c2cc3d15 | ||
|
|
a94ef980bb | ||
|
|
eea0c24d2b | ||
|
|
c8fded3c56 | ||
|
|
8f2ba5e186 | ||
|
|
5ce2823d0b | ||
|
|
a0c70d326f | ||
|
|
5f28fd4114 | ||
|
|
7151db0909 | ||
|
|
e82888f8f3 | ||
|
|
4fb60a6ec6 | ||
|
|
27f22f6094 | ||
|
|
7497a0151a | ||
|
|
41f133afb1 | ||
|
|
4b15ecbc1b | ||
|
|
6498130850 | ||
|
|
24bd1121af | ||
|
|
3cccf741d6 | ||
|
|
0a2d2c3f43 | ||
|
|
969da0f2a6 | ||
|
|
2061b68a2f | ||
|
|
443dea5055 | ||
|
|
a4c6365ede | ||
|
|
c9c044386e | ||
|
|
2744f8285c | ||
|
|
7bf5f20b06 | ||
|
|
b43aa84c2a | ||
|
|
dd27d88309 | ||
|
|
8dc36a72b2 | ||
|
|
d3ca301675 | ||
|
|
43e3469e63 | ||
|
|
cdc3dc6740 | ||
|
|
6fba8b61e7 | ||
|
|
b34594a1dc | ||
|
|
19964d253e | ||
|
|
165f3ed25a | ||
|
|
5058290103 | ||
|
|
358a6029a1 | ||
|
|
fa4bfa729d | ||
|
|
9c9e43cf46 | ||
|
|
b7e5bd0144 | ||
|
|
58dc6f5832 | ||
|
|
f409af1c37 | ||
|
|
9e0c94f1a4 | ||
|
|
3794d61a77 | ||
|
|
d22da54d53 | ||
|
|
8e34c44e0d | ||
|
|
b71434acf6 | ||
|
|
7e158ed9b9 | ||
|
|
2ec0d067f3 | ||
|
|
effc65b777 | ||
|
|
c48e248283 | ||
|
|
f9e9a4547c | ||
|
|
63e35aba6d | ||
|
|
8f852fb9ac | ||
|
|
bf6a13b43f | ||
|
|
12030f6ce9 | ||
|
|
07da878bba | ||
|
|
8d5c3bdec8 | ||
|
|
ce95772afa | ||
|
|
b9f27b2b00 | ||
|
|
0059cabebe | ||
|
|
326ee79c8c | ||
|
|
54cc265ee6 | ||
|
|
e38778b4d0 | ||
|
|
6152d3c14a | ||
|
|
8a172170ea | ||
|
|
64b5d64709 | ||
|
|
67d7315003 | ||
|
|
47da4a2a1a | ||
|
|
174be9c2d1 | ||
|
|
9b68539322 | ||
|
|
2a4660ffa6 | ||
|
|
dce0cf7ee4 | ||
|
|
d6c39d4aba | ||
|
|
fd7e183f40 | ||
|
|
bf78a80f29 | ||
|
|
0ff630b8bd | ||
|
|
49b9e3f278 | ||
|
|
a4cc65c6a4 | ||
|
|
0b46187ac5 | ||
|
|
14ef5af936 | ||
|
|
539d9c6d0e | ||
|
|
56bcc5ef5e | ||
|
|
d6b0324e24 | ||
|
|
ff044e2592 | ||
|
|
3c7747ab97 | ||
|
|
34d97221ed | ||
|
|
84e78d34cd | ||
|
|
ac73806aee | ||
|
|
2105e9a5c9 | ||
|
|
2a36cc4327 | ||
|
|
c3feaf9a15 | ||
|
|
d8537a98aa | ||
|
|
42a6001ba5 | ||
|
|
4d9eb35230 | ||
|
|
e4ac296a1f | ||
|
|
01b49e7864 | ||
|
|
bd0b85a8d2 | ||
|
|
3d59a4c516 | ||
|
|
08ceff0f03 | ||
|
|
d6ae88ac43 | ||
|
|
5c8f016dd6 | ||
|
|
17288017d8 | ||
|
|
1e2757b52f | ||
|
|
0dce2f057e | ||
|
|
e017c5c304 | ||
|
|
a3e828f90a | ||
|
|
74e5c24fdc | ||
|
|
76c0abaa22 | ||
|
|
a52b5fd711 | ||
|
|
ffa51406b6 | ||
|
|
0b3b267e63 | ||
|
|
fcdb9d8257 | ||
|
|
04943ca525 | ||
|
|
574d4a1223 | ||
|
|
7349814cb2 | ||
|
|
114c5eb356 | ||
|
|
191f861f6e | ||
|
|
fac1fcc3a6 | ||
|
|
d0490c5eb5 | ||
|
|
2673efa9fc | ||
|
|
d4bce7b0a1 | ||
|
|
ba4a7ce6ab | ||
|
|
58f10153ab | ||
|
|
e7b65e3f26 | ||
|
|
fe91473748 | ||
|
|
0140402ad4 | ||
|
|
f56cba59ae | ||
|
|
fed74f05fc | ||
|
|
0888f11257 | ||
|
|
7205d5bb9c | ||
|
|
17a5ef882f | ||
|
|
ea68dbc56f | ||
|
|
0cec8af074 | ||
|
|
f7d0fc5768 | ||
|
|
bcaab694c8 | ||
|
|
247a3d5ab3 | ||
|
|
8e262a1e10 | ||
|
|
f63695bdc7 | ||
|
|
b051613b62 | ||
|
|
b886379d34 | ||
|
|
2a780dd2bb | ||
|
|
9cf7b80110 | ||
|
|
8fee73f1d1 | ||
|
|
36edb9373b | ||
|
|
374c4b265a | ||
|
|
db0b685ae1 | ||
|
|
23d33b8402 | ||
|
|
8a57be3e63 | ||
|
|
823cb03f9b | ||
|
|
e96cbcb057 | ||
|
|
fa0e7bcb54 | ||
|
|
20292a7742 | ||
|
|
943bde7eed | ||
|
|
9701af0736 | ||
|
|
1456cc40e1 | ||
|
|
dc1f88c44c | ||
|
|
55c916956f | ||
|
|
51eda57618 | ||
|
|
d6a55e1ec0 | ||
|
|
b78210421c | ||
|
|
1324269f1d | ||
|
|
cda6cb5cc0 | ||
|
|
c1b8619b26 | ||
|
|
4203e25321 | ||
|
|
aa02c7b93a | ||
|
|
0ff477579b | ||
|
|
62a8e8c119 | ||
|
|
fa212e0911 | ||
|
|
c8ad902a60 | ||
|
|
f05515d7d6 | ||
|
|
95bbcce941 | ||
|
|
d6b98f1518 | ||
|
|
bd9b1b11c5 | ||
|
|
e4c4960972 | ||
|
|
2a26031261 | ||
|
|
1d6e212955 | ||
|
|
9fa3743d21 | ||
|
|
7b373c79d9 | ||
|
|
4e9266e2d5 | ||
|
|
ea957e297c | ||
|
|
9320b6beb8 | ||
|
|
1319bf4a8c |
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
# ignore everything
|
||||
*
|
||||
|
||||
# allow only what we need
|
||||
!commafeed-server/target/commafeed.jar
|
||||
!commafeed-server/config.yml.example
|
||||
18
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- enhancement
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
89
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
name: Java CI
|
||||
|
||||
on: [ push ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ "8", "11", "17" ]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java }}
|
||||
distribution: "temurin"
|
||||
cache: "maven"
|
||||
|
||||
# Build
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --update-snapshots verify
|
||||
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.java == '8' }}
|
||||
with:
|
||||
name: commafeed.jar
|
||||
path: commafeed-server/target/commafeed.jar
|
||||
|
||||
# Docker
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v2
|
||||
if: ${{ matrix.java == '8' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker build and push tag
|
||||
uses: docker/build-push-action@v4
|
||||
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7
|
||||
tags: |
|
||||
athou/commafeed:latest
|
||||
athou/commafeed:${{ github.ref_name }}
|
||||
|
||||
- name: Docker build and push master
|
||||
uses: docker/build-push-action@v4
|
||||
if: ${{ matrix.java == '8' && github.ref_name == 'master' }}
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7
|
||||
tags: athou/commafeed:master
|
||||
|
||||
# Create GitHub release after Docker image has been published
|
||||
- name: Extract Changelog Entry
|
||||
uses: mindsers/changelog-reader-action@v2
|
||||
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
|
||||
id: changelog_reader
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ matrix.java == '8' && github.ref_type == 'tag' }}
|
||||
with:
|
||||
name: CommaFeed ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog_reader.outputs.changes }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
commafeed-server/target/commafeed.jar
|
||||
commafeed-server/config.yml.example
|
||||
7
.gitignore
vendored
@@ -1,8 +1,12 @@
|
||||
# config gile
|
||||
# config file
|
||||
config.yml
|
||||
|
||||
# build directory
|
||||
target
|
||||
target-ide
|
||||
|
||||
# database files
|
||||
database
|
||||
|
||||
# log files
|
||||
log
|
||||
@@ -22,6 +26,7 @@ src/main/app/lib
|
||||
.classpath
|
||||
.settings
|
||||
.factorypath
|
||||
.checkstyle
|
||||
|
||||
# IntelliJ Idea files
|
||||
.idea
|
||||
|
||||
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
18
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
|
||||
141
CHANGELOG.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Changelog
|
||||
|
||||
## [3.3.0]
|
||||
|
||||
- there are now database changes, rolling back to 2.x will no longer be possible
|
||||
- restore support for user custom CSS rules
|
||||
- add support for user custom JS code that will be executed on page load
|
||||
|
||||
|
||||
## [3.2.0]
|
||||
|
||||
- restore the welcome page
|
||||
- only apply hover effect for unread entries (same as commafeed v2)
|
||||
- move notifications at the bottom of the screen
|
||||
- always use https for sharing urls
|
||||
- add support for redis ACLs
|
||||
- transition to google analytics v4
|
||||
|
||||
## [3.1.0]
|
||||
|
||||
- add an even more compact layout
|
||||
- restore hover effect from commafeed 2.x
|
||||
- view mode (compact, expanded, ...) is now stored on the device so you can have a different view mode on desktop and
|
||||
mobile
|
||||
- fix for the "Illegal attempt to associate a collection with two open sessions." error
|
||||
- feed fetching workflow is now orchestrated with rxjava, removing a lot of code
|
||||
|
||||
## [3.0.1]
|
||||
|
||||
- allow env variable substitution in config.yml
|
||||
- e.g. having a custom config.yml file with `app.session.path=${SOME_ENV_VAR}` will substitute `SOME_ENV_VAR` with
|
||||
its value
|
||||
- allow env variable prefixed with `CF_` to override config.yml properties
|
||||
- e.g. setting `CF_APP_ALLOWREGISTRATIONS=true` will set `app.allowRegistrations` to `true`
|
||||
|
||||
## [3.0.0]
|
||||
|
||||
- complete overhaul of the UI
|
||||
- backend and frontend are now in separate maven modules
|
||||
- no changes to the api or the database
|
||||
- Docker images are now automatically built and available at https://hub.docker.com/r/athou/commafeed
|
||||
|
||||
## [2.6.0]
|
||||
|
||||
- add support for media content as a backup for missing content (useful for youtube feeds)
|
||||
- correctly follow http error code 308 redirects
|
||||
- fixed a bug that prevented users from deleting their account
|
||||
- fixed a bug that made commafeed store entry contents multiple times
|
||||
- fixed a bug that prevented the app to be used as an installed app on mobile devices if the context path of commafeed
|
||||
was not "/"
|
||||
- fixed a bug that prevented entries from being "marked as read older than xxx" for a feed that was just added
|
||||
- removed support for google+ and readability as those services no longer exist
|
||||
- removed support for deploying on openshift
|
||||
- removed alphabetical sorting of entries because of really poor performance (title cannot be indexed)
|
||||
- improve performance for instances with the heavy load setting enabled by preventing CommaFeed from fetching feeds from
|
||||
users that did not log in for a long time
|
||||
- various dependencies upgrades (notably dropwizard from 1.3 to 2.1)
|
||||
- add support for mariadb
|
||||
- add support for java17+ runtime
|
||||
- various security improvements
|
||||
|
||||
## [2.5.0]
|
||||
|
||||
- unread count is now displayed in a favicon badge when supported
|
||||
- the user agent string for the bot fetching feeds is now configurable
|
||||
- feed parsing performance improvements
|
||||
- support for java9+ runtime
|
||||
- can now properly start from an empty postgresql database
|
||||
|
||||
## [2.4.0]
|
||||
|
||||
- users were not able to change password or delete account
|
||||
- fix api key generation
|
||||
- feed entries can now be sorted alphabetically
|
||||
- fix facebook sharing
|
||||
- fix layout on iOS
|
||||
- postgresql driver update (fix for postgres 9.6)
|
||||
- various internationalization fixes
|
||||
- security fixes
|
||||
|
||||
## [2.3.0]
|
||||
|
||||
- dropwizard upgrade 0.9.1
|
||||
- feed enclosures are hidden if they already displayed in the content
|
||||
- fix youtube favicons
|
||||
- various internationalization fixes
|
||||
|
||||
## [2.2.0]
|
||||
|
||||
- fix youtube and instagram favicon fetching
|
||||
- mark as read filter was lost when a feed was rearranged with drag&drop
|
||||
- feed entry categories are now displayed if available
|
||||
- various performance and dependencies upgrades
|
||||
- java8 is now required
|
||||
|
||||
## [2.1.0]
|
||||
|
||||
- dropwizard upgrade to 0.8.0
|
||||
- you have to remove the "app.contextPath" setting from your yml file, you can optionally use
|
||||
server.applicationContextPath instead
|
||||
- new setting app.maxFeedCapacity for deleting old entries
|
||||
- ability to set filtering expressions for subscriptions to automatically mark new entries as read based on title,
|
||||
content, author or url.
|
||||
- ability to use !keyword or -keyword to exclude a keyword from a search query
|
||||
- facebook feeds now show user favicon instead of facebook favicon
|
||||
- new dark theme 'nightsky'
|
||||
|
||||
## [2.0.3]
|
||||
|
||||
- internet explorer ajax cache workaround
|
||||
- categories are now deletable again
|
||||
- openshift support is back
|
||||
- youtube feeds now show user favicon instead of youtube favicon
|
||||
|
||||
## [2.0.2]
|
||||
|
||||
- api using the api key is now working again
|
||||
- context path is now configurable in config.yml (see app.contextPath in config.yml.example)
|
||||
- fix login on firefox when fields are autofilled by the browser
|
||||
- fix scrolling of subscriptions list on mobile
|
||||
- user is now logged in after registration
|
||||
- fix link to documentation on home page and about page
|
||||
- fields autocomplete is disabled on the profile page
|
||||
- users are able to delete their account again
|
||||
- chinese and malaysian translation files are now correctly loaded
|
||||
- software version in user-agent when fetching feeds is no longer hardcoded
|
||||
- admin settings page is now read only, settings are configured in config.yml
|
||||
- added link to metrics on the admin settings page
|
||||
- Rome (rss library) upgrade to 1.5.0
|
||||
|
||||
## [2.0.1]
|
||||
|
||||
- the redis pool no longer throws an exception when it is unable to aquire a new connection
|
||||
|
||||
## [2.0.0]
|
||||
|
||||
- The backend has been completely rewritten using Dropwizard instead of TomEE, resulting in a lot less memory
|
||||
consumption and better overall performances.
|
||||
See the README on how to build CommaFeed from now on.
|
||||
- CommaFeed should no longer fetch the same feed multiple times in a row
|
||||
- Users can use their username or email to log in
|
||||
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM eclipse-temurin:17-jre
|
||||
|
||||
EXPOSE 8082
|
||||
|
||||
RUN mkdir -p /commafeed/data
|
||||
VOLUME /commafeed/data
|
||||
ENV CF_SESSION_PATH=/commafeed/data/sessions
|
||||
|
||||
COPY commafeed-server/config.yml.example config.yml
|
||||
COPY commafeed-server/target/commafeed.jar .
|
||||
|
||||
CMD ["java", "-Djava.net.preferIPv4Stack=true", "-jar", "commafeed.jar", "server", "config.yml"]
|
||||
192
README.md
@@ -1,90 +1,102 @@
|
||||
CommaFeed [](https://buildhive.cloudbees.com/job/Athou/job/commafeed/)
|
||||
=========
|
||||
Sources for [CommaFeed.com](http://www.commafeed.com/).
|
||||
|
||||
Google Reader inspired self-hosted RSS reader, based on Dropwizard and AngularJS.
|
||||
|
||||
Related open-source projects
|
||||
----------------------------
|
||||
|
||||
Android apps: [News+ extension](https://github.com/Athou/commafeed-newsplus) - [Android app](https://github.com/doomrobo/CommaFeed-Android-Reader)
|
||||
|
||||
Browser extensions: [Chrome](https://github.com/Athou/commafeed-chrome) - [Firefox](https://github.com/Athou/commafeed-firefox) - [Opera](https://github.com/Athou/commafeed-opera) - [Safari](https://github.com/Athou/commafeed-safari)
|
||||
|
||||
Deployment on your own server
|
||||
-----------------------------
|
||||
|
||||
CommaFeed 2.0 has been rewritten to use Dropwizard and gulp instead of using tomee and wro4j. The latest version of the 1.x branch is available [here](https://github.com/Athou/commafeed/tree/1.x).
|
||||
|
||||
For storage, you can either use an embedded H2 database or an external MySQL, PostgreSQL or SQLServer database.
|
||||
You also need Maven 3.x (and a Java 1.7+ JDK) installed in order to build the application.
|
||||
|
||||
To install maven and openjdk on Ubuntu, issue the following commands
|
||||
|
||||
sudo add-apt-repository ppa:natecarlson/maven3
|
||||
sudo apt-get update
|
||||
sudo apt-get install openjdk-7-jdk maven3
|
||||
|
||||
# Not required but if you don't, use 'mvn3' instead of 'mvn' for the rest of the instructions.
|
||||
sudo ln -s /usr/bin/mvn3 /usr/bin/mvn
|
||||
|
||||
On Windows and other operating systems, just download maven 3.x from the [official site](http://maven.apache.org/), extract it somewhere and add the `bin` directory to your `PATH` environment variable.
|
||||
|
||||
Clone this repository. If you don't have git you can download the sources as a zip file from [here](https://github.com/Athou/commafeed/archive/master.zip)
|
||||
|
||||
git clone https://github.com/Athou/commafeed.git
|
||||
cd commafeed
|
||||
|
||||
Now build the application
|
||||
|
||||
mvn clean package
|
||||
|
||||
Copy `config.yml.example` to `config.yml` then edit the file to your liking.
|
||||
Issue the following command to run the app, the server will listen by default on ``http://localhost:8082`. The default user is `admin` and the default password is `admin`.
|
||||
|
||||
java -jar target/commafeed.jar server config.yml
|
||||
|
||||
You can use nginx or apache as a proxy http server. Note that when using apache, the `ProxyPreserveHost on` option should be set in your config file.
|
||||
|
||||
Local development
|
||||
-----------------
|
||||
|
||||
To start the dropwizard backend, use your IDE to run CommaFeedApplication as your main class, and pass `server config.dev.yml` as arguments to the program.
|
||||
To start the client-side webserver with watches on assets, run `gulp dev`. The server is now running on port 8082 and is proxying REST requests to dropwizard on port 8083.
|
||||
|
||||
|
||||
Translate CommaFeed into your language
|
||||
--------------------------------------
|
||||
|
||||
Files for internationalization are located [here](https://github.com/Athou/commafeed/tree/master/src/main/app/i18n).
|
||||
|
||||
To add a new language, create a new file in that directory.
|
||||
The name of the file should be the two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
|
||||
The language has to be referenced in the `src/main/app/js/i18n.js` file to be picked up.
|
||||
|
||||
Themes
|
||||
---------------------
|
||||
|
||||
To create a theme, create a new file `src/main/webapp/sass/themes/_<theme>.scss`. Your styles should be wrapped in a `#theme-<theme>` element and use the [SCSS format](http://sass-lang.com/) which is a superset of CSS.
|
||||
|
||||
Don't forget to reference your theme in `src/main/webapp/sass/app.scss` and in `src/main/webapp/js/controllers.js` (look for `$scope.themes`).
|
||||
|
||||
See [_test.scss](https://github.com/Athou/commafeed/blob/master/src/main/webapp/sass/themes/_test.scss) for an example.
|
||||
|
||||
|
||||
Copyright and license
|
||||
---------------------
|
||||
|
||||
Copyright 2013-2014 CommaFeed.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this work except in compliance with the License.
|
||||
You may obtain a copy of the License in the LICENSE file, or at:
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
# CommaFeed
|
||||
|
||||
Google Reader inspired self-hosted RSS reader, based on Dropwizard and React/TypeScript.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- 4 different layouts
|
||||
- Dark theme
|
||||
- Fully responsive
|
||||
- Keyboard shortcuts for almost everything
|
||||
- Support for right-to-left feeds
|
||||
- Translated in 25+ languages
|
||||
- Supports thousands of users and millions of feeds
|
||||
- OPML import/export
|
||||
- REST API
|
||||
|
||||
## Related open-source projects
|
||||
|
||||
Browser extensions:
|
||||
|
||||
- [Chrome](https://github.com/Athou/commafeed-chrome)
|
||||
- [Firefox](https://github.com/Athou/commafeed-firefox)
|
||||
- [Opera](https://github.com/Athou/commafeed-opera)
|
||||
- [Safari](https://github.com/Athou/commafeed-safari)
|
||||
|
||||
## Deployment on your own server
|
||||
|
||||
### Docker
|
||||
|
||||
Docker images are built automatically and are available at https://hub.docker.com/r/athou/commafeed
|
||||
|
||||
### Download precompiled package
|
||||
|
||||
mkdir commafeed && cd commafeed
|
||||
wget https://github.com/Athou/commafeed/releases/latest/download/commafeed.jar
|
||||
wget https://github.com/Athou/commafeed/releases/latest/download/config.yml.example -O config.yml
|
||||
java -Djava.net.preferIPv4Stack=true -jar commafeed.jar server config.yml
|
||||
|
||||
The server will listen on http://localhost:8082. The default
|
||||
user is `admin` and the default password is `admin`.
|
||||
|
||||
### Build from sources
|
||||
|
||||
git clone https://github.com/Athou/commafeed.git
|
||||
cd commafeed
|
||||
./mvnw clean package
|
||||
cp commafeed-server/config.yml.example config.yml
|
||||
java -Djava.net.preferIPv4Stack=true -jar commafeed-server/target/commafeed.jar server config.yml
|
||||
|
||||
The server will listen on http://localhost:8082. The default
|
||||
user is `admin` and the default password is `admin`.
|
||||
|
||||
## Translation
|
||||
|
||||
Files for internationalization are
|
||||
located [here](https://github.com/Athou/commafeed/tree/master/commafeed-client/src/locales).
|
||||
|
||||
To add a new language:
|
||||
|
||||
- edit `commafeed-client/src/i18n.ts`
|
||||
- add the new locale to the `locales` array.
|
||||
- import the dayjs locale
|
||||
- edit `commafeed-client/.linguirc` and add the new locale to the `locales` array.
|
||||
- run `npm run i18n` and add translations to the newly created `commafeed-client/src/locales/[locale]/messages.po` file
|
||||
|
||||
The name of the locale should be the
|
||||
two-letters [ISO-639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
|
||||
|
||||
## Local development
|
||||
|
||||
### Backend
|
||||
|
||||
- Open `commafeed-server` in your preferred Java IDE.
|
||||
- CommaFeed uses Lombok, you need the Lombok plugin for your IDE.
|
||||
- Start `CommaFeedApplication.java` in debug mode with `server config.dev.yml` as arguments
|
||||
|
||||
### Frontend
|
||||
|
||||
- Open `commafeed-client` in your preferred JavaScript IDE.
|
||||
- run `npm install`
|
||||
- run `npm run dev`
|
||||
|
||||
The frontend server is now running at http://localhost:8082 and is proxying REST requests to the backend running on
|
||||
port 8083
|
||||
|
||||
## Copyright and license
|
||||
|
||||
Copyright 2013-2023 CommaFeed.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this work except in compliance with the License.
|
||||
You may obtain a copy of the License in the LICENSE file, or at:
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
6
SECURITY.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you found a vulnerability that you deem too sensitive to disclose publicly in a Github issue, please send an email at jeremiepanzer at gmail dot com.
|
||||
Thanks !
|
||||
33
bower.json
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "commafeed",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"jquery": "1.11.0",
|
||||
"jquery-ui": "1.11",
|
||||
"jquery-mousewheel": "3.1.12",
|
||||
"lodash": "2.4.1",
|
||||
"bootstrap": "3.1.1",
|
||||
"font-awesome": "3.2.1",
|
||||
"angular": "1.2.16",
|
||||
"angular-resource": "1.2.16",
|
||||
"angular-route": "1.2.16",
|
||||
"angular-sanitize": "1.2.16",
|
||||
"angular-touch": "1.2.16",
|
||||
"angular-animate": "1.2.16",
|
||||
"angular-ui-router": "0.2.8",
|
||||
"angular-ui-utils": "0.1.0",
|
||||
"angular-ui-select2": "0.0.5",
|
||||
"angular-bootstrap": "0.2.0",
|
||||
"angular-loading-bar": "0.5.0",
|
||||
"angular-translate": "2.2.0",
|
||||
"angular-translate-loader-static-files": "2.2.0",
|
||||
"ngInfiniteScroll": "1.0.0",
|
||||
"ng-grid": "2.0.6",
|
||||
"mousetrap": "1.4.6",
|
||||
"momentjs": "2.6.0",
|
||||
"device.js": "matthewhudson/device.js#2ae5c775e35ccc837589e5af34e292c54936778c",
|
||||
"readabilicons": "arc90/readability-readabilicons#34c55561c5b8ec6e90714b50237c06b13cb9d59c",
|
||||
"zocial": "samcollins/css-social-buttons#1f59ecacde475e563fb6771667597493ec4eecb6",
|
||||
"swagger-ui": "2.0.21"
|
||||
}
|
||||
}
|
||||
6
commafeed-client/.eslintignore
Normal file
@@ -0,0 +1,6 @@
|
||||
dist
|
||||
node_modules
|
||||
|
||||
vite.config.ts
|
||||
|
||||
src/locales/**/*.ts
|
||||
85
commafeed-client/.eslintrc
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"react-app",
|
||||
"airbnb",
|
||||
"airbnb-typescript",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "prettier", "hooks"],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
// make eslint check prettier rules
|
||||
"prettier/prettier": "error",
|
||||
|
||||
// enforce consistent curly braces usage
|
||||
"curly": ["error", "multi-line", "consistent"],
|
||||
|
||||
// set "props" to false because it cases false positives with immer
|
||||
"no-param-reassign": ["error", { "props": false }],
|
||||
|
||||
"prefer-destructuring": [
|
||||
"error",
|
||||
{
|
||||
"array": false,
|
||||
"object": true
|
||||
},
|
||||
{
|
||||
"enforceForRenamedProperties": false
|
||||
}
|
||||
],
|
||||
|
||||
// causes issues in thunks when we want to dispatch an action that is defined in the reducer
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
|
||||
// make sure the key prop is filled when required
|
||||
"react/jsx-key": ["error", { "checkFragmentShorthand": true }],
|
||||
|
||||
// configure additional hooks
|
||||
"react-hooks/exhaustive-deps": [
|
||||
"warn",
|
||||
{
|
||||
"additionalHooks": "(^useAsync$|useDidUpdate)"
|
||||
}
|
||||
],
|
||||
|
||||
// trigger even if props is used only in createStyles()
|
||||
"react/no-unused-prop-types": "off",
|
||||
|
||||
// no longer required with modern react versions
|
||||
"react/react-in-jsx-scope": "off",
|
||||
|
||||
// not required with typescript
|
||||
"react/prop-types": "off",
|
||||
"react/require-default-props": "off",
|
||||
|
||||
// matter of taste
|
||||
"react/destructuring-assignment": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
|
||||
// enforce hook call order
|
||||
"hooks/sort": [
|
||||
2,
|
||||
{
|
||||
"groups": [
|
||||
"useLocation",
|
||||
"useParams",
|
||||
"useState",
|
||||
"useAppSelector",
|
||||
"useAppDispatch",
|
||||
"useAsync",
|
||||
"useForm",
|
||||
"useAsyncCallback",
|
||||
"useCallback",
|
||||
"useEffect"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
33
commafeed-client/.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# rollup-plugin-visualizer
|
||||
/stats.html
|
||||
|
||||
# vite
|
||||
vite.config.ts.timestamp-*.mjs
|
||||
|
||||
# compiled locales
|
||||
src/locales/**/*.ts
|
||||
52
commafeed-client/.linguirc
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"locales": [
|
||||
"ar",
|
||||
"ca",
|
||||
"cs",
|
||||
"cy",
|
||||
"da",
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"fa",
|
||||
"fi",
|
||||
"fr",
|
||||
"gl",
|
||||
"hu",
|
||||
"id",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"ms",
|
||||
"nb",
|
||||
"nl",
|
||||
"nn",
|
||||
"pl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sk",
|
||||
"sv",
|
||||
"tr",
|
||||
"zh"
|
||||
],
|
||||
"catalogs": [
|
||||
{
|
||||
"path": "src/locales/{locale}/messages",
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"src/locales/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"format": "po",
|
||||
"formatOptions": {
|
||||
"origins": true,
|
||||
"lineNumbers": false
|
||||
},
|
||||
"sourceLocale": "en",
|
||||
"fallbackLocales": {
|
||||
"default": "en"
|
||||
}
|
||||
}
|
||||
7
commafeed-client/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 140,
|
||||
"semi": false,
|
||||
"tabWidth": 4,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
16
commafeed-client/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="stylesheet" href="custom_css.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>CommaFeed</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script src="custom_js.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
11961
commafeed-client/package-lock.json
generated
Normal file
84
commafeed-client/package.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "commafeed-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"dev:typescript": "tsc --watch",
|
||||
"build": "npm run i18n:compile && tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ci": "vitest run",
|
||||
"eslint": "eslint --ext=.js,.jsx,.ts,.tsx src",
|
||||
"i18n": "npm run i18n:extract && npm run i18n:compile",
|
||||
"i18n:extract": "lingui extract --clean",
|
||||
"i18n:compile": "lingui compile --typescript",
|
||||
"postinstall": "npm run i18n:compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.0",
|
||||
"@fontsource/open-sans": "^4.5.14",
|
||||
"@lingui/core": "^4.0.0",
|
||||
"@lingui/macro": "^4.0.0",
|
||||
"@lingui/react": "^4.0.0",
|
||||
"@mantine/core": "^6.0.10",
|
||||
"@mantine/form": "^6.0.10",
|
||||
"@mantine/hooks": "^6.0.10",
|
||||
"@mantine/modals": "^6.0.10",
|
||||
"@mantine/notifications": "^6.0.10",
|
||||
"@mantine/spotlight": "^6.0.10",
|
||||
"@mantine/styles": "^6.0.10",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"axios": "^1.4.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"interweave": "^13.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"react": "^18.2.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-contexify": "^6.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-infinite-scroller": "^1.2.6",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.11.1",
|
||||
"react-swipeable": "^7.0.0",
|
||||
"swagger-ui-react": "^4.18.3",
|
||||
"tinycon": "^0.6.8",
|
||||
"use-local-storage": "^3.0.0",
|
||||
"websocket-heartbeat-js": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lingui/cli": "^4.0.0",
|
||||
"@lingui/vite-plugin": "^4.0.0",
|
||||
"@types/eslint": "^8.37.0",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/mousetrap": "^1.6.11",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/react-infinite-scroller": "^1.2.3",
|
||||
"@types/swagger-ui-react": "^4.18.0",
|
||||
"@types/tinycon": "^0.6.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-hooks": "^0.4.3",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"prettier": "^2.8.8",
|
||||
"rollup-plugin-visualizer": "^5.9.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.5",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.31.0",
|
||||
"vitest-mock-extended": "^1.1.3"
|
||||
}
|
||||
}
|
||||
98
commafeed-client/pom.xml
Normal file
@@ -0,0 +1,98 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.commafeed</groupId>
|
||||
<artifactId>commafeed</artifactId>
|
||||
<version>3.3.0</version>
|
||||
</parent>
|
||||
<artifactId>commafeed-client</artifactId>
|
||||
<name>CommaFeed Client</name>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<version>1.12.1</version>
|
||||
<?m2e ignore?>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>install node and npm</id>
|
||||
<goals>
|
||||
<goal>install-node-and-npm</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
<configuration>
|
||||
<nodeVersion>v16.16.0</nodeVersion>
|
||||
<npmVersion>8.15.0</npmVersion>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm install</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
<configuration>
|
||||
<arguments>ci</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm run eslint</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
<configuration>
|
||||
<arguments>run eslint</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm run test</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
<configuration>
|
||||
<arguments>run test:ci</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>npm run build</id>
|
||||
<goals>
|
||||
<goal>npm</goal>
|
||||
</goals>
|
||||
<phase>compile</phase>
|
||||
<configuration>
|
||||
<arguments>run build</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy web interface to resources</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/classes/assets</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>dist</directory>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
BIN
commafeed-client/public/app-icon-114.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
commafeed-client/public/app-icon-144.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
commafeed-client/public/app-icon-192.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
34
commafeed-client/public/manifest.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
|
||||
"name": "CommaFeed",
|
||||
"scope": ".",
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"theme_color": "#f88a14",
|
||||
"icons": [
|
||||
{
|
||||
"src": "app-icon-72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "app-icon-114.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "app-icon-144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "app-icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
162
commafeed-client/src/App.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { i18n } from "@lingui/core"
|
||||
import { I18nProvider } from "@lingui/react"
|
||||
import { ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core"
|
||||
import { useColorScheme } from "@mantine/hooks"
|
||||
import { ModalsProvider } from "@mantine/modals"
|
||||
import { Notifications } from "@mantine/notifications"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectTo } from "app/slices/redirect"
|
||||
import { reloadServerInfos } from "app/slices/server"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { categoryUnreadCount } from "app/utils"
|
||||
import { ErrorBoundary } from "components/ErrorBoundary"
|
||||
import { Header } from "components/header/Header"
|
||||
import { Tree } from "components/sidebar/Tree"
|
||||
import { useI18n } from "i18n"
|
||||
import { AdminUsersPage } from "pages/admin/AdminUsersPage"
|
||||
import { MetricsPage } from "pages/admin/MetricsPage"
|
||||
import { AboutPage } from "pages/app/AboutPage"
|
||||
import { AddPage } from "pages/app/AddPage"
|
||||
import { CategoryDetailsPage } from "pages/app/CategoryDetailsPage"
|
||||
import { FeedDetailsPage } from "pages/app/FeedDetailsPage"
|
||||
import { FeedEntriesPage } from "pages/app/FeedEntriesPage"
|
||||
import Layout from "pages/app/Layout"
|
||||
import { SettingsPage } from "pages/app/SettingsPage"
|
||||
import { TagDetailsPage } from "pages/app/TagDetailsPage"
|
||||
import { LoginPage } from "pages/auth/LoginPage"
|
||||
import { PasswordRecoveryPage } from "pages/auth/PasswordRecoveryPage"
|
||||
import { RegistrationPage } from "pages/auth/RegistrationPage"
|
||||
import { WelcomePage } from "pages/WelcomePage"
|
||||
import React, { useEffect } from "react"
|
||||
import ReactGA from "react-ga4"
|
||||
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom"
|
||||
import Tinycon from "tinycon"
|
||||
import useLocalStorage from "use-local-storage"
|
||||
|
||||
function Providers(props: { children: React.ReactNode }) {
|
||||
const preferredColorScheme = useColorScheme()
|
||||
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>("color-scheme", preferredColorScheme)
|
||||
const toggleColorScheme = (value?: ColorScheme) => setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"))
|
||||
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
|
||||
<MantineProvider
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
theme={{
|
||||
primaryColor: "orange",
|
||||
colorScheme,
|
||||
fontFamily: "Open Sans",
|
||||
}}
|
||||
>
|
||||
<ModalsProvider>
|
||||
<Notifications position="bottom-right" zIndex={9999} />
|
||||
<ErrorBoundary>{props.children}</ErrorBoundary>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</ColorSchemeProvider>
|
||||
</I18nProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// swagger-ui is very large, load only on-demand
|
||||
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage"))
|
||||
|
||||
function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
|
||||
<Route path="welcome" element={<WelcomePage />} />
|
||||
<Route path="login" element={<LoginPage />} />
|
||||
<Route path="register" element={<RegistrationPage />} />
|
||||
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
|
||||
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} />}>
|
||||
<Route path="category">
|
||||
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
|
||||
<Route path=":id/details" element={<CategoryDetailsPage />} />
|
||||
</Route>
|
||||
<Route path="feed">
|
||||
<Route path=":id" element={<FeedEntriesPage sourceType="feed" />} />
|
||||
<Route path=":id/details" element={<FeedDetailsPage />} />
|
||||
</Route>
|
||||
<Route path="tag">
|
||||
<Route path=":id" element={<FeedEntriesPage sourceType="tag" />} />
|
||||
<Route path=":id/details" element={<TagDetailsPage />} />
|
||||
</Route>
|
||||
<Route path="add" element={<AddPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="admin">
|
||||
<Route path="users" element={<AdminUsersPage />} />
|
||||
<Route path="metrics" element={<MetricsPage />} />
|
||||
</Route>
|
||||
<Route path="about" element={<AboutPage />} />
|
||||
<Route path="api" element={<ApiDocumentationPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
function RedirectHandler() {
|
||||
const target = useAppSelector(state => state.redirect.to)
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
useEffect(() => {
|
||||
if (target) {
|
||||
// pages can subscribe to state.timestamp in order to refresh when navigating to an url matching the current page
|
||||
navigate(target, { state: { timestamp: new Date() } })
|
||||
dispatch(redirectTo(undefined))
|
||||
}
|
||||
}, [target, dispatch, navigate])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function GoogleAnalyticsHandler() {
|
||||
const location = useLocation()
|
||||
const googleAnalyticsCode = useAppSelector(state => state.server.serverInfos?.googleAnalyticsCode)
|
||||
|
||||
useEffect(() => {
|
||||
if (googleAnalyticsCode) ReactGA.initialize(googleAnalyticsCode)
|
||||
}, [googleAnalyticsCode])
|
||||
|
||||
useEffect(() => {
|
||||
ReactGA.send({ hitType: "pageview", page: location.pathname })
|
||||
}, [location])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function FaviconHandler() {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
useEffect(() => {
|
||||
const unreadCount = categoryUnreadCount(root)
|
||||
if (unreadCount === 0) Tinycon.reset()
|
||||
else Tinycon.setBubble(unreadCount)
|
||||
}, [root])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function App() {
|
||||
useI18n()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(reloadServerInfos())
|
||||
}, [dispatch])
|
||||
|
||||
return (
|
||||
<Providers>
|
||||
<>
|
||||
<FaviconHandler />
|
||||
<HashRouter>
|
||||
<GoogleAnalyticsHandler />
|
||||
<RedirectHandler />
|
||||
<AppRoutes />
|
||||
</HashRouter>
|
||||
</>
|
||||
</Providers>
|
||||
)
|
||||
}
|
||||
115
commafeed-client/src/app/client.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import axios from "axios"
|
||||
import {
|
||||
AddCategoryRequest,
|
||||
Category,
|
||||
CategoryModificationRequest,
|
||||
CollapseRequest,
|
||||
Entries,
|
||||
FeedInfo,
|
||||
FeedInfoRequest,
|
||||
FeedModificationRequest,
|
||||
GetEntriesPaginatedRequest,
|
||||
IDRequest,
|
||||
LoginRequest,
|
||||
MarkRequest,
|
||||
Metrics,
|
||||
MultipleMarkRequest,
|
||||
PasswordResetRequest,
|
||||
ProfileModificationRequest,
|
||||
RegistrationRequest,
|
||||
ServerInfo,
|
||||
Settings,
|
||||
StarRequest,
|
||||
SubscribeRequest,
|
||||
Subscription,
|
||||
TagRequest,
|
||||
UserModel,
|
||||
} from "./types"
|
||||
|
||||
const axiosInstance = axios.create({ baseURL: "./rest", withCredentials: true })
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error.response.status === 401 && error.response.data === "Credentials are required to access this resource.") {
|
||||
window.location.hash = "/welcome"
|
||||
}
|
||||
throw error
|
||||
}
|
||||
)
|
||||
|
||||
export const client = {
|
||||
category: {
|
||||
getRoot: () => axiosInstance.get<Category>("category/get"),
|
||||
modify: (req: CategoryModificationRequest) => axiosInstance.post("category/modify", req),
|
||||
collapse: (req: CollapseRequest) => axiosInstance.post("category/collapse", req),
|
||||
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("category/entries", { params: req }),
|
||||
markEntries: (req: MarkRequest) => axiosInstance.post("category/mark", req),
|
||||
add: (req: AddCategoryRequest) => axiosInstance.post("category/add", req),
|
||||
delete: (req: IDRequest) => axiosInstance.post("category/delete", req),
|
||||
},
|
||||
entry: {
|
||||
mark: (req: MarkRequest) => axiosInstance.post("entry/mark", req),
|
||||
markMultiple: (req: MultipleMarkRequest) => axiosInstance.post("entry/markMultiple", req),
|
||||
star: (req: StarRequest) => axiosInstance.post("entry/star", req),
|
||||
getTags: () => axiosInstance.get<string[]>("entry/tags"),
|
||||
tag: (req: TagRequest) => axiosInstance.post("entry/tag", req),
|
||||
},
|
||||
feed: {
|
||||
get: (id: string) => axiosInstance.get<Subscription>(`feed/get/${id}`),
|
||||
modify: (req: FeedModificationRequest) => axiosInstance.post("feed/modify", req),
|
||||
getEntries: (req: GetEntriesPaginatedRequest) => axiosInstance.get<Entries>("feed/entries", { params: req }),
|
||||
markEntries: (req: MarkRequest) => axiosInstance.post("feed/mark", req),
|
||||
fetchFeed: (req: FeedInfoRequest) => axiosInstance.post<FeedInfo>("feed/fetch", req),
|
||||
refreshAll: () => axiosInstance.get("feed/refreshAll"),
|
||||
subscribe: (req: SubscribeRequest) => axiosInstance.post<number>("feed/subscribe", req),
|
||||
unsubscribe: (req: IDRequest) => axiosInstance.post("feed/unsubscribe", req),
|
||||
importOpml: (req: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append("file", req)
|
||||
return axiosInstance.post("feed/import", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
user: {
|
||||
login: (req: LoginRequest) => axiosInstance.post("user/login", req),
|
||||
register: (req: RegistrationRequest) => axiosInstance.post("user/register", req),
|
||||
passwordReset: (req: PasswordResetRequest) => axiosInstance.post("user/passwordReset", req),
|
||||
getSettings: () => axiosInstance.get<Settings>("user/settings"),
|
||||
saveSettings: (settings: Settings) => axiosInstance.post("user/settings", settings),
|
||||
getProfile: () => axiosInstance.get<UserModel>("user/profile"),
|
||||
saveProfile: (req: ProfileModificationRequest) => axiosInstance.post("user/profile", req),
|
||||
deleteProfile: () => axiosInstance.post("user/profile/deleteAccount"),
|
||||
},
|
||||
server: {
|
||||
getServerInfos: () => axiosInstance.get<ServerInfo>("server/get"),
|
||||
},
|
||||
admin: {
|
||||
getAllUsers: () => axiosInstance.get<UserModel[]>("admin/user/getAll"),
|
||||
saveUser: (req: UserModel) => axiosInstance.post("admin/user/save", req),
|
||||
deleteUser: (req: IDRequest) => axiosInstance.post("admin/user/delete", req),
|
||||
getMetrics: () => axiosInstance.get<Metrics>("admin/metrics"),
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* transform an error object to an array of strings that can be displayed to the user
|
||||
* @param err an error object (e.g. from axios)
|
||||
* @returns an array of messages to show the user
|
||||
*/
|
||||
export const errorToStrings = (err: unknown) => {
|
||||
let strings: string[] = []
|
||||
|
||||
if (axios.isAxiosError(err)) {
|
||||
if (err.response) {
|
||||
const { data } = err.response
|
||||
if (typeof data === "string") strings.push(data)
|
||||
if (typeof data === "object" && data.message) strings.push(data.message)
|
||||
if (typeof data === "object" && data.errors) strings = [...strings, ...data.errors]
|
||||
}
|
||||
}
|
||||
|
||||
return strings
|
||||
}
|
||||
101
commafeed-client/src/app/constants.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { DEFAULT_THEME } from "@mantine/core"
|
||||
import { IconType } from "react-icons"
|
||||
import { FaAt } from "react-icons/fa"
|
||||
import { SiBuffer, SiFacebook, SiGmail, SiInstapaper, SiPocket, SiTumblr, SiTwitter } from "react-icons/si"
|
||||
import { Category, Entry, SharingSettings } from "./types"
|
||||
|
||||
const categories: { [key: string]: Category } = {
|
||||
all: {
|
||||
id: "all",
|
||||
name: t`All`,
|
||||
expanded: false,
|
||||
children: [],
|
||||
feeds: [],
|
||||
position: 0,
|
||||
},
|
||||
starred: {
|
||||
id: "starred",
|
||||
name: t`Starred`,
|
||||
expanded: false,
|
||||
children: [],
|
||||
feeds: [],
|
||||
position: 1,
|
||||
},
|
||||
}
|
||||
|
||||
const sharing: {
|
||||
[key in keyof SharingSettings]: {
|
||||
label: string
|
||||
icon: IconType
|
||||
color: `#${string}`
|
||||
url: (url: string, description: string) => string
|
||||
}
|
||||
} = {
|
||||
email: {
|
||||
label: "Email",
|
||||
icon: FaAt,
|
||||
color: "#000000",
|
||||
url: (url, desc) => `mailto:?subject=${desc}&body=${url}`,
|
||||
},
|
||||
gmail: {
|
||||
label: "Gmail",
|
||||
icon: SiGmail,
|
||||
color: "#EA4335",
|
||||
url: (url, desc) => `https://mail.google.com/mail/?view=cm&fs=1&tf=1&source=mailto&su=${desc}&body=${url}`,
|
||||
},
|
||||
facebook: {
|
||||
label: "Facebook",
|
||||
icon: SiFacebook,
|
||||
color: "#1B74E4",
|
||||
url: url => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
|
||||
},
|
||||
twitter: {
|
||||
label: "Twitter",
|
||||
icon: SiTwitter,
|
||||
color: "#1D9BF0",
|
||||
url: (url, desc) => `https://twitter.com/share?text=${desc}&url=${url}`,
|
||||
},
|
||||
tumblr: {
|
||||
label: "Tumblr",
|
||||
icon: SiTumblr,
|
||||
color: "#375672",
|
||||
url: (url, desc) => `https://www.tumblr.com/share/link?url=${url}&name=${desc}`,
|
||||
},
|
||||
pocket: {
|
||||
label: "Pocket",
|
||||
icon: SiPocket,
|
||||
color: "#EF4154",
|
||||
url: (url, desc) => `https://getpocket.com/save?url=${url}&title=${desc}`,
|
||||
},
|
||||
instapaper: {
|
||||
label: "Instapaper",
|
||||
icon: SiInstapaper,
|
||||
color: "#010101",
|
||||
url: (url, desc) => `https://www.instapaper.com/hello2?url=${url}&title=${desc}`,
|
||||
},
|
||||
buffer: {
|
||||
label: "Buffer",
|
||||
icon: SiBuffer,
|
||||
color: "#000000",
|
||||
url: (url, desc) => `https://bufferapp.com/add?url=${url}&text=${desc}`,
|
||||
},
|
||||
}
|
||||
|
||||
export const Constants = {
|
||||
categories,
|
||||
sharing,
|
||||
layout: {
|
||||
mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
|
||||
headerHeight: 60,
|
||||
sidebarWidth: 350,
|
||||
entryMaxWidth: 650,
|
||||
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
|
||||
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
|
||||
},
|
||||
dom: {
|
||||
mainScrollAreaId: "main-scroll-area-id",
|
||||
entryId: (entry: Entry) => `entry-id-${entry.id}`,
|
||||
},
|
||||
bitcoinWalletAddress: "1dymfUxqCWpyD7a6rQSqNy4rLVDBsAr5e",
|
||||
}
|
||||
143
commafeed-client/src/app/slices/entries.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/* eslint-disable import/first */
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { reducers } from "app/store"
|
||||
import { Entries, Entry } from "app/types"
|
||||
import { AxiosResponse } from "axios"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { mockReset } from "vitest-mock-extended"
|
||||
import { loadEntries, loadMoreEntries, markAllEntries, markEntry } from "./entries"
|
||||
|
||||
const mockClient = await vi.hoisted(async () => {
|
||||
const mockModule = await import("vitest-mock-extended")
|
||||
return mockModule.mockDeep<typeof client>()
|
||||
})
|
||||
vi.mock("app/client", () => ({ client: mockClient }))
|
||||
|
||||
describe("entries", () => {
|
||||
beforeEach(() => {
|
||||
mockReset(mockClient)
|
||||
})
|
||||
|
||||
it("loads entries", async () => {
|
||||
mockClient.feed.getEntries.mockResolvedValue({
|
||||
data: {
|
||||
entries: [{ id: "3" } as Entry],
|
||||
hasMore: false,
|
||||
name: "my-feed",
|
||||
errorCount: 3,
|
||||
feedLink: "https://mysite.com/feed",
|
||||
timestamp: 123,
|
||||
ignoredReadStatus: false,
|
||||
},
|
||||
} as AxiosResponse<Entries>)
|
||||
|
||||
const store = configureStore({ reducer: reducers })
|
||||
const promise = store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: true }))
|
||||
|
||||
expect(store.getState().entries.source.type).toBe("feed")
|
||||
expect(store.getState().entries.source.id).toBe("feed-id")
|
||||
expect(store.getState().entries.entries).toStrictEqual([])
|
||||
expect(store.getState().entries.hasMore).toBe(true)
|
||||
expect(store.getState().entries.sourceLabel).toBe("")
|
||||
expect(store.getState().entries.sourceWebsiteUrl).toBe("")
|
||||
expect(store.getState().entries.timestamp).toBeUndefined()
|
||||
|
||||
await promise
|
||||
expect(store.getState().entries.source.type).toBe("feed")
|
||||
expect(store.getState().entries.source.id).toBe("feed-id")
|
||||
expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }])
|
||||
expect(store.getState().entries.hasMore).toBe(false)
|
||||
expect(store.getState().entries.sourceLabel).toBe("my-feed")
|
||||
expect(store.getState().entries.sourceWebsiteUrl).toBe("https://mysite.com/feed")
|
||||
expect(store.getState().entries.timestamp).toBe(123)
|
||||
})
|
||||
|
||||
it("loads more entries", async () => {
|
||||
mockClient.category.getEntries.mockResolvedValue({
|
||||
data: {
|
||||
entries: [{ id: "4" } as Entry],
|
||||
hasMore: false,
|
||||
name: "my-feed",
|
||||
errorCount: 3,
|
||||
feedLink: "https://mysite.com/feed",
|
||||
timestamp: 123,
|
||||
ignoredReadStatus: false,
|
||||
},
|
||||
} as AxiosResponse<Entries>)
|
||||
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
entries: {
|
||||
source: {
|
||||
type: "category",
|
||||
id: "category-id",
|
||||
},
|
||||
sourceLabel: "",
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3" } as Entry],
|
||||
hasMore: true,
|
||||
scrollingToEntry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const promise = store.dispatch(loadMoreEntries())
|
||||
|
||||
await promise
|
||||
expect(store.getState().entries.entries).toStrictEqual([{ id: "3" }, { id: "4" }])
|
||||
expect(store.getState().entries.hasMore).toBe(false)
|
||||
})
|
||||
|
||||
it("marks an entry as read", async () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
entries: {
|
||||
source: {
|
||||
type: "category",
|
||||
id: "category-id",
|
||||
},
|
||||
sourceLabel: "",
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
|
||||
hasMore: true,
|
||||
scrollingToEntry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
store.dispatch(markEntry({ entry: { id: "3" } as Entry, read: true }))
|
||||
expect(store.getState().entries.entries).toStrictEqual([
|
||||
{ id: "3", read: true },
|
||||
{ id: "4", read: false },
|
||||
])
|
||||
expect(mockClient.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
|
||||
})
|
||||
|
||||
it("marks all entries as read", async () => {
|
||||
const store = configureStore({
|
||||
reducer: reducers,
|
||||
preloadedState: {
|
||||
entries: {
|
||||
source: {
|
||||
type: "category",
|
||||
id: "category-id",
|
||||
},
|
||||
sourceLabel: "",
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [{ id: "3", read: false } as Entry, { id: "4", read: false } as Entry],
|
||||
hasMore: true,
|
||||
scrollingToEntry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
store.dispatch(markAllEntries({ sourceType: "category", req: { id: "all", read: true } }))
|
||||
expect(store.getState().entries.entries).toStrictEqual([
|
||||
{ id: "3", read: true },
|
||||
{ id: "4", read: true },
|
||||
])
|
||||
expect(mockClient.category.markEntries).toHaveBeenCalledWith({ id: "all", read: true })
|
||||
})
|
||||
})
|
||||
339
commafeed-client/src/app/slices/entries.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { RootState } from "app/store"
|
||||
import { Entries, Entry, MarkRequest, TagRequest } from "app/types"
|
||||
import { scrollToWithCallback } from "app/utils"
|
||||
import { flushSync } from "react-dom"
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { reloadTree } from "./tree"
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { reloadTags } from "./user"
|
||||
|
||||
export type EntrySourceType = "category" | "feed" | "tag"
|
||||
export type EntrySource = { type: EntrySourceType; id: string }
|
||||
export type ExpendableEntry = Entry & { expanded?: boolean }
|
||||
|
||||
interface EntriesState {
|
||||
/** selected source */
|
||||
source: EntrySource
|
||||
sourceLabel: string
|
||||
sourceWebsiteUrl: string
|
||||
entries: ExpendableEntry[]
|
||||
/** stores when the first batch of entries were retrieved
|
||||
*
|
||||
* this is used when marking all entries of a feed/category to only mark entries up to that timestamp as newer entries were potentially never shown
|
||||
*/
|
||||
timestamp?: number
|
||||
selectedEntryId?: string
|
||||
hasMore: boolean
|
||||
search?: string
|
||||
scrollingToEntry: boolean
|
||||
}
|
||||
|
||||
const initialState: EntriesState = {
|
||||
source: {
|
||||
type: "category",
|
||||
id: Constants.categories.all.id,
|
||||
},
|
||||
sourceLabel: "",
|
||||
sourceWebsiteUrl: "",
|
||||
entries: [],
|
||||
hasMore: true,
|
||||
scrollingToEntry: false,
|
||||
}
|
||||
|
||||
const getEndpoint = (sourceType: EntrySourceType) =>
|
||||
sourceType === "category" || sourceType === "tag" ? client.category.getEntries : client.feed.getEntries
|
||||
export const loadEntries = createAsyncThunk<Entries, { source: EntrySource; clearSearch: boolean }, { state: RootState }>(
|
||||
"entries/load",
|
||||
async (arg, thunkApi) => {
|
||||
if (arg.clearSearch) thunkApi.dispatch(setSearch(""))
|
||||
|
||||
const state = thunkApi.getState()
|
||||
const endpoint = getEndpoint(arg.source.type)
|
||||
const result = await endpoint(buildGetEntriesPaginatedRequest(state, arg.source, 0))
|
||||
return result.data
|
||||
}
|
||||
)
|
||||
export const loadMoreEntries = createAsyncThunk<Entries, void, { state: RootState }>("entries/loadMore", async (_, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { source } = state.entries
|
||||
const offset =
|
||||
state.user.settings?.readingMode === "all" ? state.entries.entries.length : state.entries.entries.filter(e => !e.read).length
|
||||
const endpoint = getEndpoint(state.entries.source.type)
|
||||
const result = await endpoint(buildGetEntriesPaginatedRequest(state, source, offset))
|
||||
return result.data
|
||||
})
|
||||
const buildGetEntriesPaginatedRequest = (state: RootState, source: EntrySource, offset: number) => ({
|
||||
id: source.type === "tag" ? Constants.categories.all.id : source.id,
|
||||
order: state.user.settings?.readingOrder,
|
||||
readType: state.user.settings?.readingMode,
|
||||
offset,
|
||||
limit: 50,
|
||||
tag: source.type === "tag" ? source.id : undefined,
|
||||
keywords: state.entries.search,
|
||||
})
|
||||
export const reloadEntries = createAsyncThunk<void, void, { state: RootState }>("entries/reload", async (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||
})
|
||||
export const search = createAsyncThunk<void, string, { state: RootState }>("entries/search", async (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
thunkApi.dispatch(setSearch(arg))
|
||||
thunkApi.dispatch(loadEntries({ source: state.entries.source, clearSearch: false }))
|
||||
})
|
||||
export const markEntry = createAsyncThunk(
|
||||
"entries/entry/mark",
|
||||
(arg: { entry: Entry; read: boolean }) => {
|
||||
client.entry.mark({
|
||||
id: arg.entry.id,
|
||||
read: arg.read,
|
||||
})
|
||||
},
|
||||
{
|
||||
condition: arg => arg.entry.read !== arg.read,
|
||||
}
|
||||
)
|
||||
export const markMultipleEntries = createAsyncThunk(
|
||||
"entries/entry/markMultiple",
|
||||
async (arg: { entries: Entry[]; read: boolean }, thunkApi) => {
|
||||
const requests: MarkRequest[] = arg.entries.map(e => ({
|
||||
id: e.id,
|
||||
read: arg.read,
|
||||
}))
|
||||
await client.entry.markMultiple({ requests })
|
||||
thunkApi.dispatch(reloadTree())
|
||||
}
|
||||
)
|
||||
export const markEntriesUpToEntry = createAsyncThunk<void, Entry, { state: RootState }>(
|
||||
"entries/entry/upToEntry",
|
||||
async (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
|
||||
const index = entries.findIndex(e => e.id === arg.id)
|
||||
if (index === -1) return
|
||||
|
||||
thunkApi.dispatch(
|
||||
markMultipleEntries({
|
||||
entries: entries.slice(0, index + 1),
|
||||
read: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
export const markAllEntries = createAsyncThunk<void, { sourceType: EntrySourceType; req: MarkRequest }, { state: RootState }>(
|
||||
"entries/entry/markAll",
|
||||
async (arg, thunkApi) => {
|
||||
const endpoint = arg.sourceType === "category" ? client.category.markEntries : client.feed.markEntries
|
||||
await endpoint(arg.req)
|
||||
thunkApi.dispatch(reloadEntries())
|
||||
thunkApi.dispatch(reloadTree())
|
||||
}
|
||||
)
|
||||
export const starEntry = createAsyncThunk("entries/entry/star", (arg: { entry: Entry; starred: boolean }) => {
|
||||
client.entry.star({
|
||||
id: arg.entry.id,
|
||||
feedId: +arg.entry.feedId,
|
||||
starred: arg.starred,
|
||||
})
|
||||
})
|
||||
export const selectEntry = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
entry: Entry
|
||||
expand: boolean
|
||||
markAsRead: boolean
|
||||
scrollToEntry: boolean
|
||||
},
|
||||
{ state: RootState }
|
||||
>("entries/entry/select", (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const entry = state.entries.entries.find(e => e.id === arg.entry.id)
|
||||
if (!entry) return
|
||||
|
||||
// flushSync is required because we need the newly selected entry to be expanded
|
||||
// and the previously selected entry to be collapsed to be able to scroll to the right position
|
||||
flushSync(() => {
|
||||
// mark as read if requested
|
||||
if (arg.markAsRead) {
|
||||
thunkApi.dispatch(markEntry({ entry, read: true }))
|
||||
}
|
||||
|
||||
// set entry as selected
|
||||
thunkApi.dispatch(entriesSlice.actions.setSelectedEntry(entry))
|
||||
|
||||
// expand if requested
|
||||
const previouslySelectedEntry = state.entries.entries.find(e => e.id === state.entries.selectedEntryId)
|
||||
if (previouslySelectedEntry) {
|
||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry: previouslySelectedEntry, expanded: false }))
|
||||
}
|
||||
thunkApi.dispatch(entriesSlice.actions.setEntryExpanded({ entry, expanded: arg.expand }))
|
||||
})
|
||||
|
||||
if (arg.scrollToEntry) {
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(entry))
|
||||
if (entryElement) {
|
||||
const scrollSpeed = state.user.settings?.scrollSpeed
|
||||
thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(true))
|
||||
scrollToEntry(entryElement, scrollSpeed, () => thunkApi.dispatch(entriesSlice.actions.setScrollingToEntry(false)))
|
||||
}
|
||||
}
|
||||
})
|
||||
const scrollToEntry = (entryElement: HTMLElement, scrollSpeed: number | undefined, onScrollEnded: () => void) => {
|
||||
// the entry is entirely visible, no need to scroll
|
||||
if (Constants.layout.isTopVisible(entryElement) && Constants.layout.isBottomVisible(entryElement)) {
|
||||
onScrollEnded()
|
||||
return
|
||||
}
|
||||
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
if (scrollArea) {
|
||||
scrollToWithCallback({
|
||||
element: scrollArea,
|
||||
options: {
|
||||
// add a small gap between the top of the content and the top of the page
|
||||
top: entryElement.offsetTop - 3,
|
||||
behavior: scrollSpeed && scrollSpeed > 0 ? "smooth" : "auto",
|
||||
},
|
||||
onScrollEnded,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const selectPreviousEntry = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
expand: boolean
|
||||
markAsRead: boolean
|
||||
scrollToEntry: boolean
|
||||
},
|
||||
{ state: RootState }
|
||||
>("entries/entry/selectPrevious", (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const previousIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) - 1
|
||||
if (previousIndex >= 0) {
|
||||
thunkApi.dispatch(
|
||||
selectEntry({
|
||||
entry: entries[previousIndex],
|
||||
expand: arg.expand,
|
||||
markAsRead: arg.markAsRead,
|
||||
scrollToEntry: arg.scrollToEntry,
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
export const selectNextEntry = createAsyncThunk<
|
||||
void,
|
||||
{
|
||||
expand: boolean
|
||||
markAsRead: boolean
|
||||
scrollToEntry: boolean
|
||||
},
|
||||
{ state: RootState }
|
||||
>("entries/entry/selectNext", (arg, thunkApi) => {
|
||||
const state = thunkApi.getState()
|
||||
const { entries } = state.entries
|
||||
const nextIndex = entries.findIndex(e => e.id === state.entries.selectedEntryId) + 1
|
||||
if (nextIndex < entries.length) {
|
||||
thunkApi.dispatch(
|
||||
selectEntry({
|
||||
entry: entries[nextIndex],
|
||||
expand: arg.expand,
|
||||
markAsRead: arg.markAsRead,
|
||||
scrollToEntry: arg.scrollToEntry,
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
export const tagEntry = createAsyncThunk<void, TagRequest, { state: RootState }>("entries/entry/tag", async (arg, thunkApi) => {
|
||||
await client.entry.tag(arg)
|
||||
thunkApi.dispatch(reloadTags())
|
||||
})
|
||||
|
||||
export const entriesSlice = createSlice({
|
||||
name: "entries",
|
||||
initialState,
|
||||
reducers: {
|
||||
setSelectedEntry: (state, action: PayloadAction<Entry>) => {
|
||||
state.selectedEntryId = action.payload.id
|
||||
},
|
||||
setEntryExpanded: (state, action: PayloadAction<{ entry: Entry; expanded: boolean }>) => {
|
||||
state.entries
|
||||
.filter(e => e.id === action.payload.entry.id)
|
||||
.forEach(e => {
|
||||
e.expanded = action.payload.expanded
|
||||
})
|
||||
},
|
||||
setScrollingToEntry: (state, action: PayloadAction<boolean>) => {
|
||||
state.scrollingToEntry = action.payload
|
||||
},
|
||||
setSearch: (state, action: PayloadAction<string>) => {
|
||||
state.search = action.payload
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(markEntry.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => e.id === action.meta.arg.entry.id)
|
||||
.forEach(e => {
|
||||
e.read = action.meta.arg.read
|
||||
})
|
||||
})
|
||||
builder.addCase(markMultipleEntries.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))
|
||||
.forEach(e => {
|
||||
e.read = action.meta.arg.read
|
||||
})
|
||||
})
|
||||
builder.addCase(markAllEntries.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => (action.meta.arg.req.olderThan ? e.date < action.meta.arg.req.olderThan : true))
|
||||
.forEach(e => {
|
||||
e.read = true
|
||||
})
|
||||
})
|
||||
builder.addCase(starEntry.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => action.meta.arg.entry.id === e.id && action.meta.arg.entry.feedId === e.feedId)
|
||||
.forEach(e => {
|
||||
e.starred = action.meta.arg.starred
|
||||
})
|
||||
})
|
||||
builder.addCase(loadEntries.pending, (state, action) => {
|
||||
state.source = action.meta.arg.source
|
||||
state.entries = []
|
||||
state.timestamp = undefined
|
||||
state.sourceLabel = ""
|
||||
state.sourceWebsiteUrl = ""
|
||||
state.hasMore = true
|
||||
state.selectedEntryId = undefined
|
||||
})
|
||||
builder.addCase(loadEntries.fulfilled, (state, action) => {
|
||||
state.entries = action.payload.entries
|
||||
state.timestamp = action.payload.timestamp
|
||||
state.sourceLabel = action.payload.name
|
||||
state.sourceWebsiteUrl = action.payload.feedLink
|
||||
state.hasMore = action.payload.hasMore
|
||||
})
|
||||
builder.addCase(loadMoreEntries.fulfilled, (state, action) => {
|
||||
// remove already existing entries
|
||||
const entriesToAdd = action.payload.entries.filter(e => !state.entries.some(e2 => e.id === e2.id))
|
||||
state.entries = [...state.entries, ...entriesToAdd]
|
||||
state.hasMore = action.payload.hasMore
|
||||
})
|
||||
builder.addCase(tagEntry.pending, (state, action) => {
|
||||
state.entries
|
||||
.filter(e => +e.id === action.meta.arg.entryId)
|
||||
.forEach(e => {
|
||||
e.tags = action.meta.arg.tags
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { setSearch } = entriesSlice.actions
|
||||
export default entriesSlice.reducer
|
||||
10
commafeed-client/src/app/slices/redirect.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { store } from "app/store"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { redirectToCategory } from "./redirect"
|
||||
|
||||
describe("redirects", () => {
|
||||
it("redirects to category", async () => {
|
||||
await store.dispatch(redirectToCategory("1"))
|
||||
expect(store.getState().redirect.to).toBe("/app/category/1")
|
||||
})
|
||||
})
|
||||
61
commafeed-client/src/app/slices/redirect.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { Constants } from "app/constants"
|
||||
import { RootState } from "app/store"
|
||||
|
||||
interface RedirectState {
|
||||
to?: string
|
||||
}
|
||||
|
||||
const initialState: RedirectState = {}
|
||||
|
||||
export const redirectToLogin = createAsyncThunk("redirect/login", (_, thunkApi) => thunkApi.dispatch(redirectTo("/login")))
|
||||
export const redirectToRegistration = createAsyncThunk("redirect/register", (_, thunkApi) => thunkApi.dispatch(redirectTo("/register")))
|
||||
export const redirectToPasswordRecovery = createAsyncThunk("redirect/passwordRecovery", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/passwordRecovery"))
|
||||
)
|
||||
export const redirectToSelectedSource = createAsyncThunk<void, void, { state: RootState }>("redirect/selectedSource", (_, thunkApi) => {
|
||||
const { source } = thunkApi.getState().entries
|
||||
thunkApi.dispatch(redirectTo(`/app/${source.type}/${source.id}`))
|
||||
})
|
||||
export const redirectToCategory = createAsyncThunk("redirect/category", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/category/${id}`))
|
||||
)
|
||||
export const redirectToRootCategory = createAsyncThunk("redirect/category/root", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectToCategory(Constants.categories.all.id))
|
||||
)
|
||||
export const redirectToCategoryDetails = createAsyncThunk("redirect/category/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/category/${id}/details`))
|
||||
)
|
||||
export const redirectToFeed = createAsyncThunk("redirect/feed", (id: string | number, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}`))
|
||||
)
|
||||
export const redirectToFeedDetails = createAsyncThunk("redirect/feed/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/feed/${id}/details`))
|
||||
)
|
||||
export const redirectToTag = createAsyncThunk("redirect/tag", (id: string, thunkApi) => thunkApi.dispatch(redirectTo(`/app/tag/${id}`)))
|
||||
export const redirectToTagDetails = createAsyncThunk("redirect/tag/details", (id: string, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo(`/app/tag/${id}/details`))
|
||||
)
|
||||
export const redirectToAdd = createAsyncThunk("redirect/add", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/add")))
|
||||
export const redirectToSettings = createAsyncThunk("redirect/settings", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/settings")))
|
||||
export const redirectToAdminUsers = createAsyncThunk("redirect/admin/users", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/app/admin/users"))
|
||||
)
|
||||
export const redirectToMetrics = createAsyncThunk("redirect/admin/metrics", (_, thunkApi) =>
|
||||
thunkApi.dispatch(redirectTo("/app/admin/metrics"))
|
||||
)
|
||||
export const redirectToAbout = createAsyncThunk("redirect/about", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/about")))
|
||||
export const redirectToApiDocumentation = createAsyncThunk("redirect/api", (_, thunkApi) => thunkApi.dispatch(redirectTo("/app/api")))
|
||||
|
||||
export const redirectSlice = createSlice({
|
||||
name: "redirect",
|
||||
initialState,
|
||||
reducers: {
|
||||
redirectTo: (state, action: PayloadAction<string | undefined>) => {
|
||||
state.to = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { redirectTo } = redirectSlice.actions
|
||||
export default redirectSlice.reducer
|
||||
23
commafeed-client/src/app/slices/server.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { ServerInfo } from "app/types"
|
||||
|
||||
interface ServerState {
|
||||
serverInfos?: ServerInfo
|
||||
}
|
||||
|
||||
const initialState: ServerState = {}
|
||||
|
||||
export const reloadServerInfos = createAsyncThunk("server/infos", () => client.server.getServerInfos().then(r => r.data))
|
||||
export const serverSlice = createSlice({
|
||||
name: "server",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadServerInfos.fulfilled, (state, action) => {
|
||||
state.serverInfos = action.payload
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export default serverSlice.reducer
|
||||
58
commafeed-client/src/app/slices/tree.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { Category, CollapseRequest } from "app/types"
|
||||
import { visitCategoryTree } from "app/utils"
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { markEntry } from "./entries"
|
||||
import { redirectTo } from "./redirect"
|
||||
|
||||
interface TreeState {
|
||||
rootCategory?: Category
|
||||
mobileMenuOpen: boolean
|
||||
}
|
||||
|
||||
const initialState: TreeState = {
|
||||
mobileMenuOpen: false,
|
||||
}
|
||||
|
||||
export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
|
||||
export const collapseTreeCategory = createAsyncThunk("tree/category/collapse", async (req: CollapseRequest) =>
|
||||
client.category.collapse(req)
|
||||
)
|
||||
|
||||
export const treeSlice = createSlice({
|
||||
name: "tree",
|
||||
initialState,
|
||||
reducers: {
|
||||
setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.mobileMenuOpen = action.payload
|
||||
},
|
||||
},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadTree.fulfilled, (state, action) => {
|
||||
state.rootCategory = action.payload
|
||||
})
|
||||
builder.addCase(collapseTreeCategory.pending, (state, action) => {
|
||||
if (!state.rootCategory) return
|
||||
visitCategoryTree(state.rootCategory, c => {
|
||||
if (+c.id === action.meta.arg.id) c.expanded = !action.meta.arg.collapse
|
||||
})
|
||||
})
|
||||
builder.addCase(markEntry.pending, (state, action) => {
|
||||
if (!state.rootCategory) return
|
||||
visitCategoryTree(state.rootCategory, c =>
|
||||
c.feeds
|
||||
.filter(f => f.id === +action.meta.arg.entry.feedId)
|
||||
.forEach(f => {
|
||||
f.unread = action.meta.arg.read ? f.unread - 1 : f.unread + 1
|
||||
})
|
||||
)
|
||||
})
|
||||
builder.addCase(redirectTo, state => {
|
||||
state.mobileMenuOpen = false
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { setMobileMenuOpen } = treeSlice.actions
|
||||
export default treeSlice.reducer
|
||||
161
commafeed-client/src/app/slices/user.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { showNotification } from "@mantine/notifications"
|
||||
import { createAsyncThunk, createSlice, isAnyOf } from "@reduxjs/toolkit"
|
||||
import { client } from "app/client"
|
||||
import { RootState } from "app/store"
|
||||
import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel } from "app/types"
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { reloadEntries } from "./entries"
|
||||
|
||||
interface UserState {
|
||||
settings?: Settings
|
||||
profile?: UserModel
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
const initialState: UserState = {}
|
||||
|
||||
export const reloadSettings = createAsyncThunk("settings/reload", () => client.user.getSettings().then(r => r.data))
|
||||
export const reloadProfile = createAsyncThunk("profile/reload", () => client.user.getProfile().then(r => r.data))
|
||||
export const reloadTags = createAsyncThunk("entries/tags", () => client.entry.getTags().then(r => r.data))
|
||||
export const changeReadingMode = createAsyncThunk<void, ReadingMode, { state: RootState }>(
|
||||
"settings/readingMode",
|
||||
(readingMode, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, readingMode })
|
||||
thunkApi.dispatch(reloadEntries())
|
||||
}
|
||||
)
|
||||
export const changeReadingOrder = createAsyncThunk<void, ReadingOrder, { state: RootState }>(
|
||||
"settings/readingOrder",
|
||||
(readingOrder, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, readingOrder })
|
||||
thunkApi.dispatch(reloadEntries())
|
||||
}
|
||||
)
|
||||
export const changeLanguage = createAsyncThunk<
|
||||
void,
|
||||
string,
|
||||
{
|
||||
state: RootState
|
||||
}
|
||||
>("settings/language", (language, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, language })
|
||||
})
|
||||
export const changeScrollSpeed = createAsyncThunk<
|
||||
void,
|
||||
boolean,
|
||||
{
|
||||
state: RootState
|
||||
}
|
||||
>("settings/scrollSpeed", (speed, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 })
|
||||
})
|
||||
export const changeShowRead = createAsyncThunk<
|
||||
void,
|
||||
boolean,
|
||||
{
|
||||
state: RootState
|
||||
}
|
||||
>("settings/showRead", (showRead, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, showRead })
|
||||
})
|
||||
export const changeScrollMarks = createAsyncThunk<
|
||||
void,
|
||||
boolean,
|
||||
{
|
||||
state: RootState
|
||||
}
|
||||
>("settings/scrollMarks", (scrollMarks, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({ ...settings, scrollMarks })
|
||||
})
|
||||
export const changeSharingSetting = createAsyncThunk<
|
||||
void,
|
||||
{ site: keyof SharingSettings; value: boolean },
|
||||
{
|
||||
state: RootState
|
||||
}
|
||||
>("settings/sharingSetting", (sharingSetting, thunkApi) => {
|
||||
const { settings } = thunkApi.getState().user
|
||||
if (!settings) return
|
||||
client.user.saveSettings({
|
||||
...settings,
|
||||
sharingSettings: {
|
||||
...settings.sharingSettings,
|
||||
[sharingSetting.site]: sharingSetting.value,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
export const userSlice = createSlice({
|
||||
name: "user",
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: builder => {
|
||||
builder.addCase(reloadSettings.fulfilled, (state, action) => {
|
||||
state.settings = action.payload
|
||||
})
|
||||
builder.addCase(reloadProfile.fulfilled, (state, action) => {
|
||||
state.profile = action.payload
|
||||
})
|
||||
builder.addCase(reloadTags.fulfilled, (state, action) => {
|
||||
state.tags = action.payload
|
||||
})
|
||||
builder.addCase(changeReadingMode.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.readingMode = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeReadingOrder.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.readingOrder = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeLanguage.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.language = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeScrollSpeed.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.scrollSpeed = action.meta.arg ? 400 : 0
|
||||
})
|
||||
builder.addCase(changeShowRead.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.showRead = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeScrollMarks.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.scrollMarks = action.meta.arg
|
||||
})
|
||||
builder.addCase(changeSharingSetting.pending, (state, action) => {
|
||||
if (!state.settings) return
|
||||
state.settings.sharingSettings[action.meta.arg.site] = action.meta.arg.value
|
||||
})
|
||||
builder.addMatcher(
|
||||
isAnyOf(
|
||||
changeLanguage.fulfilled,
|
||||
changeScrollSpeed.fulfilled,
|
||||
changeShowRead.fulfilled,
|
||||
changeScrollMarks.fulfilled,
|
||||
changeSharingSetting.fulfilled
|
||||
),
|
||||
() => {
|
||||
showNotification({
|
||||
message: t`Settings saved.`,
|
||||
color: "green",
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default userSlice.reducer
|
||||
26
commafeed-client/src/app/store.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { configureStore } from "@reduxjs/toolkit"
|
||||
import { setupListeners } from "@reduxjs/toolkit/query"
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||
import entriesReducer from "./slices/entries"
|
||||
import redirectReducer from "./slices/redirect"
|
||||
import serverReducer from "./slices/server"
|
||||
import treeReducer from "./slices/tree"
|
||||
import userReducer from "./slices/user"
|
||||
|
||||
export const reducers = {
|
||||
entries: entriesReducer,
|
||||
redirect: redirectReducer,
|
||||
tree: treeReducer,
|
||||
server: serverReducer,
|
||||
user: userReducer,
|
||||
}
|
||||
|
||||
export const store = configureStore({ reducer: reducers })
|
||||
|
||||
setupListeners(store.dispatch)
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
308
commafeed-client/src/app/types.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
export interface AddCategoryRequest {
|
||||
name: string
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
export interface ApplicationSettings {
|
||||
publicUrl: string
|
||||
allowRegistrations: boolean
|
||||
createDemoAccount: boolean
|
||||
googleAnalyticsTrackingCode?: string
|
||||
googleAuthKey?: string
|
||||
backgroundThreads: number
|
||||
databaseUpdateThreads: number
|
||||
smtpHost?: string
|
||||
smtpPort?: number
|
||||
smtpTls?: boolean
|
||||
smtpUserName?: string
|
||||
smtpPassword?: string
|
||||
smtpFromAddress?: string
|
||||
graphiteEnabled?: boolean
|
||||
graphitePrefix?: string
|
||||
graphiteHost?: string
|
||||
graphitePort?: number
|
||||
graphiteInterval?: number
|
||||
heavyLoad: boolean
|
||||
pubsubhubbub: boolean
|
||||
imageProxyEnabled: boolean
|
||||
queryTimeout: number
|
||||
keepStatusDays: number
|
||||
maxFeedCapacity: number
|
||||
refreshIntervalMinutes: number
|
||||
cache: ApplicationSettingsCache
|
||||
announcement?: string
|
||||
userAgent?: string
|
||||
unreadThreshold?: Date
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string
|
||||
parentId?: string
|
||||
parentName?: string
|
||||
name: string
|
||||
children: Category[]
|
||||
feeds: Subscription[]
|
||||
expanded: boolean
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface CategoryModificationRequest {
|
||||
id: number
|
||||
name?: string
|
||||
parentId?: string
|
||||
position?: number
|
||||
}
|
||||
|
||||
export interface CollapseRequest {
|
||||
id: number
|
||||
collapse: boolean
|
||||
}
|
||||
|
||||
export interface Entries {
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
feedLink: string
|
||||
timestamp: number
|
||||
hasMore: boolean
|
||||
offset?: number
|
||||
limit?: number
|
||||
entries: Entry[]
|
||||
ignoredReadStatus: boolean
|
||||
}
|
||||
|
||||
export interface Entry {
|
||||
id: string
|
||||
guid: string
|
||||
title: string
|
||||
content: string
|
||||
categories?: string
|
||||
rtl: boolean
|
||||
author?: string
|
||||
enclosureUrl?: string
|
||||
enclosureType?: string
|
||||
mediaDescription?: string
|
||||
mediaThumbnailUrl?: string
|
||||
mediaThumbnailWidth?: number
|
||||
mediaThumbnailHeight?: number
|
||||
date: number
|
||||
insertedDate: number
|
||||
feedId: string
|
||||
feedName: string
|
||||
feedUrl: string
|
||||
feedLink: string
|
||||
iconUrl: string
|
||||
url: string
|
||||
read: boolean
|
||||
starred: boolean
|
||||
markable: boolean
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface FeedInfo {
|
||||
url: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface FeedInfoRequest {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface FeedModificationRequest {
|
||||
id: number
|
||||
name?: string
|
||||
categoryId?: string
|
||||
position?: number
|
||||
filter?: string
|
||||
}
|
||||
|
||||
export interface GetEntriesRequest {
|
||||
id: string
|
||||
readType?: ReadingMode
|
||||
newerThan?: number
|
||||
order?: ReadingOrder
|
||||
keywords?: string
|
||||
onlyIds?: boolean
|
||||
excludedSubscriptionIds?: string
|
||||
tag?: string
|
||||
}
|
||||
|
||||
export interface GetEntriesPaginatedRequest extends GetEntriesRequest {
|
||||
offset: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
export interface IDRequest {
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
name: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface MarkRequest {
|
||||
id: string
|
||||
read: boolean
|
||||
olderThan?: number
|
||||
keywords?: string
|
||||
excludedSubscriptions?: number[]
|
||||
}
|
||||
|
||||
export interface MetricCounter {
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface MetricGauge {
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface MetricMeter {
|
||||
count: number
|
||||
m15_rate: number
|
||||
m1_rate: number
|
||||
m5_rate: number
|
||||
mean_rate: number
|
||||
units: string
|
||||
}
|
||||
|
||||
export type MetricTimer = {
|
||||
count: number
|
||||
max: number
|
||||
mean: number
|
||||
min: number
|
||||
p50: number
|
||||
p75: number
|
||||
p95: number
|
||||
p98: number
|
||||
p99: number
|
||||
p999: number
|
||||
stddev: number
|
||||
m15_rate: number
|
||||
m1_rate: number
|
||||
m5_rate: number
|
||||
mean_rate: number
|
||||
duration_units: string
|
||||
rate_units: string
|
||||
}
|
||||
|
||||
export interface Metrics {
|
||||
counters: { [key: string]: MetricCounter }
|
||||
gauges: { [key: string]: MetricGauge }
|
||||
meters: { [key: string]: MetricMeter }
|
||||
timers: { [key: string]: MetricTimer }
|
||||
}
|
||||
|
||||
export interface MultipleMarkRequest {
|
||||
requests: MarkRequest[]
|
||||
}
|
||||
|
||||
export interface PasswordResetRequest {
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface ProfileModificationRequest {
|
||||
currentPassword: string
|
||||
email: string
|
||||
newPassword?: string
|
||||
newApiKey?: boolean
|
||||
}
|
||||
|
||||
export interface RegistrationRequest {
|
||||
name: string
|
||||
password: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
announcement?: string
|
||||
version: string
|
||||
gitCommit: string
|
||||
allowRegistrations: boolean
|
||||
googleAnalyticsCode?: string
|
||||
smtpEnabled: boolean
|
||||
demoAccountEnabled: boolean
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
language: string
|
||||
readingMode: ReadingMode
|
||||
readingOrder: ReadingOrder
|
||||
showRead: boolean
|
||||
scrollMarks: boolean
|
||||
customCss?: string
|
||||
customJs?: string
|
||||
scrollSpeed: number
|
||||
sharingSettings: SharingSettings
|
||||
}
|
||||
|
||||
export interface SharingSettings {
|
||||
email: boolean
|
||||
gmail: boolean
|
||||
facebook: boolean
|
||||
twitter: boolean
|
||||
tumblr: boolean
|
||||
pocket: boolean
|
||||
instapaper: boolean
|
||||
buffer: boolean
|
||||
}
|
||||
|
||||
export interface StarRequest {
|
||||
id: string
|
||||
feedId: number
|
||||
starred: boolean
|
||||
}
|
||||
|
||||
export interface SubscribeRequest {
|
||||
url: string
|
||||
title: string
|
||||
categoryId?: string
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: number
|
||||
name: string
|
||||
message?: string
|
||||
errorCount: number
|
||||
lastRefresh?: number
|
||||
nextRefresh?: number
|
||||
feedUrl: string
|
||||
feedLink: string
|
||||
iconUrl: string
|
||||
unread: number
|
||||
categoryId?: string
|
||||
position?: number
|
||||
newestItemTime?: number
|
||||
filter?: string
|
||||
}
|
||||
|
||||
export interface TagRequest {
|
||||
entryId: number
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface UnreadCount {
|
||||
feedId?: number
|
||||
unreadCount?: number
|
||||
newestItemTime?: number
|
||||
}
|
||||
|
||||
export interface UserModel {
|
||||
id: number
|
||||
name: string
|
||||
email?: string
|
||||
apiKey?: string
|
||||
password?: string
|
||||
enabled: boolean
|
||||
created: number
|
||||
lastLogin?: number
|
||||
admin: boolean
|
||||
}
|
||||
|
||||
export type ApplicationSettingsCache = "NOOP" | "REDIS"
|
||||
|
||||
export type ReadingMode = "all" | "unread"
|
||||
|
||||
export type ReadingOrder = "asc" | "desc"
|
||||
|
||||
export type ViewMode = "title" | "cozy" | "detailed" | "expanded"
|
||||
66
commafeed-client/src/app/utils.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Category } from "./types"
|
||||
|
||||
export function visitCategoryTree(category: Category, visitor: (category: Category) => void): void {
|
||||
visitor(category)
|
||||
category.children.forEach(child => visitCategoryTree(child, visitor))
|
||||
}
|
||||
|
||||
export function flattenCategoryTree(category: Category): Category[] {
|
||||
const categories: Category[] = []
|
||||
visitCategoryTree(category, c => categories.push(c))
|
||||
return categories
|
||||
}
|
||||
|
||||
export function categoryUnreadCount(category?: Category): number {
|
||||
if (!category) return 0
|
||||
|
||||
return flattenCategoryTree(category)
|
||||
.flatMap(c => c.feeds)
|
||||
.map(f => f.unread)
|
||||
.reduce((total, current) => total + current, 0)
|
||||
}
|
||||
|
||||
export const calculatePlaceholderSize = ({ width, height, maxWidth }: { width?: number; height?: number; maxWidth: number }) => {
|
||||
const placeholderWidth = width && Math.min(width, maxWidth)
|
||||
const placeholderHeight = height && width && width > maxWidth ? height * (maxWidth / width) : height
|
||||
return { width: placeholderWidth, height: placeholderHeight }
|
||||
}
|
||||
|
||||
export const scrollToWithCallback = ({
|
||||
element,
|
||||
options,
|
||||
onScrollEnded,
|
||||
}: {
|
||||
element: HTMLElement
|
||||
options: ScrollToOptions
|
||||
onScrollEnded: () => void
|
||||
}) => {
|
||||
const offset = (options.top ?? 0).toFixed()
|
||||
|
||||
const onScroll = () => {
|
||||
if (element.offsetTop.toFixed() === offset) {
|
||||
element.removeEventListener("scroll", onScroll)
|
||||
onScrollEnded()
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener("scroll", onScroll)
|
||||
|
||||
// scrollTo does not trigger if there's nothing to do, trigger it manually
|
||||
onScroll()
|
||||
|
||||
element.scrollTo(options)
|
||||
}
|
||||
|
||||
export const openLinkInBackgroundTab = (url: string) => {
|
||||
// simulate ctrl+click to open tab in background
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.rel = "noreferrer"
|
||||
a.dispatchEvent(
|
||||
new MouseEvent("click", {
|
||||
ctrlKey: true,
|
||||
metaKey: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
commafeed-client/src/assets/welcome_page_dark.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
commafeed-client/src/assets/welcome_page_light.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
34
commafeed-client/src/components/ActionButtton.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { ActionIcon, Button, useMantineTheme } from "@mantine/core"
|
||||
import { ActionIconProps } from "@mantine/core/lib/ActionIcon/ActionIcon"
|
||||
import { ButtonProps } from "@mantine/core/lib/Button/Button"
|
||||
import { useMediaQuery } from "@mantine/hooks"
|
||||
import { forwardRef, MouseEventHandler, ReactNode } from "react"
|
||||
|
||||
interface ActionButtonProps {
|
||||
className?: string
|
||||
icon?: ReactNode
|
||||
label?: ReactNode
|
||||
onClick?: MouseEventHandler
|
||||
variant?: ActionIconProps["variant"] & ButtonProps["variant"]
|
||||
showLabelOnMobile?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches between Button with label (desktop) and ActionIcon (mobile)
|
||||
*/
|
||||
export const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>((props: ActionButtonProps, ref) => {
|
||||
const theme = useMantineTheme()
|
||||
const variant = props.variant ?? "subtle"
|
||||
const mobile = !useMediaQuery(`(min-width: ${theme.breakpoints.lg})`)
|
||||
const iconOnly = !props.showLabelOnMobile && (mobile || !props.label)
|
||||
return iconOnly ? (
|
||||
<ActionIcon ref={ref} color={theme.primaryColor} variant={variant} className={props.className} onClick={props.onClick}>
|
||||
{props.icon}
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<Button ref={ref} variant={variant} size="xs" className={props.className} leftIcon={props.icon} onClick={props.onClick}>
|
||||
{props.label}
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
ActionButton.displayName = "HeaderButton"
|
||||
48
commafeed-client/src/components/Alert.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Alert as MantineAlert } from "@mantine/core"
|
||||
import { Fragment } from "react"
|
||||
import { TbAlertCircle, TbAlertTriangle, TbCircleCheck } from "react-icons/tb"
|
||||
|
||||
type Level = "error" | "warning" | "success"
|
||||
export interface ErrorsAlertProps {
|
||||
level?: Level
|
||||
messages: string[]
|
||||
}
|
||||
|
||||
export function Alert(props: ErrorsAlertProps) {
|
||||
let title: React.ReactNode
|
||||
let color: string
|
||||
let icon: React.ReactNode
|
||||
|
||||
const level = props.level ?? "error"
|
||||
switch (level) {
|
||||
case "error":
|
||||
title = <Trans>Error</Trans>
|
||||
color = "red"
|
||||
icon = <TbAlertCircle />
|
||||
break
|
||||
case "warning":
|
||||
title = <Trans>Warning</Trans>
|
||||
color = "orange"
|
||||
icon = <TbAlertTriangle />
|
||||
break
|
||||
case "success":
|
||||
title = <Trans>Success</Trans>
|
||||
color = "green"
|
||||
icon = <TbCircleCheck />
|
||||
break
|
||||
default:
|
||||
throw Error(`unsupported level: ${level}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<MantineAlert title={title} color={color} icon={icon}>
|
||||
{props.messages.map((m, i) => (
|
||||
<Fragment key={m}>
|
||||
<Box>{m}</Box>
|
||||
{i !== props.messages.length - 1 && <br />}
|
||||
</Fragment>
|
||||
))}
|
||||
</MantineAlert>
|
||||
)
|
||||
}
|
||||
5
commafeed-client/src/components/ButtonToolbar.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Group } from "@mantine/core"
|
||||
|
||||
export function ButtonToolbar(props: { children: React.ReactNode }) {
|
||||
return <Group spacing={14}>{props.children}</Group>
|
||||
}
|
||||
26
commafeed-client/src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ErrorPage } from "pages/ErrorPage"
|
||||
import React, { ReactNode } from "react"
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
error?: Error
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
this.setState({ error })
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) return <ErrorPage error={this.state.error} />
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Box, Center, createStyles } from "@mantine/core"
|
||||
import { useState } from "react"
|
||||
import { TbPhoto } from "react-icons/tb"
|
||||
|
||||
interface ImageWithPlaceholderWhileLoadingProps {
|
||||
src: string
|
||||
alt: string
|
||||
title?: string
|
||||
width?: number
|
||||
height?: number | "auto"
|
||||
placeholderWidth?: number
|
||||
placeholderHeight?: number
|
||||
placeholderBackgroundColor?: string
|
||||
placeholderIconSize?: number
|
||||
placeholderIconColor?: string
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme, props: ImageWithPlaceholderWhileLoadingProps) => ({
|
||||
placeholder: {
|
||||
width: props.placeholderWidth ?? 400,
|
||||
height: props.placeholderHeight ?? 600,
|
||||
maxWidth: "100%",
|
||||
color: props.placeholderIconColor ?? theme.fn.variant({ color: theme.primaryColor, variant: "subtle" }).color,
|
||||
backgroundColor: props.placeholderBackgroundColor ?? (theme.colorScheme === "dark" ? theme.colors.dark[5] : theme.colors.gray[1]),
|
||||
},
|
||||
}))
|
||||
|
||||
export function ImageWithPlaceholderWhileLoading(props: ImageWithPlaceholderWhileLoadingProps) {
|
||||
const { classes } = useStyles(props)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && (
|
||||
<Box>
|
||||
<Center className={classes.placeholder}>
|
||||
<div>
|
||||
<TbPhoto size={props.placeholderIconSize ?? 48} />
|
||||
</div>
|
||||
</Center>
|
||||
</Box>
|
||||
)}
|
||||
<img
|
||||
src={props.src}
|
||||
alt={props.alt}
|
||||
title={props.title}
|
||||
width={props.width}
|
||||
height={props.height}
|
||||
onLoad={() => setLoading(false)}
|
||||
style={{ display: loading ? "none" : "block" }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
183
commafeed-client/src/components/KeyboardShortcutsHelp.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Kbd, Table } from "@mantine/core"
|
||||
|
||||
export function KeyboardShortcutsHelp() {
|
||||
return (
|
||||
<Table striped highlightOnHover>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Refresh</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>R</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open next entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>J</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open previous entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>K</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Set focus on next entry without opening it</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>N</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Set focus on previous entry without opening it</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>P</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Move the page down</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Space</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Move the page up</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Shift</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>
|
||||
<Trans>Space</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open/close current entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>O</Kbd>
|
||||
<span>, </span>
|
||||
<Kbd>
|
||||
<Trans>Enter</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open current entry in a new tab</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>V</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Open current entry in a new tab in the background</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>B</Kbd>
|
||||
<span>, </span>
|
||||
<Kbd>
|
||||
<Trans>Middle click</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Toggle read status of current entry</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>M</Kbd>
|
||||
<span>, </span>
|
||||
<Trans>Swipe header to the right</Trans>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Mark all entries as read</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Shift</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>A</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Go to the All view</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>G</Kbd>
|
||||
<span> </span>
|
||||
<Kbd>A</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Navigate to a subscription by entering its name</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Ctrl</Trans>
|
||||
</Kbd>
|
||||
<span> + </span>
|
||||
<Kbd>K</Kbd>
|
||||
<span>, </span>
|
||||
<Kbd>G</Kbd>
|
||||
<span> </span>
|
||||
<Kbd>U</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show entry menu (desktop)</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Right click</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show entry menu (mobile)</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>
|
||||
<Trans>Long press</Trans>
|
||||
</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Trans>Show keyboard shortcut help</Trans>
|
||||
</td>
|
||||
<td>
|
||||
<Kbd>?</Kbd>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
9
commafeed-client/src/components/Loader.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Center, Loader as MantineLoader } from "@mantine/core"
|
||||
|
||||
export function Loader() {
|
||||
return (
|
||||
<Center>
|
||||
<MantineLoader size="xl" variant="bars" />
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
10
commafeed-client/src/components/Logo.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Image } from "@mantine/core"
|
||||
import logo from "assets/logo.svg"
|
||||
|
||||
export interface LogoProps {
|
||||
size: number
|
||||
}
|
||||
|
||||
export function Logo(props: LogoProps) {
|
||||
return <Image src={logo} width={props.size} />
|
||||
}
|
||||
14
commafeed-client/src/components/RelativeDate.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import dayjs from "dayjs"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export function RelativeDate(props: { date: Date | number | undefined }) {
|
||||
const [now, setNow] = useState(new Date())
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => setNow(new Date()), 60 * 1000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (!props.date) return <Trans>N/A</Trans>
|
||||
return <>{dayjs(props.date).from(dayjs(now))}</>
|
||||
}
|
||||
50
commafeed-client/src/components/admin/UserEdit.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Button, Checkbox, Group, PasswordInput, Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { UserModel } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy } from "react-icons/tb"
|
||||
|
||||
interface UserEditProps {
|
||||
user?: UserModel
|
||||
onCancel: () => void
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
export function UserEdit(props: UserEditProps) {
|
||||
const form = useForm<UserModel>({
|
||||
initialValues: props.user ?? ({ enabled: true } as UserModel),
|
||||
})
|
||||
const saveUser = useAsyncCallback(client.admin.saveUser, { onSuccess: props.onSave })
|
||||
|
||||
return (
|
||||
<>
|
||||
{saveUser.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(saveUser.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(saveUser.execute)}>
|
||||
<Stack>
|
||||
<TextInput label={<Trans>Name</Trans>} {...form.getInputProps("name")} required />
|
||||
<PasswordInput label={<Trans>Password</Trans>} {...form.getInputProps("password")} required={!props.user} />
|
||||
<TextInput type="email" label={<Trans>E-mail</Trans>} {...form.getInputProps("email")} />
|
||||
<Checkbox label={<Trans>Admin</Trans>} {...form.getInputProps("admin", { type: "checkbox" })} />
|
||||
<Checkbox label={<Trans>Enabled</Trans>} {...form.getInputProps("enabled", { type: "checkbox" })} />
|
||||
|
||||
<Group>
|
||||
<Button variant="default" onClick={props.onCancel}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveUser.loading}>
|
||||
<Trans>Save</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
97
commafeed-client/src/components/content/Content.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Box, createStyles, Mark, TypographyStylesProvider } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { calculatePlaceholderSize } from "app/utils"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave"
|
||||
|
||||
export interface ContentProps {
|
||||
content: string
|
||||
}
|
||||
|
||||
const useStyles = createStyles(theme => ({
|
||||
content: {
|
||||
// break long links or long words
|
||||
overflowWrap: "anywhere",
|
||||
"& a": {
|
||||
color: theme.fn.variant({ color: theme.primaryColor, variant: "subtle" }).color,
|
||||
},
|
||||
"& iframe": {
|
||||
maxWidth: "100%",
|
||||
},
|
||||
"& pre, & code": {
|
||||
whiteSpace: "pre-wrap",
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const transform: TransformCallback = node => {
|
||||
if (node.tagName === "IMG") {
|
||||
// show placeholders for loading img tags, this allows the entry to have its final height immediately
|
||||
const src = node.getAttribute("src") ?? undefined
|
||||
if (!src) return undefined
|
||||
|
||||
const alt = node.getAttribute("alt") ?? "image"
|
||||
const title = node.getAttribute("title") ?? undefined
|
||||
const nodeWidth = node.getAttribute("width")
|
||||
const nodeHeight = node.getAttribute("height")
|
||||
const width = nodeWidth ? parseInt(nodeWidth, 10) : undefined
|
||||
const height = nodeHeight ? parseInt(nodeHeight, 10) : undefined
|
||||
const placeholderSize = calculatePlaceholderSize({
|
||||
width,
|
||||
height,
|
||||
maxWidth: Constants.layout.entryMaxWidth,
|
||||
})
|
||||
|
||||
return (
|
||||
<ImageWithPlaceholderWhileLoading
|
||||
src={src}
|
||||
alt={alt}
|
||||
title={title}
|
||||
width={width}
|
||||
height="auto"
|
||||
placeholderWidth={placeholderSize.width}
|
||||
placeholderHeight={placeholderSize.height}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
class HighlightMatcher extends Matcher {
|
||||
private search: string
|
||||
|
||||
constructor(search: string) {
|
||||
super("highlight")
|
||||
this.search = search
|
||||
}
|
||||
|
||||
match(string: string): MatchResponse<unknown> | null {
|
||||
const pattern = this.search.split(" ").join("|")
|
||||
return this.doMatch(string, new RegExp(pattern, "i"), () => ({}))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
|
||||
replaceWith(children: ChildrenNode, props: unknown): Node {
|
||||
return <Mark>{children}</Mark>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
asTag(): string {
|
||||
return "span"
|
||||
}
|
||||
}
|
||||
|
||||
export function Content(props: ContentProps) {
|
||||
const { classes } = useStyles()
|
||||
const search = useAppSelector(state => state.entries.search)
|
||||
const matchers = search ? [new HighlightMatcher(search)] : []
|
||||
|
||||
return (
|
||||
<TypographyStylesProvider>
|
||||
<Box className={classes.content}>
|
||||
<Interweave content={props.content} transform={transform} matchers={matchers} />
|
||||
</Box>
|
||||
</TypographyStylesProvider>
|
||||
)
|
||||
}
|
||||
26
commafeed-client/src/components/content/Enclosure.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { TypographyStylesProvider } from "@mantine/core"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
|
||||
export function Enclosure(props: { enclosureType: string; enclosureUrl: string }) {
|
||||
const hasVideo = props.enclosureType && props.enclosureType.indexOf("video") === 0
|
||||
const hasAudio = props.enclosureType && props.enclosureType.indexOf("audio") === 0
|
||||
const hasImage = props.enclosureType && props.enclosureType.indexOf("image") === 0
|
||||
|
||||
return (
|
||||
<TypographyStylesProvider>
|
||||
{hasVideo && (
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
<video controls>
|
||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||
</video>
|
||||
)}
|
||||
{hasAudio && (
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
<audio controls>
|
||||
<source src={props.enclosureUrl} type={props.enclosureType} />
|
||||
</audio>
|
||||
)}
|
||||
{hasImage && <ImageWithPlaceholderWhileLoading src={props.enclosureUrl} alt="enclosure" />}
|
||||
</TypographyStylesProvider>
|
||||
)
|
||||
}
|
||||
273
commafeed-client/src/components/content/FeedEntries.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { openModal } from "@mantine/modals"
|
||||
import { Constants } from "app/constants"
|
||||
import {
|
||||
ExpendableEntry,
|
||||
loadMoreEntries,
|
||||
markAllEntries,
|
||||
markEntry,
|
||||
reloadEntries,
|
||||
selectEntry,
|
||||
selectNextEntry,
|
||||
selectPreviousEntry,
|
||||
} from "app/slices/entries"
|
||||
import { redirectToRootCategory } from "app/slices/redirect"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { openLinkInBackgroundTab } from "app/utils"
|
||||
import { KeyboardShortcutsHelp } from "components/KeyboardShortcutsHelp"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import throttle from "lodash/throttle"
|
||||
import { useEffect } from "react"
|
||||
import InfiniteScroll from "react-infinite-scroller"
|
||||
import { FeedEntry } from "./FeedEntry"
|
||||
|
||||
export function FeedEntries() {
|
||||
const source = useAppSelector(state => state.entries.source)
|
||||
const entries = useAppSelector(state => state.entries.entries)
|
||||
const entriesTimestamp = useAppSelector(state => state.entries.timestamp)
|
||||
const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId)
|
||||
const hasMore = useAppSelector(state => state.entries.hasMore)
|
||||
const { viewMode } = useViewMode()
|
||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||
const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const selectedEntry = entries.find(e => e.id === selectedEntryId)
|
||||
|
||||
const headerClicked = (entry: ExpendableEntry, event: React.MouseEvent) => {
|
||||
if (event.button === 1 || event.ctrlKey || event.metaKey) {
|
||||
// middle click
|
||||
dispatch(markEntry({ entry, read: true }))
|
||||
} else if (event.button === 0) {
|
||||
// main click
|
||||
// don't trigger the link
|
||||
event.preventDefault()
|
||||
|
||||
dispatch(
|
||||
selectEntry({
|
||||
entry,
|
||||
expand: !entry.expanded,
|
||||
markAsRead: !entry.expanded,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
|
||||
const listener = () => {
|
||||
if (viewMode !== "expanded") return
|
||||
if (scrollingToEntry) return
|
||||
|
||||
const currentEntry = entries
|
||||
// use slice to get a copy of the array because reverse mutates the array in-place
|
||||
.slice()
|
||||
.reverse()
|
||||
.find(e => {
|
||||
const el = document.getElementById(Constants.dom.entryId(e))
|
||||
return el && !Constants.layout.isTopVisible(el)
|
||||
})
|
||||
if (currentEntry) {
|
||||
dispatch(
|
||||
selectEntry({
|
||||
entry: currentEntry,
|
||||
expand: false,
|
||||
markAsRead: !!scrollMarks,
|
||||
scrollToEntry: false,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
const throttledListener = throttle(listener, 100)
|
||||
scrollArea?.addEventListener("scroll", throttledListener)
|
||||
return () => scrollArea?.removeEventListener("scroll", throttledListener)
|
||||
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
|
||||
|
||||
useMousetrap("r", () => dispatch(reloadEntries()))
|
||||
useMousetrap("j", () =>
|
||||
dispatch(
|
||||
selectNextEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
useMousetrap("n", () =>
|
||||
dispatch(
|
||||
selectNextEntry({
|
||||
expand: false,
|
||||
markAsRead: false,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
useMousetrap("k", () =>
|
||||
dispatch(
|
||||
selectPreviousEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
useMousetrap("p", () =>
|
||||
dispatch(
|
||||
selectPreviousEntry({
|
||||
expand: false,
|
||||
markAsRead: false,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
useMousetrap("space", () => {
|
||||
if (selectedEntry) {
|
||||
if (selectedEntry.expanded) {
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry))
|
||||
if (entryElement && Constants.layout.isBottomVisible(entryElement)) {
|
||||
dispatch(
|
||||
selectNextEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
scrollArea?.scrollTo({
|
||||
top: scrollArea.scrollTop + scrollArea.clientHeight * 0.8,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
dispatch(
|
||||
selectEntry({
|
||||
entry: selectedEntry,
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
dispatch(
|
||||
selectNextEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
useMousetrap("shift+space", () => {
|
||||
if (selectedEntry) {
|
||||
if (selectedEntry.expanded) {
|
||||
const entryElement = document.getElementById(Constants.dom.entryId(selectedEntry))
|
||||
if (entryElement && Constants.layout.isTopVisible(entryElement)) {
|
||||
dispatch(
|
||||
selectPreviousEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
scrollArea?.scrollTo({
|
||||
top: scrollArea.scrollTop - scrollArea.clientHeight * 0.8,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
dispatch(
|
||||
selectPreviousEntry({
|
||||
expand: true,
|
||||
markAsRead: true,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
useMousetrap(["o", "enter"], () => {
|
||||
// toggle expanded status
|
||||
if (!selectedEntry) return
|
||||
dispatch(
|
||||
selectEntry({
|
||||
entry: selectedEntry,
|
||||
expand: !selectedEntry.expanded,
|
||||
markAsRead: !selectedEntry.expanded,
|
||||
scrollToEntry: true,
|
||||
})
|
||||
)
|
||||
})
|
||||
useMousetrap("v", () => {
|
||||
// open tab in foreground
|
||||
if (!selectedEntry) return
|
||||
window.open(selectedEntry.url, "_blank", "noreferrer")
|
||||
})
|
||||
useMousetrap("b", () => {
|
||||
// simulate ctrl+click to open tab in background
|
||||
if (!selectedEntry) return
|
||||
openLinkInBackgroundTab(selectedEntry.url)
|
||||
})
|
||||
useMousetrap("m", () => {
|
||||
// toggle read status
|
||||
if (!selectedEntry) return
|
||||
dispatch(markEntry({ entry: selectedEntry, read: !selectedEntry.read }))
|
||||
})
|
||||
useMousetrap("shift+a", () => {
|
||||
// mark all entries as read
|
||||
dispatch(
|
||||
markAllEntries({
|
||||
sourceType: source.type,
|
||||
req: {
|
||||
id: source.id,
|
||||
read: true,
|
||||
olderThan: entriesTimestamp,
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
useMousetrap("g a", () => dispatch(redirectToRootCategory()))
|
||||
useMousetrap("?", () =>
|
||||
openModal({
|
||||
title: <Trans>Keyboard shortcuts</Trans>,
|
||||
size: "xl",
|
||||
children: <KeyboardShortcutsHelp />,
|
||||
})
|
||||
)
|
||||
|
||||
if (!entries) return <Loader />
|
||||
return (
|
||||
<InfiniteScroll
|
||||
id="entries"
|
||||
initialLoad={false}
|
||||
loadMore={() => dispatch(loadMoreEntries())}
|
||||
hasMore={hasMore}
|
||||
loader={<Loader key={0} />}
|
||||
useWindow={false}
|
||||
getScrollParent={() => document.getElementById(Constants.dom.mainScrollAreaId)}
|
||||
>
|
||||
{entries.map(entry => (
|
||||
<div
|
||||
key={entry.id}
|
||||
ref={el => {
|
||||
if (el) el.id = Constants.dom.entryId(entry)
|
||||
}}
|
||||
>
|
||||
<FeedEntry
|
||||
entry={entry}
|
||||
expanded={!!entry.expanded || viewMode === "expanded"}
|
||||
showSelectionIndicator={entry.id === selectedEntryId && (!entry.expanded || viewMode === "expanded")}
|
||||
onHeaderClick={event => headerClicked(entry, event)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
)
|
||||
}
|
||||
125
commafeed-client/src/components/content/FeedEntry.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Box, createStyles, Divider, Paper } from "@mantine/core"
|
||||
import { MantineNumberSize } from "@mantine/styles"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntry } from "app/slices/entries"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { Entry, ViewMode } from "app/types"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import React from "react"
|
||||
import { useSwipeable } from "react-swipeable"
|
||||
import { FeedEntryBody } from "./FeedEntryBody"
|
||||
import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader"
|
||||
import { FeedEntryContextMenu, useFeedEntryContextMenu } from "./FeedEntryContextMenu"
|
||||
import { FeedEntryFooter } from "./FeedEntryFooter"
|
||||
import { FeedEntryHeader } from "./FeedEntryHeader"
|
||||
|
||||
interface FeedEntryProps {
|
||||
entry: Entry
|
||||
expanded: boolean
|
||||
showSelectionIndicator: boolean
|
||||
onHeaderClick: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: ViewMode }) => {
|
||||
let backgroundColor
|
||||
if (theme.colorScheme === "dark") backgroundColor = props.entry.read ? "inherit" : theme.colors.dark[5]
|
||||
else backgroundColor = props.entry.read && !props.expanded ? theme.colors.gray[0] : "inherit"
|
||||
|
||||
let marginY = 10
|
||||
if (props.viewMode === "title") marginY = 2
|
||||
else if (props.viewMode === "cozy") marginY = 6
|
||||
|
||||
let mobileMarginY = 6
|
||||
if (props.viewMode === "title") mobileMarginY = 2
|
||||
else if (props.viewMode === "cozy") mobileMarginY = 4
|
||||
|
||||
let backgroundHoverColor = backgroundColor
|
||||
if (!props.expanded && !props.entry.read) {
|
||||
backgroundHoverColor = theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1]
|
||||
}
|
||||
|
||||
const styles = {
|
||||
paper: {
|
||||
backgroundColor,
|
||||
marginTop: marginY,
|
||||
marginBottom: marginY,
|
||||
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
||||
marginTop: mobileMarginY,
|
||||
marginBottom: mobileMarginY,
|
||||
},
|
||||
"@media (hover: hover)": {
|
||||
"&:hover": {
|
||||
backgroundColor: backgroundHoverColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
headerLink: {
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
},
|
||||
body: {
|
||||
maxWidth: Constants.layout.entryMaxWidth,
|
||||
},
|
||||
}
|
||||
|
||||
if (props.showSelectionIndicator) {
|
||||
styles.paper.borderLeftColor = theme.colorScheme === "dark" ? theme.colors.orange[4] : theme.colors.orange[6]
|
||||
}
|
||||
|
||||
return styles
|
||||
})
|
||||
|
||||
export function FeedEntry(props: FeedEntryProps) {
|
||||
const { viewMode } = useViewMode()
|
||||
const { classes } = useStyles({ ...props, viewMode })
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const swipeHandlers = useSwipeable({
|
||||
onSwipedRight: () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read })),
|
||||
})
|
||||
|
||||
const { onContextMenu } = useFeedEntryContextMenu(props.entry)
|
||||
|
||||
let paddingX: MantineNumberSize = "xs"
|
||||
if (viewMode === "title" || viewMode === "cozy") paddingX = 6
|
||||
|
||||
let paddingY: MantineNumberSize = "xs"
|
||||
if (viewMode === "title") paddingY = 4
|
||||
else if (viewMode === "cozy") paddingY = 8
|
||||
|
||||
let borderRadius: MantineNumberSize = "sm"
|
||||
if (viewMode === "title") borderRadius = 0
|
||||
else if (viewMode === "cozy") borderRadius = "xs"
|
||||
|
||||
const compactHeader = !props.expanded && (viewMode === "title" || viewMode === "cozy")
|
||||
return (
|
||||
<Paper withBorder radius={borderRadius} className={classes.paper}>
|
||||
<a
|
||||
className={classes.headerLink}
|
||||
href={props.entry.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={props.onHeaderClick}
|
||||
onAuxClick={props.onHeaderClick}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<Box px={paddingX} py={paddingY} {...swipeHandlers}>
|
||||
{compactHeader && <FeedEntryCompactHeader entry={props.entry} />}
|
||||
{!compactHeader && <FeedEntryHeader entry={props.entry} expanded={props.expanded} />}
|
||||
</Box>
|
||||
</a>
|
||||
{props.expanded && (
|
||||
<Box px={paddingX} pb={paddingY}>
|
||||
<Box className={classes.body} sx={{ direction: props.entry.rtl ? "rtl" : "ltr" }}>
|
||||
<FeedEntryBody entry={props.entry} />
|
||||
</Box>
|
||||
<Divider variant="dashed" my={paddingY} />
|
||||
<FeedEntryFooter entry={props.entry} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<FeedEntryContextMenu entry={props.entry} />
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
35
commafeed-client/src/components/content/FeedEntryBody.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { Entry } from "app/types"
|
||||
import { Content } from "./Content"
|
||||
import { Enclosure } from "./Enclosure"
|
||||
import { Media } from "./Media"
|
||||
|
||||
export interface FeedEntryBodyProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
export function FeedEntryBody(props: FeedEntryBodyProps) {
|
||||
return (
|
||||
<Box>
|
||||
<Box>
|
||||
<Content content={props.entry.content} />
|
||||
</Box>
|
||||
{props.entry.enclosureType && props.entry.enclosureUrl && (
|
||||
<Box pt="md">
|
||||
<Enclosure enclosureType={props.entry.enclosureType} enclosureUrl={props.entry.enclosureUrl} />
|
||||
</Box>
|
||||
)}
|
||||
{/* show media only if we don't have content to avoid duplicate content */}
|
||||
{!props.entry.content && props.entry.mediaThumbnailUrl && (
|
||||
<Box pt="md">
|
||||
<Media
|
||||
thumbnailUrl={props.entry.mediaThumbnailUrl}
|
||||
thumbnailWidth={props.entry.mediaThumbnailWidth}
|
||||
thumbnailHeight={props.entry.mediaThumbnailHeight}
|
||||
description={props.entry.mediaDescription}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Box, createStyles, Text } from "@mantine/core"
|
||||
import { Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
import { FeedFavicon } from "./FeedFavicon"
|
||||
|
||||
export interface FeedEntryHeaderProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme, props: FeedEntryHeaderProps) => ({
|
||||
wrapper: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
columnGap: "10px",
|
||||
},
|
||||
title: {
|
||||
flexGrow: 1,
|
||||
fontWeight: theme.colorScheme === "light" && !props.entry.read ? "bold" : "inherit",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
feedName: {
|
||||
width: "145px",
|
||||
minWidth: "145px",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
date: {
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
}))
|
||||
export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
|
||||
const { classes } = useStyles(props)
|
||||
return (
|
||||
<Box className={classes.wrapper}>
|
||||
<Box>
|
||||
<FeedFavicon url={props.entry.iconUrl} />
|
||||
</Box>
|
||||
<OnDesktop>
|
||||
<Text color="dimmed" className={classes.feedName}>
|
||||
{props.entry.feedName}
|
||||
</Text>
|
||||
</OnDesktop>
|
||||
<Box className={classes.title}>
|
||||
<FeedEntryTitle entry={props.entry} />
|
||||
</Box>
|
||||
<OnDesktop>
|
||||
<Text color="dimmed" className={classes.date}>
|
||||
<RelativeDate date={props.entry.date} />
|
||||
</Text>
|
||||
</OnDesktop>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
128
commafeed-client/src/components/content/FeedEntryContextMenu.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { createStyles, Group } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry } from "app/slices/entries"
|
||||
import { redirectToFeed } from "app/slices/redirect"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Entry } from "app/types"
|
||||
import { openLinkInBackgroundTab } from "app/utils"
|
||||
import { throttle, truncate } from "lodash"
|
||||
import { useEffect } from "react"
|
||||
import { Item, Menu, Separator, useContextMenu } from "react-contexify"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbRss, TbStar, TbStarOff } from "react-icons/tb"
|
||||
|
||||
interface FeedEntryContextMenuProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
const iconSize = 16
|
||||
const useStyles = createStyles(theme => ({
|
||||
menu: {
|
||||
// apply mantine theme from MenuItem.styles.ts
|
||||
fontSize: theme.fontSizes.sm,
|
||||
"--contexify-item-color": `${theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-color": `${theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black} !important`,
|
||||
"--contexify-activeItem-bgColor": `${
|
||||
theme.colorScheme === "dark" ? theme.fn.rgba(theme.colors.dark[3], 0.35) : theme.colors.gray[1]
|
||||
} !important`,
|
||||
},
|
||||
}))
|
||||
|
||||
const menuId = (entry: Entry) => entry.id
|
||||
|
||||
export function FeedEntryContextMenu(props: FeedEntryContextMenuProps) {
|
||||
const { classes, theme } = useStyles()
|
||||
const sourceType = useAppSelector(state => state.entries.source.type)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return (
|
||||
<Menu id={menuId(props.entry)} theme={theme.colorScheme} animation={false} className={classes.menu}>
|
||||
<Item
|
||||
onClick={() => {
|
||||
window.open(props.entry.url, "_blank", "noreferrer")
|
||||
dispatch(markEntry({ entry: props.entry, read: true }))
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<TbExternalLink size={iconSize} />
|
||||
<Trans>Open link in new tab</Trans>
|
||||
</Group>
|
||||
</Item>
|
||||
<Item
|
||||
onClick={() => {
|
||||
openLinkInBackgroundTab(props.entry.url)
|
||||
dispatch(markEntry({ entry: props.entry, read: true }))
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<TbExternalLink size={iconSize} />
|
||||
<Trans>Open link in new background tab</Trans>
|
||||
</Group>
|
||||
</Item>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Item onClick={() => dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}>
|
||||
<Group>
|
||||
{props.entry.starred ? <TbStarOff size={iconSize} /> : <TbStar size={iconSize} />}
|
||||
{props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
|
||||
</Group>
|
||||
</Item>
|
||||
<Item onClick={() => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))}>
|
||||
<Group>
|
||||
{props.entry.read ? <TbEyeOff size={iconSize} /> : <TbEyeCheck size={iconSize} />}
|
||||
{props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
||||
</Group>
|
||||
</Item>
|
||||
<Item onClick={() => dispatch(markEntriesUpToEntry(props.entry))}>
|
||||
<Group>
|
||||
<TbArrowBarToDown size={iconSize} />
|
||||
<Trans>Mark as read up to here</Trans>
|
||||
</Group>
|
||||
</Item>
|
||||
|
||||
{sourceType === "category" && (
|
||||
<>
|
||||
<Separator />
|
||||
|
||||
<Item
|
||||
onClick={() => {
|
||||
dispatch(redirectToFeed(props.entry.feedId))
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<TbRss size={iconSize} />
|
||||
<Trans>Go to {truncate(props.entry.feedName)}</Trans>
|
||||
</Group>
|
||||
</Item>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export function useFeedEntryContextMenu(entry: Entry) {
|
||||
const contextMenu = useContextMenu({
|
||||
id: menuId(entry),
|
||||
})
|
||||
|
||||
const onContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
contextMenu.show({
|
||||
event,
|
||||
})
|
||||
}
|
||||
|
||||
// close context menu on scroll
|
||||
useEffect(() => {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
|
||||
const listener = () => contextMenu.hideAll()
|
||||
const throttledListener = throttle(listener, 100)
|
||||
|
||||
scrollArea?.addEventListener("scroll", throttledListener)
|
||||
return () => scrollArea?.removeEventListener("scroll", throttledListener)
|
||||
}, [contextMenu])
|
||||
|
||||
return { onContextMenu }
|
||||
}
|
||||
108
commafeed-client/src/components/content/FeedEntryFooter.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { Group, Indicator, MultiSelect, Popover } from "@mantine/core"
|
||||
import { useMediaQuery } from "@mantine/hooks"
|
||||
import { Constants } from "app/constants"
|
||||
import { markEntriesUpToEntry, markEntry, starEntry, tagEntry } from "app/slices/entries"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Entry } from "app/types"
|
||||
import { ActionButton } from "components/ActionButtton"
|
||||
import { ButtonToolbar } from "components/ButtonToolbar"
|
||||
import { throttle } from "lodash"
|
||||
import { useEffect, useState } from "react"
|
||||
import { TbArrowBarToDown, TbExternalLink, TbEyeCheck, TbEyeOff, TbShare, TbStar, TbStarOff, TbTag } from "react-icons/tb"
|
||||
import { ShareButtons } from "./ShareButtons"
|
||||
|
||||
interface FeedEntryFooterProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
export function FeedEntryFooter(props: FeedEntryFooterProps) {
|
||||
const [scrollPosition, setScrollPosition] = useState(0)
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const mobile = !useMediaQuery(`(min-width: ${Constants.layout.mobileBreakpoint})`)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const showSharingButtons = sharingSettings && Object.values(sharingSettings).some(v => v)
|
||||
|
||||
const readStatusButtonClicked = () => dispatch(markEntry({ entry: props.entry, read: !props.entry.read }))
|
||||
const onTagsChange = (values: string[]) =>
|
||||
dispatch(
|
||||
tagEntry({
|
||||
entryId: +props.entry.id,
|
||||
tags: values,
|
||||
})
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const scrollArea = document.getElementById(Constants.dom.mainScrollAreaId)
|
||||
|
||||
const listener = () => setScrollPosition(scrollArea ? scrollArea.scrollTop : 0)
|
||||
const throttledListener = throttle(listener, 100)
|
||||
|
||||
scrollArea?.addEventListener("scroll", throttledListener)
|
||||
return () => scrollArea?.removeEventListener("scroll", throttledListener)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Group position="apart">
|
||||
<ButtonToolbar>
|
||||
{props.entry.markable && (
|
||||
<ActionButton
|
||||
icon={props.entry.read ? <TbEyeOff size={18} /> : <TbEyeCheck size={18} />}
|
||||
label={props.entry.read ? <Trans>Keep unread</Trans> : <Trans>Mark as read</Trans>}
|
||||
onClick={readStatusButtonClicked}
|
||||
/>
|
||||
)}
|
||||
<ActionButton
|
||||
icon={props.entry.starred ? <TbStarOff size={18} /> : <TbStar size={18} />}
|
||||
label={props.entry.starred ? <Trans>Unstar</Trans> : <Trans>Star</Trans>}
|
||||
onClick={() => dispatch(starEntry({ entry: props.entry, starred: !props.entry.starred }))}
|
||||
/>
|
||||
|
||||
{showSharingButtons && (
|
||||
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}>
|
||||
<Popover.Target>
|
||||
<ActionButton icon={<TbShare size={18} />} label={<Trans>Share</Trans>} />
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<ShareButtons url={props.entry.url} description={props.entry.title} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
{tags && (
|
||||
<Popover withArrow withinPortal shadow="md" positionDependencies={[scrollPosition]} closeOnClickOutside={!mobile}>
|
||||
<Popover.Target>
|
||||
<Indicator label={props.entry.tags.length} disabled={props.entry.tags.length === 0} inline size={16}>
|
||||
<ActionButton icon={<TbTag size={18} />} label={<Trans>Tags</Trans>} />
|
||||
</Indicator>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<MultiSelect
|
||||
data={tags}
|
||||
placeholder="Tags"
|
||||
searchable
|
||||
creatable
|
||||
autoFocus
|
||||
getCreateLabel={query => t`Create tag: ${query}`}
|
||||
value={props.entry.tags}
|
||||
onChange={onTagsChange}
|
||||
/>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<a href={props.entry.url} target="_blank" rel="noreferrer">
|
||||
<ActionButton icon={<TbExternalLink size={18} />} label={<Trans>Open link</Trans>} />
|
||||
</a>
|
||||
</ButtonToolbar>
|
||||
|
||||
<ActionButton
|
||||
icon={<TbArrowBarToDown size={18} />}
|
||||
label={<Trans>Mark as read up to here</Trans>}
|
||||
onClick={() => dispatch(markEntriesUpToEntry(props.entry))}
|
||||
/>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
60
commafeed-client/src/components/content/FeedEntryHeader.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Box, createStyles, Text } from "@mantine/core"
|
||||
import { Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
import { FeedFavicon } from "./FeedFavicon"
|
||||
|
||||
export interface FeedEntryHeaderProps {
|
||||
entry: Entry
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme, props: FeedEntryHeaderProps) => ({
|
||||
headerText: {
|
||||
fontWeight: theme.colorScheme === "light" && !props.entry.read ? "bold" : "inherit",
|
||||
whiteSpace: props.expanded ? "inherit" : "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
headerSubtext: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontSize: "90%",
|
||||
whiteSpace: props.expanded ? "inherit" : "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
}))
|
||||
export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
const { classes } = useStyles(props)
|
||||
return (
|
||||
<Box>
|
||||
<Box className={classes.headerText}>
|
||||
<FeedEntryTitle entry={props.entry} />
|
||||
</Box>
|
||||
<Box className={classes.headerSubtext}>
|
||||
<Box mr={6}>
|
||||
<FeedFavicon url={props.entry.iconUrl} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color="dimmed">{props.entry.feedName}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color="dimmed">
|
||||
<span> · </span>
|
||||
<RelativeDate date={props.entry.date} />
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{props.expanded && (
|
||||
<Box className={classes.headerSubtext}>
|
||||
<Text color="dimmed">
|
||||
{props.entry.author && <span>by {props.entry.author}</span>}
|
||||
{props.entry.author && props.entry.categories && <span> · </span>}
|
||||
{props.entry.categories && <span>{props.entry.categories}</span>}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
21
commafeed-client/src/components/content/FeedEntryTitle.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Highlight } from "@mantine/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { Entry } from "app/types"
|
||||
|
||||
export interface FeedEntryTitleProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
export function FeedEntryTitle(props: FeedEntryTitleProps) {
|
||||
const search = useAppSelector(state => state.entries.search)
|
||||
const keywords = search?.split(" ")
|
||||
return (
|
||||
<Highlight
|
||||
highlight={keywords ?? ""}
|
||||
// make sure ellipsis is shown when title is too long
|
||||
span
|
||||
>
|
||||
{props.entry.title}
|
||||
</Highlight>
|
||||
)
|
||||
}
|
||||
22
commafeed-client/src/components/content/FeedFavicon.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
|
||||
export interface FeedFaviconProps {
|
||||
url: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function FeedFavicon({ url, size = 18 }: FeedFaviconProps) {
|
||||
return (
|
||||
<ImageWithPlaceholderWhileLoading
|
||||
src={url}
|
||||
alt="feed favicon"
|
||||
width={size}
|
||||
height={size}
|
||||
placeholderWidth={size}
|
||||
placeholderHeight={size}
|
||||
placeholderBackgroundColor="inherit"
|
||||
placeholderIconSize={size}
|
||||
placeholderIconColor="inherit"
|
||||
/>
|
||||
)
|
||||
}
|
||||
39
commafeed-client/src/components/content/Media.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Box, TypographyStylesProvider } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { calculatePlaceholderSize } from "app/utils"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { Content } from "./Content"
|
||||
|
||||
export interface MediaProps {
|
||||
thumbnailUrl: string
|
||||
thumbnailWidth?: number
|
||||
thumbnailHeight?: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function Media(props: MediaProps) {
|
||||
const width = props.thumbnailWidth
|
||||
const height = props.thumbnailHeight
|
||||
const placeholderSize = calculatePlaceholderSize({
|
||||
width,
|
||||
height,
|
||||
maxWidth: Constants.layout.entryMaxWidth,
|
||||
})
|
||||
return (
|
||||
<TypographyStylesProvider>
|
||||
<ImageWithPlaceholderWhileLoading
|
||||
src={props.thumbnailUrl}
|
||||
alt="media thumbnail"
|
||||
width={props.thumbnailWidth}
|
||||
height={props.thumbnailHeight}
|
||||
placeholderWidth={placeholderSize.width}
|
||||
placeholderHeight={placeholderSize.height}
|
||||
/>
|
||||
{props.description && (
|
||||
<Box pt="md">
|
||||
<Content content={props.description} />
|
||||
</Box>
|
||||
)}
|
||||
</TypographyStylesProvider>
|
||||
)
|
||||
}
|
||||
55
commafeed-client/src/components/content/ShareButtons.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ActionIcon, Box, createStyles, SimpleGrid } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { SharingSettings } from "app/types"
|
||||
import { IconType } from "react-icons"
|
||||
|
||||
type Color = `#${string}`
|
||||
|
||||
const useStyles = createStyles((theme, props: { color: Color }) => ({
|
||||
socialIcon: {
|
||||
color: props.color,
|
||||
backgroundColor: theme.colorScheme === "dark" ? theme.colors.gray[2] : "white",
|
||||
borderRadius: "50%",
|
||||
},
|
||||
}))
|
||||
|
||||
function ShareButton({ url, icon, color }: { url: string; icon: IconType; color: Color }) {
|
||||
const { classes } = useStyles({ color })
|
||||
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
window.open(url, "", "menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=800,height=600")
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionIcon>
|
||||
<a href={url} target="_blank" rel="noreferrer" onClick={onClick}>
|
||||
<Box p={6} className={classes.socialIcon}>
|
||||
{icon({ size: 18 })}
|
||||
</Box>
|
||||
</a>
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
|
||||
export function ShareButtons(props: { url: string; description: string }) {
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const url = encodeURIComponent(props.url)
|
||||
const desc = encodeURIComponent(props.description)
|
||||
|
||||
return (
|
||||
<SimpleGrid cols={4}>
|
||||
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>)
|
||||
.filter(site => sharingSettings && sharingSettings[site])
|
||||
.map(site => (
|
||||
<ShareButton
|
||||
key={site}
|
||||
icon={Constants.sharing[site].icon}
|
||||
color={Constants.sharing[site].color}
|
||||
url={Constants.sharing[site].url(url, desc)}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)
|
||||
}
|
||||
50
commafeed-client/src/components/content/add/AddCategory.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { Box, Button, Group, Stack, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/slices/redirect"
|
||||
import { reloadTree } from "app/slices/tree"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { AddCategoryRequest } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbFolderPlus } from "react-icons/tb"
|
||||
import { CategorySelect } from "./CategorySelect"
|
||||
|
||||
export function AddCategory() {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const form = useForm<AddCategoryRequest>()
|
||||
|
||||
const addCategory = useAsyncCallback(client.category.add, {
|
||||
onSuccess: () => {
|
||||
dispatch(reloadTree())
|
||||
dispatch(redirectToSelectedSource())
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{addCategory.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(addCategory.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(addCategory.execute)}>
|
||||
<Stack>
|
||||
<TextInput label={<Trans>Category</Trans>} placeholder={t`Category`} {...form.getInputProps("name")} required />
|
||||
<CategorySelect label={<Trans>Parent</Trans>} {...form.getInputProps("parentId")} clearable />
|
||||
<Group position="center">
|
||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" leftIcon={<TbFolderPlus size={16} />} loading={addCategory.loading}>
|
||||
<Trans>Add</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { Select, SelectItem, SelectProps } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { flattenCategoryTree } from "app/utils"
|
||||
|
||||
type CategorySelectProps = Partial<SelectProps> & {
|
||||
withAll?: boolean
|
||||
withoutCategoryIds?: string[]
|
||||
}
|
||||
|
||||
export function CategorySelect(props: CategorySelectProps) {
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const categories = rootCategory && flattenCategoryTree(rootCategory)
|
||||
const selectData: SelectItem[] | undefined = categories
|
||||
?.filter(c => c.id !== Constants.categories.all.id)
|
||||
.filter(c => !props.withoutCategoryIds || !props.withoutCategoryIds.includes(c.id))
|
||||
.sort((c1, c2) => c1.name.localeCompare(c2.name))
|
||||
.map(c => ({
|
||||
label: c.parentName ? t`${c.name} (in ${c.parentName})` : c.name,
|
||||
value: c.id,
|
||||
}))
|
||||
if (props.withAll) {
|
||||
selectData?.unshift({
|
||||
label: t`All`,
|
||||
value: Constants.categories.all.id,
|
||||
})
|
||||
}
|
||||
|
||||
return <Select {...props} data={selectData ?? []} disabled={!selectData} />
|
||||
}
|
||||
63
commafeed-client/src/components/content/add/ImportOpml.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { Box, Button, FileInput, Group, Stack } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/slices/redirect"
|
||||
import { reloadTree } from "app/slices/tree"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbFileImport } from "react-icons/tb"
|
||||
|
||||
export function ImportOpml() {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const form = useForm<{ file: File }>({
|
||||
validate: {
|
||||
file: v => (v ? null : t`file is required`),
|
||||
},
|
||||
})
|
||||
|
||||
const importOpml = useAsyncCallback(client.feed.importOpml, {
|
||||
onSuccess: () => {
|
||||
dispatch(reloadTree())
|
||||
dispatch(redirectToSelectedSource())
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{importOpml.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(importOpml.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(v => importOpml.execute(v.file))}>
|
||||
<Stack>
|
||||
<FileInput
|
||||
label={<Trans>OPML file</Trans>}
|
||||
placeholder={t`OPML file`}
|
||||
description={
|
||||
<Trans>
|
||||
An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your
|
||||
data from other feed reading services.
|
||||
</Trans>
|
||||
}
|
||||
{...form.getInputProps("file")}
|
||||
required
|
||||
accept="application/xml"
|
||||
/>
|
||||
<Group position="center">
|
||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" leftIcon={<TbFileImport size={16} />} loading={importOpml.loading}>
|
||||
<Trans>Import</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
126
commafeed-client/src/components/content/add/Subscribe.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Button, Group, Stack, Stepper, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { Constants } from "app/constants"
|
||||
import { redirectToFeed, redirectToSelectedSource } from "app/slices/redirect"
|
||||
import { reloadTree } from "app/slices/tree"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { FeedInfoRequest, SubscribeRequest } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useState } from "react"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbRss } from "react-icons/tb"
|
||||
import { CategorySelect } from "./CategorySelect"
|
||||
|
||||
export function Subscribe() {
|
||||
const [activeStep, setActiveStep] = useState(0)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const step0Form = useForm<FeedInfoRequest>({
|
||||
initialValues: {
|
||||
url: "",
|
||||
},
|
||||
})
|
||||
|
||||
const step1Form = useForm<SubscribeRequest>({
|
||||
initialValues: {
|
||||
url: "",
|
||||
title: "",
|
||||
categoryId: Constants.categories.all.id,
|
||||
},
|
||||
})
|
||||
|
||||
const fetchFeed = useAsyncCallback(client.feed.fetchFeed, {
|
||||
onSuccess: ({ data }) => {
|
||||
step1Form.setFieldValue("url", data.url)
|
||||
step1Form.setFieldValue("title", data.title)
|
||||
setActiveStep(step => step + 1)
|
||||
},
|
||||
})
|
||||
const subscribe = useAsyncCallback(client.feed.subscribe, {
|
||||
onSuccess: sub => {
|
||||
dispatch(reloadTree())
|
||||
dispatch(redirectToFeed(sub.data))
|
||||
},
|
||||
})
|
||||
|
||||
const previousStep = () => {
|
||||
if (activeStep === 0) dispatch(redirectToSelectedSource())
|
||||
else setActiveStep(activeStep - 1)
|
||||
}
|
||||
const nextStep = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
if (activeStep === 0) {
|
||||
step0Form.onSubmit(fetchFeed.execute)(e)
|
||||
} else if (activeStep === 1) {
|
||||
step1Form.onSubmit(subscribe.execute)(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{fetchFeed.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(fetchFeed.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{subscribe.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(subscribe.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={nextStep}>
|
||||
<Stepper active={activeStep} onStepClick={setActiveStep}>
|
||||
<Stepper.Step
|
||||
label={<Trans>Analyze feed</Trans>}
|
||||
description={<Trans>Check that the feed is working</Trans>}
|
||||
allowStepSelect={activeStep === 1}
|
||||
>
|
||||
<TextInput
|
||||
label={<Trans>Feed URL</Trans>}
|
||||
placeholder="http://www.mysite.com/rss"
|
||||
description={
|
||||
<Trans>
|
||||
The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed
|
||||
will try to find the feed in the page.
|
||||
</Trans>
|
||||
}
|
||||
required
|
||||
autoFocus
|
||||
{...step0Form.getInputProps("url")}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
<Stepper.Step
|
||||
label={<Trans>Subscribe</Trans>}
|
||||
description={<Trans>Subscribe to the feed</Trans>}
|
||||
allowStepSelect={false}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput label={<Trans>Feed URL</Trans>} {...step1Form.getInputProps("url")} disabled />
|
||||
<TextInput label={<Trans>Feed name</Trans>} {...step1Form.getInputProps("title")} required autoFocus />
|
||||
<CategorySelect label={<Trans>Category</Trans>} {...step1Form.getInputProps("categoryId")} clearable />
|
||||
</Stack>
|
||||
</Stepper.Step>
|
||||
</Stepper>
|
||||
|
||||
<Group position="center" mt="xl">
|
||||
<Button variant="default" onClick={previousStep}>
|
||||
<Trans>Back</Trans>
|
||||
</Button>
|
||||
{activeStep === 0 && (
|
||||
<Button type="submit" loading={fetchFeed.loading}>
|
||||
<Trans>Next</Trans>
|
||||
</Button>
|
||||
)}
|
||||
{activeStep === 1 && (
|
||||
<Button type="submit" leftIcon={<TbRss size={16} />} loading={fetchFeed.loading || subscribe.loading}>
|
||||
<Trans>Subscribe</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
93
commafeed-client/src/components/header/Header.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { ActionIcon, Center, Divider, Indicator, Popover, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { reloadEntries, search } from "app/slices/entries"
|
||||
import { changeReadingMode, changeReadingOrder } from "app/slices/user"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { ActionButton } from "components/ActionButtton"
|
||||
import { ButtonToolbar } from "components/ButtonToolbar"
|
||||
import { Loader } from "components/Loader"
|
||||
import { useEffect } from "react"
|
||||
import { TbArrowDown, TbArrowUp, TbEye, TbEyeOff, TbRefresh, TbSearch, TbUser, TbX } from "react-icons/tb"
|
||||
import { MarkAllAsReadButton } from "./MarkAllAsReadButton"
|
||||
import { ProfileMenu } from "./ProfileMenu"
|
||||
|
||||
function HeaderDivider() {
|
||||
return <Divider orientation="vertical" />
|
||||
}
|
||||
|
||||
const iconSize = 18
|
||||
|
||||
export function Header() {
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const searchFromStore = useAppSelector(state => state.entries.search)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const searchForm = useForm<{ search: string }>({
|
||||
validate: {
|
||||
search: value => (value.length > 0 && value.length < 3 ? t`Search requires at least 3 characters` : null),
|
||||
},
|
||||
})
|
||||
const { setValues } = searchForm
|
||||
|
||||
useEffect(() => {
|
||||
setValues({
|
||||
search: searchFromStore,
|
||||
})
|
||||
}, [setValues, searchFromStore])
|
||||
|
||||
if (!settings) return <Loader />
|
||||
return (
|
||||
<Center>
|
||||
<ButtonToolbar>
|
||||
<ActionButton
|
||||
icon={<TbRefresh size={iconSize} />}
|
||||
label={<Trans>Refresh</Trans>}
|
||||
onClick={() => dispatch(reloadEntries())}
|
||||
/>
|
||||
<MarkAllAsReadButton iconSize={iconSize} />
|
||||
|
||||
<HeaderDivider />
|
||||
|
||||
<ActionButton
|
||||
icon={settings.readingMode === "all" ? <TbEye size={iconSize} /> : <TbEyeOff size={iconSize} />}
|
||||
label={settings.readingMode === "all" ? <Trans>All</Trans> : <Trans>Unread</Trans>}
|
||||
onClick={() => dispatch(changeReadingMode(settings.readingMode === "all" ? "unread" : "all"))}
|
||||
/>
|
||||
<ActionButton
|
||||
icon={settings.readingOrder === "asc" ? <TbArrowUp size={iconSize} /> : <TbArrowDown size={iconSize} />}
|
||||
label={settings.readingOrder === "asc" ? <Trans>Asc</Trans> : <Trans>Desc</Trans>}
|
||||
onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<Popover.Target>
|
||||
<Indicator disabled={!searchFromStore}>
|
||||
<ActionButton icon={<TbSearch size={iconSize} />} label={<Trans>Search</Trans>} />
|
||||
</Indicator>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<form onSubmit={searchForm.onSubmit(values => dispatch(search(values.search)))}>
|
||||
<TextInput
|
||||
placeholder={t`Search`}
|
||||
{...searchForm.getInputProps("search")}
|
||||
icon={<TbSearch size={iconSize} />}
|
||||
rightSection={
|
||||
<ActionIcon onClick={() => searchFromStore && dispatch(search(""))}>
|
||||
<TbX />
|
||||
</ActionIcon>
|
||||
}
|
||||
autoFocus
|
||||
/>
|
||||
</form>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
<HeaderDivider />
|
||||
|
||||
<ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} />
|
||||
</ButtonToolbar>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
|
||||
import { Button, Code, Group, Modal, Slider, Stack, Text } from "@mantine/core"
|
||||
import { markAllEntries } from "app/slices/entries"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { ActionButton } from "components/ActionButtton"
|
||||
import { useState } from "react"
|
||||
import { TbChecks } from "react-icons/tb"
|
||||
|
||||
export function MarkAllAsReadButton(props: { iconSize: number }) {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const [threshold, setThreshold] = useState(0)
|
||||
const source = useAppSelector(state => state.entries.source)
|
||||
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
|
||||
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={() => setOpened(false)} title={<Trans>Mark all entries as read</Trans>}>
|
||||
<Stack>
|
||||
<Text size="sm">
|
||||
{threshold === 0 && (
|
||||
<Trans>
|
||||
Are you sure you want to mark all entries of <Code>{sourceLabel}</Code> as read?
|
||||
</Trans>
|
||||
)}
|
||||
{threshold > 0 && (
|
||||
<Trans>
|
||||
Are you sure you want to mark entries older than {threshold} days of <Code>{sourceLabel}</Code> as read?
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Slider
|
||||
py="xl"
|
||||
min={0}
|
||||
max={28}
|
||||
marks={[
|
||||
{ value: 0, label: "0" },
|
||||
{ value: 7, label: "7" },
|
||||
{ value: 14, label: "14" },
|
||||
{ value: 21, label: "21" },
|
||||
{ value: 28, label: "28" },
|
||||
]}
|
||||
value={threshold}
|
||||
onChange={setThreshold}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button variant="default" onClick={() => setOpened(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setOpened(false)
|
||||
dispatch(
|
||||
markAllEntries({
|
||||
sourceType: source.type,
|
||||
req: {
|
||||
id: source.id,
|
||||
read: true,
|
||||
olderThan: entriesTimestamp - threshold * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
})
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Trans>Confirm</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<ActionButton
|
||||
icon={<TbChecks size={props.iconSize} />}
|
||||
label={<Trans>Mark all as read</Trans>}
|
||||
onClick={() => {
|
||||
setThreshold(0)
|
||||
setOpened(true)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
189
commafeed-client/src/components/header/ProfileMenu.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Divider, Group, Menu, SegmentedControl, SegmentedControlItem, useMantineColorScheme } from "@mantine/core"
|
||||
import { showNotification } from "@mantine/notifications"
|
||||
import { client } from "app/client"
|
||||
import { redirectToAbout, redirectToAdminUsers, redirectToMetrics, redirectToSettings } from "app/slices/redirect"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { ViewMode } from "app/types"
|
||||
import { useViewMode } from "hooks/useViewMode"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
TbChartLine,
|
||||
TbHelp,
|
||||
TbLayoutList,
|
||||
TbList,
|
||||
TbListDetails,
|
||||
TbMoon,
|
||||
TbNotes,
|
||||
TbPower,
|
||||
TbSettings,
|
||||
TbSun,
|
||||
TbUsers,
|
||||
TbWorldDownload,
|
||||
} from "react-icons/tb"
|
||||
|
||||
interface ProfileMenuProps {
|
||||
control: React.ReactElement
|
||||
}
|
||||
|
||||
interface ViewModeControlItem extends SegmentedControlItem {
|
||||
value: ViewMode
|
||||
}
|
||||
|
||||
const iconSize = 16
|
||||
|
||||
const viewModeData: ViewModeControlItem[] = [
|
||||
{
|
||||
value: "title",
|
||||
label: (
|
||||
<Group>
|
||||
<TbList size={iconSize} />
|
||||
<Box ml={6}>
|
||||
<Trans>Compact</Trans>
|
||||
</Box>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "cozy",
|
||||
label: (
|
||||
<Group>
|
||||
<TbLayoutList size={iconSize} />
|
||||
<Box ml={6}>
|
||||
<Trans>Cozy</Trans>
|
||||
</Box>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "detailed",
|
||||
label: (
|
||||
<Group>
|
||||
<TbListDetails size={iconSize} />
|
||||
<Box ml={6}>
|
||||
<Trans>Detailed</Trans>
|
||||
</Box>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "expanded",
|
||||
label: (
|
||||
<Group>
|
||||
<TbNotes size={iconSize} />
|
||||
<Box ml={6}>
|
||||
<Trans>Expanded</Trans>
|
||||
</Box>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export function ProfileMenu(props: ProfileMenuProps) {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const { viewMode, setViewMode } = useViewMode()
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const admin = useAppSelector(state => state.user.profile?.admin)
|
||||
const dispatch = useAppDispatch()
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme()
|
||||
const dark = colorScheme === "dark"
|
||||
|
||||
const logout = () => {
|
||||
window.location.href = "logout"
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu position="bottom-end" closeOnItemClick={false} opened={opened} onChange={setOpened}>
|
||||
<Menu.Target>{props.control}</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{profile && <Menu.Label>{profile.name}</Menu.Label>}
|
||||
<Menu.Item
|
||||
icon={<TbSettings size={iconSize} />}
|
||||
onClick={() => {
|
||||
dispatch(redirectToSettings())
|
||||
setOpened(false)
|
||||
}}
|
||||
>
|
||||
<Trans>Settings</Trans>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<TbWorldDownload size={iconSize} />}
|
||||
onClick={() =>
|
||||
client.feed.refreshAll().then(() => {
|
||||
showNotification({
|
||||
message: <Trans>Your feeds have been queued for refresh.</Trans>,
|
||||
color: "green",
|
||||
autoClose: 1000,
|
||||
})
|
||||
setOpened(false)
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trans>Fetch all my feeds now</Trans>
|
||||
</Menu.Item>
|
||||
|
||||
<Divider />
|
||||
<Menu.Label>
|
||||
<Trans>Theme</Trans>
|
||||
</Menu.Label>
|
||||
<Menu.Item icon={dark ? <TbSun size={iconSize} /> : <TbMoon size={iconSize} />} onClick={() => toggleColorScheme()}>
|
||||
{dark ? <Trans>Switch to light theme</Trans> : <Trans>Switch to dark theme</Trans>}
|
||||
</Menu.Item>
|
||||
|
||||
<Divider />
|
||||
<Menu.Label>
|
||||
<Trans>Display</Trans>
|
||||
</Menu.Label>
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
orientation="vertical"
|
||||
data={viewModeData}
|
||||
value={viewMode}
|
||||
onChange={e => setViewMode(e as ViewMode)}
|
||||
mb="xs"
|
||||
/>
|
||||
|
||||
{admin && (
|
||||
<>
|
||||
<Divider />
|
||||
<Menu.Label>
|
||||
<Trans>Admin</Trans>
|
||||
</Menu.Label>
|
||||
<Menu.Item
|
||||
icon={<TbUsers size={iconSize} />}
|
||||
onClick={() => {
|
||||
dispatch(redirectToAdminUsers())
|
||||
setOpened(false)
|
||||
}}
|
||||
>
|
||||
<Trans>Manage users</Trans>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<TbChartLine size={iconSize} />}
|
||||
onClick={() => {
|
||||
dispatch(redirectToMetrics())
|
||||
setOpened(false)
|
||||
}}
|
||||
>
|
||||
<Trans>Metrics</Trans>
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
<Menu.Item
|
||||
icon={<TbHelp size={iconSize} />}
|
||||
onClick={() => {
|
||||
dispatch(redirectToAbout())
|
||||
setOpened(false)
|
||||
}}
|
||||
>
|
||||
<Trans>About</Trans>
|
||||
</Menu.Item>
|
||||
<Menu.Item icon={<TbPower size={iconSize} />} onClick={logout}>
|
||||
<Trans>Logout</Trans>
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
9
commafeed-client/src/components/metrics/Gauge.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { MetricGauge } from "app/types"
|
||||
|
||||
interface MeterProps {
|
||||
gauge: MetricGauge
|
||||
}
|
||||
|
||||
export function Gauge(props: MeterProps) {
|
||||
return <span>{props.gauge.value}</span>
|
||||
}
|
||||
19
commafeed-client/src/components/metrics/Meter.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { MetricMeter } from "app/types"
|
||||
|
||||
interface MeterProps {
|
||||
meter: MetricMeter
|
||||
}
|
||||
|
||||
export function Meter(props: MeterProps) {
|
||||
return (
|
||||
<Box>
|
||||
<Box>Mean: {props.meter.mean_rate.toFixed(2)}</Box>
|
||||
<Box>Last minute: {props.meter.m1_rate.toFixed(2)}</Box>
|
||||
<Box>Last 5 minutes: {props.meter.m5_rate.toFixed(2)}</Box>
|
||||
<Box>Last 15 minutes: {props.meter.m15_rate.toFixed(2)}</Box>
|
||||
<Box>Units: {props.meter.units}</Box>
|
||||
<Box>Total: {props.meter.count}</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Accordion, Box, Group } from "@mantine/core"
|
||||
|
||||
interface MetricAccordionItemProps {
|
||||
metricKey: string
|
||||
name: string
|
||||
headerValue: number
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function MetricAccordionItem({ metricKey, name, headerValue, children }: MetricAccordionItemProps) {
|
||||
return (
|
||||
<Accordion.Item value={metricKey} key={metricKey}>
|
||||
<Accordion.Control>
|
||||
<Group position="apart">
|
||||
<Box>{name}</Box>
|
||||
<Box>{headerValue}</Box>
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>{children}</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
)
|
||||
}
|
||||
19
commafeed-client/src/components/metrics/Timer.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Box } from "@mantine/core"
|
||||
import { MetricTimer } from "app/types"
|
||||
|
||||
interface MetricTimerProps {
|
||||
timer: MetricTimer
|
||||
}
|
||||
|
||||
export function Timer(props: MetricTimerProps) {
|
||||
return (
|
||||
<Box>
|
||||
<Box>Mean: {props.timer.mean_rate.toFixed(2)}</Box>
|
||||
<Box>Last minute: {props.timer.m1_rate.toFixed(2)}</Box>
|
||||
<Box>Last 5 minutes: {props.timer.m5_rate.toFixed(2)}</Box>
|
||||
<Box>Last 15 minutes: {props.timer.m15_rate.toFixed(2)}</Box>
|
||||
<Box>Units: {props.timer.rate_units}</Box>
|
||||
<Box>Total: {props.timer.count}</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
11
commafeed-client/src/components/responsive/OnDesktop.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box, MediaQuery } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import React from "react"
|
||||
|
||||
export function OnDesktop(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<MediaQuery smallerThan={Constants.layout.mobileBreakpoint} styles={{ display: "none" }}>
|
||||
<Box>{props.children}</Box>
|
||||
</MediaQuery>
|
||||
)
|
||||
}
|
||||
11
commafeed-client/src/components/responsive/OnMobile.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Box, MediaQuery } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import React from "react"
|
||||
|
||||
export function OnMobile(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<MediaQuery largerThan={Constants.layout.mobileBreakpoint} styles={{ display: "none" }}>
|
||||
<Box>{props.children}</Box>
|
||||
</MediaQuery>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Button, Group, Stack, Textarea } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToSelectedSource } from "app/slices/redirect"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useEffect } from "react"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy } from "react-icons/tb"
|
||||
|
||||
interface FormData {
|
||||
customCss: string
|
||||
customJs: string
|
||||
}
|
||||
|
||||
export function CustomCodeSettings() {
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const form = useForm<FormData>()
|
||||
const { setValues } = form
|
||||
|
||||
const saveCustomCode = useAsyncCallback(
|
||||
async (d: FormData) => {
|
||||
if (!settings) return
|
||||
await client.user.saveSettings({
|
||||
...settings,
|
||||
customCss: d.customCss,
|
||||
customJs: d.customJs,
|
||||
})
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
window.location.reload()
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!settings) return
|
||||
setValues({
|
||||
customCss: settings.customCss,
|
||||
customJs: settings.customJs,
|
||||
})
|
||||
}, [setValues, settings])
|
||||
|
||||
return (
|
||||
<>
|
||||
{saveCustomCode.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(saveCustomCode.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(saveCustomCode.execute)}>
|
||||
<Stack>
|
||||
<Textarea
|
||||
autosize
|
||||
minRows={4}
|
||||
maxRows={15}
|
||||
{...form.getInputProps("customCss")}
|
||||
description={<Trans>Custom CSS rules that will be applied</Trans>}
|
||||
styles={{
|
||||
input: {
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
autosize
|
||||
minRows={4}
|
||||
maxRows={15}
|
||||
{...form.getInputProps("customJs")}
|
||||
description={<Trans>Custom JS code that will be executed on page load</Trans>}
|
||||
styles={{
|
||||
input: {
|
||||
fontFamily: "monospace",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveCustomCode.loading}>
|
||||
<Trans>Save</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
61
commafeed-client/src/components/settings/DisplaySettings.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Divider, Select, SimpleGrid, Stack, Switch } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { changeLanguage, changeScrollMarks, changeScrollSpeed, changeSharingSetting, changeShowRead } from "app/slices/user"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { SharingSettings } from "app/types"
|
||||
import { locales } from "i18n"
|
||||
|
||||
export function DisplaySettings() {
|
||||
const language = useAppSelector(state => state.user.settings?.language)
|
||||
const scrollSpeed = useAppSelector(state => state.user.settings?.scrollSpeed)
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks)
|
||||
const sharingSettings = useAppSelector(state => state.user.settings?.sharingSettings)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Select
|
||||
description={<Trans>Language</Trans>}
|
||||
value={language}
|
||||
data={locales.map(l => ({
|
||||
value: l.key,
|
||||
label: l.label,
|
||||
}))}
|
||||
onChange={s => s && dispatch(changeLanguage(s))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Scroll smoothly when navigating between entries</Trans>}
|
||||
checked={scrollSpeed ? scrollSpeed > 0 : false}
|
||||
onChange={e => dispatch(changeScrollSpeed(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>Show feeds and categories with no unread entries</Trans>}
|
||||
checked={showRead}
|
||||
onChange={e => dispatch(changeShowRead(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label={<Trans>In expanded view, scrolling through entries mark them as read</Trans>}
|
||||
checked={scrollMarks}
|
||||
onChange={e => dispatch(changeScrollMarks(e.currentTarget.checked))}
|
||||
/>
|
||||
|
||||
<Divider label={<Trans>Sharing sites</Trans>} labelPosition="center" />
|
||||
|
||||
<SimpleGrid cols={2}>
|
||||
{(Object.keys(Constants.sharing) as Array<keyof SharingSettings>).map(site => (
|
||||
<Switch
|
||||
key={site}
|
||||
label={Constants.sharing[site].label}
|
||||
checked={sharingSettings && sharingSettings[site]}
|
||||
onChange={e => dispatch(changeSharingSetting({ site, value: e.currentTarget.checked }))}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
134
commafeed-client/src/components/settings/ProfileSettings.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { Anchor, Box, Button, Checkbox, Divider, Group, Input, PasswordInput, Stack, Text, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { openConfirmModal } from "@mantine/modals"
|
||||
import { client, errorToStrings } from "app/client"
|
||||
import { redirectToLogin, redirectToSelectedSource } from "app/slices/redirect"
|
||||
import { reloadProfile } from "app/slices/user"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { ProfileModificationRequest } from "app/types"
|
||||
import { Alert } from "components/Alert"
|
||||
import { useEffect } from "react"
|
||||
import { useAsyncCallback } from "react-async-hook"
|
||||
import { TbDeviceFloppy, TbTrash } from "react-icons/tb"
|
||||
|
||||
interface FormData extends ProfileModificationRequest {
|
||||
newPasswordConfirmation?: string
|
||||
}
|
||||
|
||||
export function ProfileSettings() {
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const form = useForm<FormData>({
|
||||
validate: {
|
||||
newPasswordConfirmation: (value, values) => (value !== values.newPassword ? t`Passwords do not match` : null),
|
||||
},
|
||||
})
|
||||
const { setValues } = form
|
||||
|
||||
const saveProfile = useAsyncCallback(client.user.saveProfile, {
|
||||
onSuccess: () => {
|
||||
dispatch(reloadProfile())
|
||||
dispatch(redirectToSelectedSource())
|
||||
},
|
||||
})
|
||||
const deleteProfile = useAsyncCallback(client.user.deleteProfile, {
|
||||
onSuccess: () => {
|
||||
dispatch(redirectToLogin())
|
||||
},
|
||||
})
|
||||
|
||||
const openDeleteProfileModal = () =>
|
||||
openConfirmModal({
|
||||
title: <Trans>Delete account</Trans>,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
<Trans>Are you sure you want to delete your account? There's no turning back!</Trans>
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: <Trans>Confirm</Trans>, cancel: <Trans>Cancel</Trans> },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => deleteProfile.execute(),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!profile) return
|
||||
setValues({
|
||||
currentPassword: "",
|
||||
email: profile.email ?? "",
|
||||
newApiKey: false,
|
||||
})
|
||||
}, [setValues, profile])
|
||||
|
||||
return (
|
||||
<>
|
||||
{saveProfile.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(saveProfile.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{deleteProfile.error && (
|
||||
<Box mb="md">
|
||||
<Alert messages={errorToStrings(deleteProfile.error)} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(saveProfile.execute)}>
|
||||
<Stack>
|
||||
<Input.Wrapper label={<Trans>User name</Trans>}>
|
||||
<Box>{profile?.name}</Box>
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label={<Trans>OPML export</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
Export your subscriptions and categories as an OPML file that can be imported in other feed reading services
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Anchor href="rest/feed/export" download="commafeed_opml.xml">
|
||||
<Trans>Download</Trans>
|
||||
</Anchor>
|
||||
</Box>
|
||||
</Input.Wrapper>
|
||||
<PasswordInput
|
||||
label={<Trans>Current password</Trans>}
|
||||
description={<Trans>Enter your current password to change profile settings</Trans>}
|
||||
required
|
||||
{...form.getInputProps("currentPassword")}
|
||||
/>
|
||||
<TextInput type="email" label={<Trans>E-mail</Trans>} {...form.getInputProps("email")} required />
|
||||
<PasswordInput
|
||||
label={<Trans>New password</Trans>}
|
||||
description={<Trans>Changing password will generate a new API key</Trans>}
|
||||
{...form.getInputProps("newPassword")}
|
||||
/>
|
||||
<PasswordInput label={<Trans>Confirm password</Trans>} {...form.getInputProps("newPasswordConfirmation")} />
|
||||
<TextInput label={<Trans>API key</Trans>} readOnly value={profile?.apiKey} />
|
||||
<Checkbox label={<Trans>Generate new API key</Trans>} {...form.getInputProps("newApiKey", { type: "checkbox" })} />
|
||||
|
||||
<Group>
|
||||
<Button variant="default" onClick={() => dispatch(redirectToSelectedSource())}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button type="submit" leftIcon={<TbDeviceFloppy size={16} />} loading={saveProfile.loading}>
|
||||
<Trans>Save</Trans>
|
||||
</Button>
|
||||
<Divider orientation="vertical" />
|
||||
<Button
|
||||
color="red"
|
||||
leftIcon={<TbTrash size={16} />}
|
||||
onClick={() => openDeleteProfileModal()}
|
||||
loading={deleteProfile.loading}
|
||||
>
|
||||
<Trans>Delete account</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
169
commafeed-client/src/components/sidebar/Tree.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Trans } from "@lingui/macro"
|
||||
import { Box, Stack } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import {
|
||||
redirectToCategory,
|
||||
redirectToCategoryDetails,
|
||||
redirectToFeed,
|
||||
redirectToFeedDetails,
|
||||
redirectToTag,
|
||||
redirectToTagDetails,
|
||||
} from "app/slices/redirect"
|
||||
import { collapseTreeCategory } from "app/slices/tree"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { Category, Subscription } from "app/types"
|
||||
import { categoryUnreadCount, flattenCategoryTree } from "app/utils"
|
||||
import { Loader } from "components/Loader"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import React from "react"
|
||||
import { TbChevronDown, TbChevronRight, TbInbox, TbStar, TbTag } from "react-icons/tb"
|
||||
import { TreeNode } from "./TreeNode"
|
||||
import { TreeSearch } from "./TreeSearch"
|
||||
|
||||
const allIcon = <TbInbox size={16} />
|
||||
const starredIcon = <TbStar size={16} />
|
||||
const tagIcon = <TbTag size={16} />
|
||||
const expandedIcon = <TbChevronDown size={16} />
|
||||
const collapsedIcon = <TbChevronRight size={16} />
|
||||
|
||||
const errorThreshold = 9
|
||||
|
||||
export function Tree() {
|
||||
const root = useAppSelector(state => state.tree.rootCategory)
|
||||
const source = useAppSelector(state => state.entries.source)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
const showRead = useAppSelector(state => state.user.settings?.showRead)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const feedClicked = (e: React.MouseEvent, id: string) => {
|
||||
if (e.detail === 2) dispatch(redirectToFeedDetails(id))
|
||||
else dispatch(redirectToFeed(id))
|
||||
}
|
||||
const categoryClicked = (e: React.MouseEvent, id: string) => {
|
||||
if (e.detail === 2) {
|
||||
dispatch(redirectToCategoryDetails(id))
|
||||
} else {
|
||||
dispatch(redirectToCategory(id))
|
||||
}
|
||||
}
|
||||
const categoryIconClicked = (e: React.MouseEvent, category: Category) => {
|
||||
e.stopPropagation()
|
||||
|
||||
dispatch(
|
||||
collapseTreeCategory({
|
||||
id: +category.id,
|
||||
collapse: category.expanded,
|
||||
})
|
||||
)
|
||||
}
|
||||
const tagClicked = (e: React.MouseEvent, id: string) => {
|
||||
if (e.detail === 2) dispatch(redirectToTagDetails(id))
|
||||
else dispatch(redirectToTag(id))
|
||||
}
|
||||
|
||||
const allCategoryNode = () => (
|
||||
<TreeNode
|
||||
id={Constants.categories.all.id}
|
||||
name={<Trans>All</Trans>}
|
||||
icon={allIcon}
|
||||
unread={categoryUnreadCount(root)}
|
||||
selected={source.type === "category" && source.id === Constants.categories.all.id}
|
||||
expanded={false}
|
||||
level={0}
|
||||
hasError={false}
|
||||
onClick={categoryClicked}
|
||||
/>
|
||||
)
|
||||
const starredCategoryNode = () => (
|
||||
<TreeNode
|
||||
id={Constants.categories.starred.id}
|
||||
name={<Trans>Starred</Trans>}
|
||||
icon={starredIcon}
|
||||
unread={0}
|
||||
selected={source.type === "category" && source.id === Constants.categories.starred.id}
|
||||
expanded={false}
|
||||
level={0}
|
||||
hasError={false}
|
||||
onClick={categoryClicked}
|
||||
/>
|
||||
)
|
||||
|
||||
const categoryNode = (category: Category, level = 0) => {
|
||||
const unreadCount = categoryUnreadCount(category)
|
||||
if (unreadCount === 0 && !showRead) return null
|
||||
|
||||
const hasError = !category.expanded && flattenCategoryTree(category).some(c => c.feeds.some(f => f.errorCount > errorThreshold))
|
||||
return (
|
||||
<TreeNode
|
||||
id={category.id}
|
||||
name={category.name}
|
||||
icon={category.expanded ? expandedIcon : collapsedIcon}
|
||||
unread={unreadCount}
|
||||
selected={source.type === "category" && source.id === category.id}
|
||||
expanded={category.expanded}
|
||||
level={level}
|
||||
hasError={hasError}
|
||||
onClick={categoryClicked}
|
||||
onIconClick={e => categoryIconClicked(e, category)}
|
||||
key={category.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const feedNode = (feed: Subscription, level = 0) => {
|
||||
if (feed.unread === 0 && !showRead) return null
|
||||
|
||||
return (
|
||||
<TreeNode
|
||||
id={String(feed.id)}
|
||||
name={feed.name}
|
||||
icon={feed.iconUrl}
|
||||
unread={feed.unread}
|
||||
selected={source.type === "feed" && source.id === String(feed.id)}
|
||||
level={level}
|
||||
hasError={feed.errorCount > errorThreshold}
|
||||
onClick={feedClicked}
|
||||
key={feed.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const tagNode = (tag: string) => (
|
||||
<TreeNode
|
||||
id={tag}
|
||||
name={tag}
|
||||
icon={tagIcon}
|
||||
unread={0}
|
||||
selected={source.type === "tag" && source.id === tag}
|
||||
level={0}
|
||||
hasError={false}
|
||||
onClick={tagClicked}
|
||||
key={tag}
|
||||
/>
|
||||
)
|
||||
|
||||
const recursiveCategoryNode = (category: Category, level = 0) => (
|
||||
<React.Fragment key={`recursiveCategoryNode-${category.id}`}>
|
||||
{categoryNode(category, level)}
|
||||
{category.expanded && category.children.map(c => recursiveCategoryNode(c, level + 1))}
|
||||
{category.expanded && category.feeds.map(f => feedNode(f, level + 1))}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
if (!root) return <Loader />
|
||||
const feeds = flattenCategoryTree(root).flatMap(c => c.feeds)
|
||||
return (
|
||||
<Stack>
|
||||
<OnDesktop>
|
||||
<TreeSearch feeds={feeds} />
|
||||
</OnDesktop>
|
||||
<Box>
|
||||
{allCategoryNode()}
|
||||
{starredCategoryNode()}
|
||||
{root.children.map(c => recursiveCategoryNode(c))}
|
||||
{root.feeds.map(f => feedNode(f))}
|
||||
{tags?.map(tag => tagNode(tag))}
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
63
commafeed-client/src/components/sidebar/TreeNode.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Box, createStyles } from "@mantine/core"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import React, { ReactNode } from "react"
|
||||
import { UnreadCount } from "./UnreadCount"
|
||||
|
||||
interface TreeNodeProps {
|
||||
id: string
|
||||
name: ReactNode
|
||||
icon: ReactNode
|
||||
unread: number
|
||||
selected: boolean
|
||||
expanded?: boolean
|
||||
level: number
|
||||
hasError: boolean
|
||||
onClick: (e: React.MouseEvent, id: string) => void
|
||||
onIconClick?: (e: React.MouseEvent, id: string) => void
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme, props: TreeNodeProps) => {
|
||||
let backgroundColor = "inherit"
|
||||
if (props.selected) backgroundColor = theme.colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[3]
|
||||
|
||||
let color
|
||||
if (props.hasError) color = theme.colors.red[6]
|
||||
else if (theme.colorScheme === "dark") color = props.unread > 0 ? theme.colors.dark[0] : theme.colors.dark[3]
|
||||
else color = props.unread > 0 ? theme.black : theme.colors.gray[6]
|
||||
|
||||
return {
|
||||
node: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
color,
|
||||
backgroundColor,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[0],
|
||||
},
|
||||
},
|
||||
nodeText: {
|
||||
flexGrow: 1,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export function TreeNode(props: TreeNodeProps) {
|
||||
const { classes } = useStyles(props)
|
||||
return (
|
||||
<Box py={1} pl={props.level * 20} className={classes.node} onClick={(e: React.MouseEvent) => props.onClick(e, props.id)}>
|
||||
<Box mr={6} onClick={(e: React.MouseEvent) => props.onIconClick && props.onIconClick(e, props.id)}>
|
||||
{typeof props.icon === "string" ? <FeedFavicon url={props.icon} /> : props.icon}
|
||||
</Box>
|
||||
<Box className={classes.nodeText}>{props.name}</Box>
|
||||
{!props.expanded && (
|
||||
<Box>
|
||||
<UnreadCount unreadCount={props.unread} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
62
commafeed-client/src/components/sidebar/TreeSearch.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { t, Trans } from "@lingui/macro"
|
||||
import { Box, Center, Kbd, TextInput } from "@mantine/core"
|
||||
import { openSpotlight, SpotlightAction, SpotlightProvider } from "@mantine/spotlight"
|
||||
import { redirectToFeed } from "app/slices/redirect"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { Subscription } from "app/types"
|
||||
import { FeedFavicon } from "components/content/FeedFavicon"
|
||||
import { useMousetrap } from "hooks/useMousetrap"
|
||||
import { TbSearch } from "react-icons/tb"
|
||||
|
||||
export interface TreeSearchProps {
|
||||
feeds: Subscription[]
|
||||
}
|
||||
|
||||
export function TreeSearch(props: TreeSearchProps) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const actions: SpotlightAction[] = props.feeds
|
||||
.sort((f1, f2) => f1.name.localeCompare(f2.name))
|
||||
.map(f => ({
|
||||
title: f.name,
|
||||
icon: <FeedFavicon url={f.iconUrl} />,
|
||||
onTrigger: () => dispatch(redirectToFeed(f.id)),
|
||||
}))
|
||||
|
||||
const searchIcon = <TbSearch size={18} />
|
||||
const rightSection = (
|
||||
<Center>
|
||||
<Kbd>Ctrl</Kbd>
|
||||
<Box mx={5}>+</Box>
|
||||
<Kbd>K</Kbd>
|
||||
</Center>
|
||||
)
|
||||
|
||||
// additional keyboard shortcut used by commafeed v1
|
||||
useMousetrap("g u", () => openSpotlight())
|
||||
|
||||
return (
|
||||
<SpotlightProvider
|
||||
actions={actions}
|
||||
searchIcon={searchIcon}
|
||||
searchPlaceholder={t`Search`}
|
||||
shortcut="ctrl+k"
|
||||
nothingFoundMessage={<Trans>Nothing found</Trans>}
|
||||
>
|
||||
<TextInput
|
||||
placeholder={t`Search`}
|
||||
icon={searchIcon}
|
||||
rightSectionWidth={100}
|
||||
rightSection={rightSection}
|
||||
styles={{
|
||||
input: { cursor: "pointer" },
|
||||
rightSection: { pointerEvents: "none" },
|
||||
}}
|
||||
onClick={() => openSpotlight()}
|
||||
// prevent focus
|
||||
onFocus={e => e.target.blur()}
|
||||
readOnly
|
||||
/>
|
||||
</SpotlightProvider>
|
||||
)
|
||||
}
|
||||
18
commafeed-client/src/components/sidebar/UnreadCount.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Badge, createStyles } from "@mantine/core"
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
badge: {
|
||||
width: "3.2rem",
|
||||
// for some reason, mantine Badge has "cursor: 'default'"
|
||||
cursor: "pointer",
|
||||
},
|
||||
}))
|
||||
|
||||
export function UnreadCount(props: { unreadCount: number }) {
|
||||
const { classes } = useStyles()
|
||||
|
||||
if (props.unreadCount <= 0) return null
|
||||
|
||||
const count = props.unreadCount >= 1000 ? "999+" : props.unreadCount
|
||||
return <Badge className={classes.badge}>{count}</Badge>
|
||||
}
|
||||
39
commafeed-client/src/hooks/useAppLoading.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { useAppSelector } from "app/store"
|
||||
|
||||
interface Step {
|
||||
label: string
|
||||
done: boolean
|
||||
}
|
||||
|
||||
export const useAppLoading = () => {
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const rootCategory = useAppSelector(state => state.tree.rootCategory)
|
||||
const tags = useAppSelector(state => state.user.tags)
|
||||
|
||||
const steps: Step[] = [
|
||||
{
|
||||
label: t`Loading settings...`,
|
||||
done: !!settings,
|
||||
},
|
||||
{
|
||||
label: t`Loading profile...`,
|
||||
done: !!profile,
|
||||
},
|
||||
{
|
||||
label: t`Loading subscriptions...`,
|
||||
done: !!rootCategory,
|
||||
},
|
||||
{
|
||||
label: t`Loading tags...`,
|
||||
done: !!tags,
|
||||
},
|
||||
]
|
||||
|
||||
const loading = steps.some(s => !s.done)
|
||||
const loadingPercentage = Math.round((100.0 * steps.filter(s => s.done).length) / steps.length)
|
||||
const loadingStepLabel = steps.find(s => !s.done)?.label
|
||||
|
||||
return { steps, loading, loadingPercentage, loadingStepLabel }
|
||||
}
|
||||
22
commafeed-client/src/hooks/useMousetrap.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import mousetrap, { ExtendedKeyboardEvent } from "mousetrap"
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
type Callback = (e: ExtendedKeyboardEvent, combo: string) => void
|
||||
|
||||
export const useMousetrap = (key: string | string[], callback: Callback) => {
|
||||
// use a ref to avoid unbinding/rebinding every time the callback changes
|
||||
const callbackRef = useRef(callback)
|
||||
callbackRef.current = callback
|
||||
|
||||
useEffect(() => {
|
||||
mousetrap.bind(key, (event, combo) => {
|
||||
callbackRef.current(event, combo)
|
||||
|
||||
// prevent default behavior
|
||||
return false
|
||||
})
|
||||
return () => {
|
||||
mousetrap.unbind(key)
|
||||
}
|
||||
}, [key])
|
||||
}
|
||||
7
commafeed-client/src/hooks/useViewMode.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ViewMode } from "app/types"
|
||||
import useLocalStorage from "use-local-storage"
|
||||
|
||||
export function useViewMode() {
|
||||
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("view-mode", "detailed")
|
||||
return { viewMode, setViewMode }
|
||||
}
|
||||
24
commafeed-client/src/hooks/useWebSocket.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { reloadTree } from "app/slices/tree"
|
||||
import { useAppDispatch } from "app/store"
|
||||
import { useEffect } from "react"
|
||||
import WebsocketHeartbeatJs from "websocket-heartbeat-js"
|
||||
|
||||
export const useWebSocket = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
const currentUrl = new URL(window.location.href)
|
||||
const wsProtocol = currentUrl.protocol === "http:" ? "ws" : "wss"
|
||||
const wsUrl = `${wsProtocol}://${currentUrl.hostname}:${currentUrl.port}/ws`
|
||||
|
||||
const ws = new WebsocketHeartbeatJs({ url: wsUrl, pingMsg: "ping" })
|
||||
ws.onmessage = event => {
|
||||
const { data } = event
|
||||
if (typeof data === "string") {
|
||||
if (data.startsWith("new-feed-entries:")) dispatch(reloadTree())
|
||||
}
|
||||
}
|
||||
|
||||
return () => ws.close()
|
||||
}, [dispatch])
|
||||
}
|
||||
118
commafeed-client/src/i18n.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { i18n, Messages } from "@lingui/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import dayjs from "dayjs"
|
||||
import "dayjs/locale/ar"
|
||||
import "dayjs/locale/ca"
|
||||
import "dayjs/locale/cs"
|
||||
import "dayjs/locale/cy"
|
||||
import "dayjs/locale/da"
|
||||
import "dayjs/locale/de"
|
||||
import "dayjs/locale/en"
|
||||
import "dayjs/locale/es"
|
||||
import "dayjs/locale/fa"
|
||||
import "dayjs/locale/fi"
|
||||
import "dayjs/locale/fr"
|
||||
import "dayjs/locale/gl"
|
||||
import "dayjs/locale/hu"
|
||||
import "dayjs/locale/id"
|
||||
import "dayjs/locale/it"
|
||||
import "dayjs/locale/ja"
|
||||
import "dayjs/locale/ko"
|
||||
import "dayjs/locale/ms"
|
||||
import "dayjs/locale/nb"
|
||||
import "dayjs/locale/nl"
|
||||
import "dayjs/locale/nn"
|
||||
import "dayjs/locale/pl"
|
||||
import "dayjs/locale/pt"
|
||||
import "dayjs/locale/ru"
|
||||
import "dayjs/locale/sk"
|
||||
import "dayjs/locale/sv"
|
||||
import "dayjs/locale/tr"
|
||||
import "dayjs/locale/zh"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { messages as arMessages } from "./locales/ar/messages"
|
||||
import { messages as caMessages } from "./locales/ca/messages"
|
||||
import { messages as csMessages } from "./locales/cs/messages"
|
||||
import { messages as cyMessages } from "./locales/cy/messages"
|
||||
import { messages as daMessages } from "./locales/da/messages"
|
||||
import { messages as deMessages } from "./locales/de/messages"
|
||||
import { messages as enMessages } from "./locales/en/messages"
|
||||
import { messages as esMessages } from "./locales/es/messages"
|
||||
import { messages as faMessages } from "./locales/fa/messages"
|
||||
import { messages as fiMessages } from "./locales/fi/messages"
|
||||
import { messages as frMessages } from "./locales/fr/messages"
|
||||
import { messages as glMessages } from "./locales/gl/messages"
|
||||
import { messages as huMessages } from "./locales/hu/messages"
|
||||
import { messages as idMessages } from "./locales/id/messages"
|
||||
import { messages as itMessages } from "./locales/it/messages"
|
||||
import { messages as jaMessages } from "./locales/ja/messages"
|
||||
import { messages as koMessages } from "./locales/ko/messages"
|
||||
import { messages as msMessages } from "./locales/ms/messages"
|
||||
import { messages as nbMessages } from "./locales/nb/messages"
|
||||
import { messages as nlMessages } from "./locales/nl/messages"
|
||||
import { messages as nnMessages } from "./locales/nn/messages"
|
||||
import { messages as plMessages } from "./locales/pl/messages"
|
||||
import { messages as ptMessages } from "./locales/pt/messages"
|
||||
import { messages as ruMessages } from "./locales/ru/messages"
|
||||
import { messages as skMessages } from "./locales/sk/messages"
|
||||
import { messages as svMessages } from "./locales/sv/messages"
|
||||
import { messages as trMessages } from "./locales/tr/messages"
|
||||
import { messages as zhMessages } from "./locales/zh/messages"
|
||||
|
||||
interface Locale {
|
||||
key: string
|
||||
label: string
|
||||
messages: Messages
|
||||
}
|
||||
|
||||
// add an object to the array to add a new locale
|
||||
// don't forget to also add it to the 'locales' array in .linguirc
|
||||
export const locales: Locale[] = [
|
||||
{ key: "ar", messages: arMessages, label: "العربية" },
|
||||
{ key: "ca", messages: caMessages, label: "Català" },
|
||||
{ key: "cs", messages: csMessages, label: "Čeština" },
|
||||
{ key: "cy", messages: cyMessages, label: "Cymraeg" },
|
||||
{ key: "da", messages: daMessages, label: "Danish" },
|
||||
{ key: "de", messages: deMessages, label: "Deutsch" },
|
||||
{ key: "en", messages: enMessages, label: "English" },
|
||||
{ key: "es", messages: esMessages, label: "Español" },
|
||||
{ key: "fa", messages: faMessages, label: "فارسی" },
|
||||
{ key: "fi", messages: fiMessages, label: "Suomi" },
|
||||
{ key: "fr", messages: frMessages, label: "Français" },
|
||||
{ key: "gl", messages: glMessages, label: "Galician" },
|
||||
{ key: "hu", messages: huMessages, label: "Magyar" },
|
||||
{ key: "id", messages: idMessages, label: "Indonesian" },
|
||||
{ key: "it", messages: itMessages, label: "Italiano" },
|
||||
{ key: "ja", messages: jaMessages, label: "日本語" },
|
||||
{ key: "ko", messages: koMessages, label: "한국어" },
|
||||
{ key: "ms", messages: msMessages, label: "Bahasa Malaysian" },
|
||||
{ key: "nb", messages: nbMessages, label: "Norsk (bokmål)" },
|
||||
{ key: "nl", messages: nlMessages, label: "Nederlands" },
|
||||
{ key: "nn", messages: nnMessages, label: "Norsk (nynorsk)" },
|
||||
{ key: "pl", messages: plMessages, label: "Polski" },
|
||||
{ key: "pt", messages: ptMessages, label: "Português" },
|
||||
{ key: "ru", messages: ruMessages, label: "Русский" },
|
||||
{ key: "sk", messages: skMessages, label: "Slovenčina" },
|
||||
{ key: "sv", messages: svMessages, label: "Svenska" },
|
||||
{ key: "tr", messages: trMessages, label: "Türkçe" },
|
||||
{ key: "zh", messages: zhMessages, label: "简体中文" },
|
||||
]
|
||||
|
||||
locales.forEach(l => {
|
||||
i18n.load({
|
||||
[l.key]: l.messages,
|
||||
})
|
||||
})
|
||||
|
||||
function activateLocale(locale: string) {
|
||||
i18n.activate(locale)
|
||||
dayjs.locale(locale)
|
||||
}
|
||||
|
||||
export const useI18n = () => {
|
||||
const locale = useAppSelector(state => state.user.settings?.language)
|
||||
useEffect(() => {
|
||||
activateLocale(locale ?? "en")
|
||||
}, [locale])
|
||||
}
|
||||
829
commafeed-client/src/locales/ar/messages.po
Normal file
@@ -0,0 +1,829 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-10-28 13:47+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: ar\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0> هل لديك حساب؟ </0> <1> تسجيل الدخول! </ 1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0> هل تحتاج إلى حساب؟ </0> <1> اشترك! </ 1>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "مفتاح API"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "حول"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
msgstr "الإجراءات"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "إضافة"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
msgstr "إضافة فئة"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Add user"
|
||||
msgstr "إضافة مستخدم"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr "إداري"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "الكل"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "تم إرسال بريد إلكتروني إذا تم تسجيل هذا العنوان. "
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "ملف opml هو ملف XML يحتوي على عناوين URL للتغذية والفئات. "
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "تحليل التغذية"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "هل أنت متأكد أنك تريد حذف الفئة <0> {categoryName} </0>؟"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "هل أنت متأكد أنك تريد حذف المستخدم <0> {userName} </0>؟"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "هل أنت متأكد أنك تريد حذف حسابك؟ "
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "هل أنت متأكد أنك تريد تعليم كافة إدخالات <0> {sourceLabel} </0> كمقروءة؟"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "هل أنت متأكد أنك تريد وضع علامة على الإدخالات الأقدم من {عتبة} يوم من <0> {sourceLabel} </0> كمقروءة؟"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "هل أنت متأكد أنك تريد إلغاء الاشتراك من <0> {feedName} </0>؟"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr "تصاعدي"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "المتغيرات المتاحة هي \"العنوان\" و \"المحتوى\" و \"url\" و \"المؤلف\" و \"الفئات\" ويتم تحويل محتواها إلى أحرف صغيرة لتسهيل مقارنة السلسلة."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "العودة"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "العودة لتسجيل الدخول"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "ملحقات المستعرض"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "إلغاء"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Category"
|
||||
msgstr "الفئة"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "سيؤدي تغيير كلمة المرور إلى إنشاء مفتاح API جديد"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "تأكد من عمل الخلاصة"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
msgstr "CommaFeed هو مشروع مفتوح المصدر. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed التالي العنصر غير المقروء"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "إصدار CommaFeed {الإصدار} ({مراجعة})"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "مضغوط"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "تأكيد"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "تأكيد كلمة المرور"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "دافئ"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Create tag: {query}"
|
||||
msgstr "إنشاء علامة: {استعلام}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr "السيطرة"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
msgstr "كلمة المرور الحالية"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
msgstr "تاريخ الإنشاء"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete"
|
||||
msgstr "حذف"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "حذف الفئة"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Delete account"
|
||||
msgstr "حذف الحساب"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
msgstr "حذف المستخدم"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr "تنازلي"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "عرض"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr "تنزيل"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "اسحب الرابط إلى شريط الإشارات"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "البريد الإلكتروني"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "عنوان البريد الإلكتروني"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "تحرير المستخدم"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "ممكن"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Enter"
|
||||
msgstr "دخول"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "أدخل كلمة المرور الحالية لتغيير إعدادات ملف التعريف"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "خطأ"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "مثال: {مثال}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "موسع"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "قم بتصدير اشتراكاتك وفئاتك كملف OPML يمكن استيراده في خدمات قراءة الأعلاف الأخرى"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "موجز URL"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "اسم الخلاصة"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "تصفية التعبير"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "هل نسيت كلمة المرور؟"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "قم بإنشاء مفتاح API في ملف التعريف الخاص بك أولاً."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "إنشاء مفتاح API جديد"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "رابط الخلاصة المولدة"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "انتقل إلى وثائق API."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
msgstr "اذهب إلى طريقة العرض \"الكل\""
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "الأشياء الجيدة"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr "المرجع نفسه"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "إذا لم يكن فارغًا ، فسيتم تقييم التعبير إلى \"صواب\" أو \"خطأ\". "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "إذا واجهت مشكلة ، فالرجاء الإبلاغ عنها على صفحة مشكلات مشروع GitHub."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
msgstr "إذا أعجبك هذا المشروع ، فيرجى التفكير في التبرع لدعم المطور والمساعدة في تغطية تكاليف إبقاء هذا الموقع على الإنترنت."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
msgstr "استيراد"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "في العرض الموسع ، التمرير عبر الإدخالات وضع علامة عليها كمقروءة"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "إبقاء غير مقروءة"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "اختصارات لوحة المفاتيح"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
msgstr "لغة"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "تاريخ آخر تسجيل دخول"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
msgstr "التحديث الأخير"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh message"
|
||||
msgstr "آخر رسالة تحديث"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "رابط"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "تحميل ملف التعريف ..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading settings..."
|
||||
msgstr "تحميل الإعدادات ..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading subscriptions..."
|
||||
msgstr "تحميل الاشتراكات ..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading tags..."
|
||||
msgstr "تحميل العلامات ..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "تسجيل الدخول"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "تسجيل الخروج"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "إدارة المستخدمين"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all as read"
|
||||
msgstr "تعليم الكل كمقروء"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "تعليم كافة الإدخالات كمقروءة"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "وضع علامة كمقروء"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "وضع علامة كمقروءة حتى هنا"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr "المقاييس"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
msgstr "تحريك الصفحة لأسفل"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page up"
|
||||
msgstr "تحريك الصفحة لأعلى"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr "لا"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Name"
|
||||
msgstr "الاسم"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "انتقل إلى اشتراك بإدخال اسمه"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "كلمة مرور جديدة"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "الأحدث أولاً"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "التالي"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Next refresh"
|
||||
msgstr "التحديث التالي"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "اختصار العنصر غير المقروء التالي"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "لا مزيد من الإدخالات"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "لم يتم العثور على شيء"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "تصدير OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "ملف OPML"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "الأقدم أولا"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "اوووه!"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "فتح الإدخال الحالي في علامة تبويب جديدة"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "فتح الإدخال الحالي في علامة تبويب جديدة في الخلفية"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "افتح الرابط"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "فتح الإدخال التالي"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "فتح الإدخال السابق"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "فتح / إغلاق الإدخال الحالي"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "طلب"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "والد"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "الفئة الأصل"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "كلمة المرور"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
msgstr "استعادة كلمة المرور"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "كلمات المرور غير متطابقة"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "المنـصب"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "الملف الشخصي"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Recover password"
|
||||
msgstr "استعادة كلمة السر"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "تحديث"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "تم إغلاق التسجيلات في مثيل CommaFeed هذا"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Save"
|
||||
msgstr "حفظ"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "قم بالتمرير بسلاسة عند التنقل بين الإدخالات"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Search"
|
||||
msgstr "بحث"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "يتطلب البحث 3 أحرف على الأقل"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "ضع التركيز على الإدخال التالي دون فتحه"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "ضع التركيز على الإدخال السابق دون فتحه"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "إعدادات"
|
||||
|
||||
#: src/app/slices/user.ts
|
||||
msgid "Settings saved."
|
||||
msgstr "تم حفظ الإعدادات."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Share"
|
||||
msgstr "مشاركة"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "مشاركة المواقع"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "الحلقة"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "إظهار موجز ويب والفئات التي لا تحتوي على إدخالات غير مقروءة"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "إظهار تعليمات اختصار لوحة المفاتيح"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "قم بالتسجيل"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "شيء سيء حدث للتو ..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "فضاء"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Star"
|
||||
msgstr "النجم"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "مميز بنجمة"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "اشتراك"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
msgstr "عنوان URL للاشتراك"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "الاشتراك في موجز ويب"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Success"
|
||||
msgstr "النجاح"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "التبديل إلى النسق الداكن"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "قم بالتبديل إلى النسق الفاتح"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
msgstr "الكلمات"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "عنوان URL للتغذية التي تريد الاشتراك فيها. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
msgstr "الموضوع"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "تبديل قراءة حالة الإدخال الحالي"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "جرب CommaFeed باستخدام الحساب التجريبي: تجريبي / تجريبي"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "غير مقروءة"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "إلغاء النجم"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Unsubscribe"
|
||||
msgstr "إلغاء الاشتراك"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "User Name or E-mail"
|
||||
msgstr "اسم المستخدم أو البريد الإلكتروني"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "User name"
|
||||
msgstr "اسم المستخدم"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
msgstr "تحذير"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Website"
|
||||
msgstr "موقع الكتروني"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "ليس لديك أي اشتراكات حتى الآن. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "الملف مطلوب"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
829
commafeed-client/src/locales/ca/messages.po
Normal file
@@ -0,0 +1,829 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-10-28 13:47+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: ca\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Teniu un compte?</0><1>Inicieu la sessió!</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Necessites un compte?</0><1>Registreu-vos!</1>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "clau API"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Accions"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "Afegir"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
msgstr "Afegeix categoria"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Add user"
|
||||
msgstr "Afegeix usuari"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Administrador"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "Tot"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "S'ha enviat un correu electrònic si aquesta adreça estava registrada. "
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "Un fitxer opml és un fitxer XML que conté URL i categories de canals. "
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analitzar el feed"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "Estàs segur que vols suprimir la categoria <0>{categoryName}</0>?"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "Esteu segur que voleu suprimir l'usuari <0>{userName}</0>?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "Esteu segur que voleu suprimir el vostre compte? "
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Esteu segur que voleu marcar totes les entrades de <0>{sourceLabel}</0> com a llegides?"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Esteu segur que voleu marcar les entrades més antigues de {threshold} dies de <0>{sourceLabel}</0> com a llegides?"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "Estàs segur que vols cancel·lar la subscripció a <0>{feedName}</0>?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Les variables disponibles són \"títol\", \"contingut\", \"url\" \"autor\" i \"categories\" i el seu contingut es converteix en minúscules per facilitar la comparació de cadenes."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Enrere"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "Tornar a iniciar sessió"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Extensions del navegador"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel·la"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Category"
|
||||
msgstr "Categoria"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "Canviar la contrasenya generarà una nova clau d'API"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Comproveu que el canal funciona"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
msgstr "CommaFeed és un projecte de codi obert. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed següent element no llegit"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "Versió CommaFeed {versió} ({revisió})"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "Compacte"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Confirmar"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Confirmeu la contrasenya"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "Acollidor"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Create tag: {query}"
|
||||
msgstr "Crea una etiqueta: {consulta}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
msgstr "Contrasenya actual"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
msgstr "Data de creació"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete"
|
||||
msgstr "Eliminar"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "Suprimeix la categoria"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Delete account"
|
||||
msgstr "Suprimeix el compte"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
msgstr "Suprimeix l'usuari"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "Mostra"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr "Descarrega"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Arrossegueu l'enllaç a la barra d'adreces d'interès"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "Correu electrònic"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "Adreça de correu electrònic"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "Edita l'usuari"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "activat"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Enter"
|
||||
msgstr "Entra"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "introduïu la vostra contrasenya actual per canviar la configuració del perfil"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Exemple: {exemple}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Ampliat"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "exporteu les vostres subscripcions i categories com a fitxer OPML que es pot importar a altres serveis de lectura de feeds"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "URL del canal"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "Nom del canal"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Expressió de filtratge"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Heu oblidat la contrasenya?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "primer genereu una clau API al vostre perfil."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "Genera una nova clau d'API"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "URL del feed generat"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "Vés a la documentació de l'API."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
msgstr "Vés a la vista Tot"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "Bones"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Si no està buida, una expressió que s'avalua com a \"vertader\" o \"fals\". "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Si trobeu un problema, informeu-lo a la pàgina de problemes del projecte GitHub."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
msgstr "Si t'agrada aquest projecte, considera una donació per donar suport al desenvolupador i ajudar a cobrir els costos de mantenir aquest lloc web en línia."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
msgstr "Importació"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "a la vista ampliada, desplaçant-se per les entrades les marqueu com a llegides"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Mantenir sense llegir"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Dreceres de teclat"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
msgstr "Llengua"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "Data de l'últim inici de sessió"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
msgstr "Última actualització"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh message"
|
||||
msgstr "últim missatge d'actualització"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Enllaç"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Carregant el perfil..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading settings..."
|
||||
msgstr "S'està carregant la configuració..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading subscriptions..."
|
||||
msgstr "S'estan carregant les subscripcions..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading tags..."
|
||||
msgstr "Carregant les etiquetes..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Inicia sessió"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "Tanca sessió"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Gestionar usuaris"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all as read"
|
||||
msgstr "Marca-ho tot com a llegit"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Marqueu totes les entrades com a llegides"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Marca com a llegit"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Marca com a llegit fins aquí"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr "mètriques"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
msgstr "Mou la pàgina cap avall"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page up"
|
||||
msgstr "Mou la pàgina cap amunt"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navegueu a una subscripció introduint-ne el nom"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Contrasenya nova"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "El més nou primer"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Següent"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Next refresh"
|
||||
msgstr "Propera actualització"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "Següent marcador d'elements no llegit"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "No hi ha més entrades"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "No s'ha trobat res"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "Exportació OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "Fitxer OPML"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "el més vell primer"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Vaja!"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "Obre l'entrada actual en una pestanya nova"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Obre l'entrada actual en una pestanya nova al fons"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Enllaç obert"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "Obre la següent entrada"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "Obre l'entrada anterior"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "Obrir/tancar l'entrada actual"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Ordre"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "pares"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "Categoria pare"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Contrasenya"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
msgstr "Recuperació de contrasenya"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Les contrasenyes no coincideixen"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Posició"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr "API REST"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Recover password"
|
||||
msgstr "Recuperar la contrasenya"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Actualitzar"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "Els registres estan tancats en aquesta instància de CommaFeed"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Save"
|
||||
msgstr "Desa"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Desplaceu-vos suaument quan navegueu entre entrades"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Search"
|
||||
msgstr "Cerca"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "la cerca requereix almenys 3 caràcters"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "posa el focus a la següent entrada sense obrir-la"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "Estableix el focus en l'entrada anterior sense obrir-la"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "Configuració"
|
||||
|
||||
#: src/app/slices/user.ts
|
||||
msgid "Settings saved."
|
||||
msgstr "Configuració desada."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Share"
|
||||
msgstr "Comparteix"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "Compartir llocs"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "canvi"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Mostra feeds i categories sense entrades no llegides"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Mostra l'ajuda de la drecera del teclat"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Inscriu-te"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "Acaba de passar una cosa dolenta..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "Espai"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Star"
|
||||
msgstr "Estrella"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "Estrellat"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Subscriu-te"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
msgstr "URL de subscripció"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "Subscriu-te al canal"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Success"
|
||||
msgstr "Éxit"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "Canvia al tema fosc"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "Canvia al tema clar"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetes"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "l'URL del canal al qual us voleu subscriure. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Canvia l'estat de lectura de l'entrada actual"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Proveu CommaFeed amb el compte de demostració: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "Sense llegir"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "Desestrellar"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Unsubscribe"
|
||||
msgstr "Donar-se de baixa"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "User Name or E-mail"
|
||||
msgstr "Nom d'usuari o correu electrònic"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "User name"
|
||||
msgstr "Nom d'usuari"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
msgstr "Avís"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Website"
|
||||
msgstr "Lloc web"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "Encara no teniu cap subscripció. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "el fitxer és necessari"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
829
commafeed-client/src/locales/cs/messages.po
Normal file
@@ -0,0 +1,829 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-10-28 13:47+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: cs\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Máte účet?</0><1>Přihlaste se!</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Potřebujete účet?</0><1>Zaregistrujte se!</1>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "Klíč API"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "Asi"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Akce"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "Přidej"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
msgstr "Přidat kategorii"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Add user"
|
||||
msgstr "Přidat uživatele"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Správce"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "Všechny"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Pokud byla tato adresa zaregistrována, byl odeslán e-mail. "
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "Soubor opml je soubor XML obsahující adresy URL a kategorie zdrojů. "
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyzujte krmivo"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "Opravdu chcete smazat kategorii <0>{categoryName}</0>?"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "Opravdu chcete smazat uživatele <0>{userName}</0>?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "Opravdu chcete smazat svůj účet? "
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Opravdu chcete označit všechny položky <0>{sourceLabel}</0> jako přečtené?"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Opravdu chcete označit záznamy starší než {threshold} dnů <0>{sourceLabel}</0> jako přečtené?"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "Opravdu se chcete odhlásit z odběru <0>{feedName}</0>?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Dostupné proměnné jsou 'název', 'obsah', 'url', 'autor' a 'kategorie' a jejich obsah je převeden na malá písmena, aby se usnadnilo porovnávání řetězců."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Zpět"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "Zpět k přihlášení"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Rozšíření prohlížeče"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Zrušit"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Category"
|
||||
msgstr "Kategorie"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "Změna hesla vygeneruje nový klíč API"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Zkontrolujte, zda zdroj funguje"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
msgstr "CommaFeed je projekt s otevřeným zdrojovým kódem. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed další nepřečtená položka"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed verze {version} ({revision})"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "Kompaktní"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Potvrdit"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Potvrďte heslo"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "Útulný"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Create tag: {query}"
|
||||
msgstr "Vytvořit značku: {query}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
msgstr "Aktuální heslo"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
msgstr "Datum vytvoření"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete"
|
||||
msgstr "Smazat"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "Odstranit kategorii"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Delete account"
|
||||
msgstr "Smazat účet"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
msgstr "Smazat uživatele"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "Displej"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr "Stáhnout"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Přetáhněte odkaz na lištu záložek"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "E-mail"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "E-mailová adresa"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "Upravit uživatele"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Povoleno"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Enter"
|
||||
msgstr "Vstupte"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Zadejte své aktuální heslo pro změnu nastavení profilu"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Chyba"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Příklad: {příklad}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Rozbaleno"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Exportujte svá předplatná a kategorie jako soubor OPML, který lze importovat do jiných služeb čtení kanálů"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "URL zdroje"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "Název zdroje"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrování výrazu"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Zapomněli jste heslo?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Nejprve ve svém profilu vygenerujte klíč API."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "Vygenerujte nový klíč API"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "Generovaná adresa URL zdroje"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "Přejděte na dokumentaci API."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
msgstr "Přejděte do zobrazení Vše"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "Dobroty"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Pokud není prázdný, výraz vyhodnocený jako 'true' nebo 'false'. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Pokud narazíte na problém, nahlaste jej prosím na stránce problémů projektu GitHub."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
msgstr "Pokud se vám tento projekt líbí, zvažte prosím dar na podporu vývojáře a pomozte pokrýt náklady na udržování tohoto webu online."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "V rozšířeném zobrazení je procházením označíte jako přečtené"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Ponechat nepřečtené"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Klávesové zkratky"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
msgstr "Jazyk"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "Datum posledního přihlášení"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
msgstr "Poslední aktualizace"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh message"
|
||||
msgstr "Poslední obnovovací zpráva"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Odkaz"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Načítání profilu..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading settings..."
|
||||
msgstr "Načítání nastavení..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading subscriptions..."
|
||||
msgstr "Načítání odběrů..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading tags..."
|
||||
msgstr "Načítání značek..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Přihlaste se"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "Odhlášení"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Spravujte uživatele"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all as read"
|
||||
msgstr "Označit vše jako přečtené"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Označte všechny položky jako přečtené"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Označit jako přečtené"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Označit jako přečtené až sem"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr "Metriky"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
msgstr "Přesuňte stránku dolů"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page up"
|
||||
msgstr "Přesuňte stránku nahoru"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Name"
|
||||
msgstr "Jméno"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Přejděte na předplatné zadáním jeho názvu"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nové heslo"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "Nejnovější jako první"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Další"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Next refresh"
|
||||
msgstr "Další obnovení"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "Další nepřečtená položka bookmarklet"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "Žádné další záznamy"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Nic nebylo nalezeno"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "Export OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "soubor OPML"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "Nejdříve nejstarší"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Jejda!"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "Otevřete aktuální položku na nové kartě"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Otevřít aktuální položku na nové kartě na pozadí"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Otevřít odkaz"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "Otevřete další položku"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "Otevřít předchozí záznam"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "Otevřít/zavřít aktuální položku"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Objednávka"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "Rodič"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "Rodičovská kategorie"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Heslo"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
msgstr "Obnovení hesla"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Hesla se neshodují"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Pozice"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Recover password"
|
||||
msgstr "Obnovte heslo"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Obnovit"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "V této instanci CommaFeed jsou registrace uzavřeny"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Save"
|
||||
msgstr "Uložit"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Posouvejte plynule při navigaci mezi položkami"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Search"
|
||||
msgstr "Hledej"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Hledání vyžaduje alespoň 3 znaky"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Zaměřte se na další položku, aniž byste ji otevřeli"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "Nastavit fokus na předchozí záznam bez jeho otevření"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "Nastavení"
|
||||
|
||||
#: src/app/slices/user.ts
|
||||
msgid "Settings saved."
|
||||
msgstr "Nastavení uloženo."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Share"
|
||||
msgstr "Sdílejte"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "Stránky pro sdílení"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Směna"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Zobrazit kanály a kategorie bez nepřečtených položek"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Zobrazit nápovědu ke klávesovým zkratkám"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Zaregistrujte se"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "Právě se stalo něco špatného..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "Vesmír"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Star"
|
||||
msgstr "Hvězda"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "S hvězdičkou"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Přihlaste se"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
msgstr "URL pro přihlášení"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "Přihlaste se k odběru kanálu"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Success"
|
||||
msgstr "Úspěch"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "Přepněte na tmavý motiv"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "Přepněte na světlé téma"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
msgstr "Značky"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "Adresa URL kanálu, k jehož odběru se chcete přihlásit. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
msgstr "Téma"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Přepne stav čtení aktuálního záznamu"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Vyzkoušejte CommaFeed s demo účtem: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "Nepřečteno"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "Odstranit hvězdu"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Unsubscribe"
|
||||
msgstr "Odhlásit odběr"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "User Name or E-mail"
|
||||
msgstr "Uživatelské jméno nebo e-mail"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "User name"
|
||||
msgstr "Uživatelské jméno"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
msgstr "Varování"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Website"
|
||||
msgstr "Webové stránky"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "Zatím nemáte žádné předplatné. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
829
commafeed-client/src/locales/cy/messages.po
Normal file
@@ -0,0 +1,829 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-10-28 13:47+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: cy\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>A oes gennych gyfrif?</0><1>Mewngofnodi!</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Angen cyfrif?</0><1>Ymunwch!</1>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "Allwedd API"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "Ynghylch"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Camau gweithredu"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "Ychwanegu"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
msgstr "Ychwanegu categori"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Add user"
|
||||
msgstr "Ychwanegu defnyddiwr"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Gweinyddol"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "Pawb"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Mae e-bost wedi'i anfon os oedd y cyfeiriad hwn wedi'i gofrestru. "
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "Mae ffeil opml yn ffeil XML sy'n cynnwys URLs porthiant a chategorïau. "
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "Dadansoddi porthiant"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "Ydych chi'n siŵr eich bod am ddileu categori <0>{categoryName}</0>?"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "Ydych chi'n siŵr eich bod am ddileu defnyddiwr <0>{userName}</0> ?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "Ydych chi'n siŵr eich bod am ddileu eich cyfrif? "
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Ydych chi'n siŵr eich bod am farcio bod pob cofnod o <0>{sourceLabel}</0> wedi'i ddarllen?"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Ydych chi'n siŵr eich bod am farcio cofnodion sy'n hŷn na {trothwy} diwrnod o <0>{sourceLabel}</0> fel rhai sydd wedi'u darllen?"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "Ydych chi'n siŵr eich bod am ddad-danysgrifio o <0>{feedName}</0>?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Y newidynnau sydd ar gael yw 'teitl', 'cynnwys', 'url' 'awdur' a 'chategorïau' ac mae eu cynnwys yn cael ei drosi i lythrennau bach er mwyn hwyluso cymhariaeth llinynnol."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Yn ôl"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "Yn ôl i fewngofnodi"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Estyniadau porwr"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Diddymu"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Category"
|
||||
msgstr "categori"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "Bydd newid cyfrinair yn cynhyrchu allwedd API newydd"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Gwiriwch fod y porthiant yn gweithio"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
msgstr "Mae ComaFeed yn brosiect ffynhonnell agored. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed eitem nesaf heb ei darllen"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "Fersiwn ComaFeed {fersiwn} ({ adolygu})"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "cryno"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Cadarnhau"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Cadarnhau'r cyfrinair"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "clyd"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Create tag: {query}"
|
||||
msgstr "Creu tag: {query}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
msgstr "Cyfrinair presennol"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
msgstr "Dyddiad creu"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete"
|
||||
msgstr "Dileu"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "Dileu Categori"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Delete account"
|
||||
msgstr "Dileu cyfrif"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
msgstr "Dileu defnyddiwr"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr "Rhag"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "Arddangos"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr "Lawrlwytho"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Llusgwch y ddolen i'r bar nod tudalen"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "E-bost"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "cyfeiriad e-bost"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "Golygu defnyddiwr"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Wedi'i alluogi"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Enter"
|
||||
msgstr "Ewch i mewn"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Rhowch eich cyfrinair presennol i newid gosodiadau proffil"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Gwall"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "enghraifft: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Ehangu"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Allforio eich tanysgrifiadau a'ch categorïau fel ffeil OPML y gellir ei mewnforio i wasanaethau darllen porthiant eraill"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "URL porthiant"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "Enw porthiant"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Hidlo mynegiant"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Wedi anghofio cyfrinair?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Cynhyrchu allwedd API yn eich proffil yn gyntaf."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "Cynhyrchu allwedd API newydd"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "url porthiant a gynhyrchir"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "Ewch i'r ddogfennaeth API."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
msgstr "Ewch i'r golwg Pawb"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "nwyddau"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Os nad yw'n wag, mynegiad sy'n gwerthuso i 'gwir' neu 'anghywir'. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Os byddwch yn dod ar draws mater, rhowch wybod amdano ar dudalen materion y prosiect GitHub."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
msgstr "Os ydych chi'n hoffi'r prosiect hwn, ystyriwch rodd i gefnogi'r datblygwr a helpu i dalu costau cadw'r wefan hon ar-lein."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
msgstr "Mewnforio"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "Mewn gwedd estynedig, mae sgrolio trwy gofnodion yn nodi eu bod wedi'u darllen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Cadwch heb ei ddarllen"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "llwybrau byr bysellfwrdd"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
msgstr "Iaith"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "Dyddiad mewngofnodi diwethaf"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
msgstr "adnewyddu diwethaf"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh message"
|
||||
msgstr "Neges adnewyddu ddiwethaf"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Cyswllt"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Wrthi'n llwytho proffil..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading settings..."
|
||||
msgstr "Wrthi'n llwytho gosodiadau..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading subscriptions..."
|
||||
msgstr "Yn llwytho tanysgrifiadau..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading tags..."
|
||||
msgstr "Wrthi'n llwytho tagiau..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Mewngofnodi"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "Allgofnodi"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Rheoli defnyddwyr"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all as read"
|
||||
msgstr "Marciwch y cyfan wedi'i ddarllen"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Marciwch bob cofnod wedi'i ddarllen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Marciwch ei fod wedi'i ddarllen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Marciwch fel y darllenwyd hyd yma"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr "metrigau"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
msgstr "Symudwch y dudalen i lawr"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page up"
|
||||
msgstr "Symudwch y dudalen i fyny"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr "Amh"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Name"
|
||||
msgstr "Enw"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Llywiwch i danysgrifiad trwy nodi ei enw"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Cyfrinair newydd"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "Y diweddaraf yn gyntaf"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Nesaf"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Next refresh"
|
||||
msgstr "Adnewyddu nesaf"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "Llyfrnod yr eitem nesaf heb ei darllen"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "Dim mwy o gofnodion"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Dim wedi'i ddarganfod"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "allforio OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "ffeil OPML"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "Hynaf yn gyntaf"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Wps!"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "Agorwch y cofnod cyfredol mewn tab newydd"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Agorwch y cofnod cyfredol mewn tab newydd yn y cefndir"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Dolen agored"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "Agor y cofnod nesaf"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "Agor cofnod blaenorol"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "Agor / cau'r cofnod cyfredol"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "gorchymyn"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "rhiant"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "Categori Rhiant"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "cyfrinair"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
msgstr "Adfer Cyfrinair"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Nid yw cyfrineiriau yn cyfateb"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Swydd"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Proffil"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Recover password"
|
||||
msgstr "Adfer cyfrinair"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Adnewyddu"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "Mae cofrestriadau ar gau ar yr achos CommaFeed hwn"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Save"
|
||||
msgstr "Arbed"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Sgroliwch yn esmwyth wrth lywio rhwng cofnodion"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Search"
|
||||
msgstr "Chwilio"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Mae angen o leiaf 3 nod ar gyfer chwilio"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Gosodwch ffocws ar y cofnod nesaf heb ei agor"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "Gosod ffocws ar gofnod blaenorol heb ei agor"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "Gosodiadau"
|
||||
|
||||
#: src/app/slices/user.ts
|
||||
msgid "Settings saved."
|
||||
msgstr "Gosodiadau wedi'u cadw."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Share"
|
||||
msgstr "Rhannu"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "Rhannu gwefannau"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "shifft"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Dangos ffrydiau a chategorïau heb unrhyw gofnodion heb eu darllen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Dangos cymorth llwybr byr bysellfwrdd"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Cofrestrwch"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "Mae rhywbeth drwg newydd ddigwydd ..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "Gofod"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Star"
|
||||
msgstr "seren"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "serennog"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Tanysgrifio"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
msgstr "Tanysgrifio URL"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "Tanysgrifio i'r porthiant"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Success"
|
||||
msgstr "Llwyddiant"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "Newid i thema dywyll"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "Newid i thema golau"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
msgstr "Tagiau"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "Y URL ar gyfer y porthwr rydych chi am danysgrifio iddo. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
msgstr "Thema"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Toglo statws darllen y cofnod cyfredol"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Rhowch gynnig ar CommaFeed gyda'r cyfrif demo: demo / demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "Heb ei ddarllen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "dad-seren"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Unsubscribe"
|
||||
msgstr "Dad-danysgrifio"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "User Name or E-mail"
|
||||
msgstr "Enw Defnyddiwr neu E-bost"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "User name"
|
||||
msgstr "Enw defnyddiwr"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
msgstr "Rhybudd"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Website"
|
||||
msgstr "Gwefan"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "Nid oes gennych unrhyw danysgrifiadau eto. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "mae angen y ffeil"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
829
commafeed-client/src/locales/da/messages.po
Normal file
@@ -0,0 +1,829 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-10-28 13:47+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: da\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Har du en konto?</0><1>Log ind!</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Har du brug for en konto?</0><1>Tilmeld dig!</1>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API-nøgle"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "Omkring"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Handlinger"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "Tilføj"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
msgstr "Tilføj kategori"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Add user"
|
||||
msgstr "Tilføj bruger"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr ""
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Der er sendt en e-mail, hvis denne adresse var registreret. "
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "En opml-fil er en XML-fil, der indeholder feed-URL'er og kategorier. "
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyser foder"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "Er du sikker på, at du vil slette kategori <0>{categoryName}</0>?"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "Er du sikker på, at du vil slette bruger <0>{brugernavn}</0>?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "Er du sikker på, at du vil slette din konto? "
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Er du sikker på, at du vil markere alle poster i <0>{sourceLabel}</0> som læst?"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Er du sikker på, at du vil markere poster, der er ældre end {threshold} dage af <0>{sourceLabel}</0> som læst?"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "Er du sikker på, at du vil afmelde <0>{feedName}</0>?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Tilgængelige variabler er 'title', 'content', 'url' 'author' og 'category', og deres indhold konverteres til små bogstaver for at lette strengsammenligning."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Tilbage"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "Tilbage for at logge ind"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Browserudvidelser"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Annuller"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Category"
|
||||
msgstr "Kategori"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "Ændring af adgangskode vil generere en ny API-nøgle"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Tjek, at foderet virker"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
msgstr "CommaFeed er et open source-projekt. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed næste ulæste element"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "Kompakt"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Bekræft"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Bekræft adgangskode"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "Hyggeligt"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Create tag: {query}"
|
||||
msgstr "Opret tag: {query}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
msgstr "Nuværende adgangskode"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
msgstr "Dato oprettet"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete"
|
||||
msgstr "Slet"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "Slet kategori"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Delete account"
|
||||
msgstr "Slet konto"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
msgstr "Slet bruger"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "Skærm"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Træk linket til bogmærkelinjen"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "E-mail"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "E-mailadresse"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "Rediger bruger"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Aktiveret"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Enter"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Indtast din nuværende adgangskode for at ændre profilindstillinger"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Fejl"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Eksempel: {eksempel}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Udvidet"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Eksporter dine abonnementer og kategorier som en OPML-fil, der kan importeres i andre feed-læsningstjenester"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "Feednavn"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtrerende udtryk"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Glemt adgangskode?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Generer først en API-nøgle i din profil."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "Generer ny API-nøgle"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "Genereret feed-url"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "Gå til API-dokumentationen."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
msgstr "Gå til visningen Alle"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "Godbidder"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Hvis det ikke er tomt, et udtryk, der vurderes til 'sand' eller 'falsk'. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Hvis du støder på et problem, bedes du rapportere det på problemsiden for GitHub-projektet."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
msgstr "Hvis du kan lide dette projekt, bedes du overveje en donation for at støtte udvikleren og hjælpe med at dække omkostningerne ved at holde denne hjemmeside online."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "I udvidet visning markerer du dem som læst, når du ruller gennem poster"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Forbehold ulæst"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Tastaturgenveje"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
msgstr "Sprog"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "Sidste login dato"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
msgstr "Sidste opdatering"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh message"
|
||||
msgstr "Sidste opdateringsmeddelelse"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr ""
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Indlæser profil..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading settings..."
|
||||
msgstr "Indlæser indstillinger..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading subscriptions..."
|
||||
msgstr "Indlæser abonnementer..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading tags..."
|
||||
msgstr "Indlæser tags..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Log ind"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "Log ud"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Administrer brugere"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all as read"
|
||||
msgstr "Marker alle som læst"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Marker alle poster som læst"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Markér som læst"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Markér som læst indtil her"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
msgstr "Flyt siden ned"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page up"
|
||||
msgstr "Flyt siden op"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Name"
|
||||
msgstr "Navn"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Naviger til et abonnement ved at indtaste dets navn"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Ny adgangskode"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "Nyeste først"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Næste"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Next refresh"
|
||||
msgstr "Næste opdatering"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "Næste ulæste emne bogmærke"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "Ingen flere poster"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Intet fundet"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "OPML eksport"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "OPML fil"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "Ældst først"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Hovsa!"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "Åbn den aktuelle post i en ny fane"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Åbn den aktuelle post i en ny fane i baggrunden"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Åbent link"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "Åbn næste post"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "Åbn forrige post"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "Åbn/luk aktuel indgang"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Bestilling"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "Forælder"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "Forældrekategori"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Adgangskode"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
msgstr "Gendannelse af adgangskode"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Adgangskoder stemmer ikke overens"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Recover password"
|
||||
msgstr "Gendan adgangskode"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Opdater"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "Registreringer er lukket på denne CommaFeed-instans"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Save"
|
||||
msgstr "Gem"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Rul jævnt, når du navigerer mellem poster"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Search"
|
||||
msgstr "Søg"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Søgning kræver mindst 3 tegn"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Sæt fokus på næste post uden at åbne den"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "Sæt fokus på forrige indtastning uden at åbne den"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "Indstillinger"
|
||||
|
||||
#: src/app/slices/user.ts
|
||||
msgid "Settings saved."
|
||||
msgstr "Indstillinger gemt."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Share"
|
||||
msgstr "Del"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "Delingssider"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Skift"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Vis feeds og kategorier uden ulæste poster"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Vis hjælp til tastaturgenveje"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Tilmeld dig"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "Der er lige sket noget slemt..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "Rum"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Star"
|
||||
msgstr "Stjerne"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "Medvirkende"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Tilmeld"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "Abonner på feedet"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Success"
|
||||
msgstr "Succes"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "Skift til mørkt tema"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "Skift til lystema"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "URL'en til det feed, du vil abonnere på. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Skift læsestatus for den aktuelle post"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Prøv CommaFeed med demokontoen: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "Ulæst"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Unstar"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Unsubscribe"
|
||||
msgstr "Afmeld"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "User Name or E-mail"
|
||||
msgstr "Brugernavn eller e-mail"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "User name"
|
||||
msgstr "Brugernavn"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
msgstr "Advarsel"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Website"
|
||||
msgstr "Hjemmeside"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "Du har ingen abonnementer endnu. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "fil er påkrævet"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
829
commafeed-client/src/locales/de/messages.po
Normal file
@@ -0,0 +1,829 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-10-28 13:47+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: de\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Haben Sie ein Konto?</0><1>Melden Sie sich an!</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Benötigen Sie ein Konto?</0><1>Melden Sie sich an!</1>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API-Schlüssel"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "Ungefähr"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Aktionen"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "Hinzufügen"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
msgstr "Kategorie hinzufügen"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Add user"
|
||||
msgstr "Benutzer hinzufügen"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Verwaltung"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Eine E-Mail wurde gesendet, wenn diese Adresse registriert wurde. "
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "Eine opml-Datei ist eine XML-Datei, die Feed-URLs und Kategorien enthält. "
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "Feed analysieren"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "Sind Sie sicher, dass Sie die Kategorie <0>{categoryName}</0> löschen möchten?"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "Sind Sie sicher, dass Sie Benutzer <0>{userName}</0> löschen möchten?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "Sind Sie sicher, dass Sie Ihr Konto löschen möchten? "
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Sind Sie sicher, dass Sie alle Einträge von <0>{sourceLabel}</0> als gelesen markieren möchten?"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Sind Sie sicher, dass Sie Einträge, die älter als {threshold} Tage von <0>{sourceLabel}</0> sind, als gelesen markieren möchten?"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "Sind Sie sicher, dass Sie <0>{feedName}</0> abbestellen möchten?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr "Asz"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Verfügbare Variablen sind „Titel“, „Inhalt“, „URL“, „Autor“ und „Kategorien“, und ihr Inhalt wird in Kleinbuchstaben umgewandelt, um den String-Vergleich zu erleichtern."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Zurück"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "Zurück zum Anmelden"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Browsererweiterungen"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Category"
|
||||
msgstr "Kategorie"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "Das Ändern des Passworts generiert einen neuen API-Schlüssel"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Überprüfen Sie, ob der Feed funktioniert"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
msgstr "CommaFeed ist ein Open-Source-Projekt. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed nächstes ungelesenes Element"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed-Version {Version} ({Revision})"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "Kompakt"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Bestätigen"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Passwort bestätigen"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "Gemütlich"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Create tag: {query}"
|
||||
msgstr "Tag erstellen: {query}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr "Strg"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
msgstr "Aktuelles Passwort"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
msgstr "Erstellungsdatum"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "Kategorie löschen"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Delete account"
|
||||
msgstr "Konto löschen"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
msgstr "Benutzer löschen"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr "Beschr"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "Anzeige"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr "Herunterladen"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Link in Lesezeichenleiste ziehen"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "E-Mail"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "Benutzer bearbeiten"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Aktiviert"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Enter"
|
||||
msgstr "Eintreten"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Geben Sie Ihr aktuelles Passwort ein, um die Profileinstellungen zu ändern"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Fehler"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Beispiel: {Beispiel}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Erweitert"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Exportieren Sie Ihre Abonnements und Kategorien als OPML-Datei, die in andere Feed-Lesedienste importiert werden kann"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "Feed-URL"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "Feedname"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filterausdruck"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Passwort vergessen?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Generieren Sie zuerst einen API-Schlüssel in Ihrem Profil."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "Neuen API-Schlüssel generieren"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "Generierte Feed-URL"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "Gehen Sie zur API-Dokumentation."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
msgstr "Zur Ansicht Alle wechseln"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "Gutes"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Wenn nicht leer, ein Ausdruck, der als „wahr“ oder „falsch“ ausgewertet wird. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Wenn Sie auf ein Problem stoßen, melden Sie es bitte auf der Problemseite des GitHub-Projekts."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
msgstr "Wenn Ihnen dieses Projekt gefällt, denken Sie bitte über eine Spende nach, um den Entwickler zu unterstützen und die Kosten für die Onlinehaltung dieser Website zu decken."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
msgstr "Importieren"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "In der erweiterten Ansicht werden Einträge beim Scrollen als gelesen markiert"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Ungelesen lassen"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Tastenkürzel"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "Letztes Anmeldedatum"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
msgstr "Letzte Aktualisierung"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh message"
|
||||
msgstr "Letzte Aktualisierungsmeldung"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Verbindung"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Lade Profil..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading settings..."
|
||||
msgstr "Lade Einstellungen..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading subscriptions..."
|
||||
msgstr "Abonnements werden geladen..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading tags..."
|
||||
msgstr "Tags werden geladen..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Einloggen"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "Abmelden"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Benutzer verwalten"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all as read"
|
||||
msgstr "Alle als gelesen markieren"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Alle Einträge als gelesen markieren"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Als gelesen markieren"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Bis hierhin als gelesen markieren"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr "Metriken"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
msgstr "Seite nach unten verschieben"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page up"
|
||||
msgstr "Bewege die Seite nach oben"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr "n. z"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigieren Sie zu einem Abonnement, indem Sie seinen Namen eingeben"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Neues Passwort"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "Neueste zuerst"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Weiter"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Next refresh"
|
||||
msgstr "Nächste Aktualisierung"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "Lesezeichen für das nächste ungelesene Element"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "Keine weiteren Einträge"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Nichts gefunden"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "OPML-Export"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "OPML-Datei"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "Älteste zuerst"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Ups!"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "Aktuellen Eintrag in neuem Tab öffnen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Aktuellen Eintrag in neuem Tab im Hintergrund öffnen"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Link öffnen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "Nächsten Eintrag öffnen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "Vorherigen Eintrag öffnen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "Aktuellen Eintrag öffnen/schließen"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Bestellung"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "Elternteil"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "Elternkategorie"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
msgstr "Passwortwiederherstellung"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Passwörter stimmen nicht überein"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Stellung"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr "REST-API"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Recover password"
|
||||
msgstr "Kennwort wiederherstellen"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Aktualisieren"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "Registrierungen sind für diese CommaFeed-Instanz geschlossen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Geschwindes Scrollen beim Navigieren zwischen Einträgen"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Search"
|
||||
msgstr "Suche"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Suche erfordert mindestens 3 Zeichen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Fokus auf den nächsten Eintrag setzen, ohne ihn zu öffnen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "Fokus auf vorherigen Eintrag setzen, ohne ihn zu öffnen"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: src/app/slices/user.ts
|
||||
msgid "Settings saved."
|
||||
msgstr "Einstellungen gespeichert."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Share"
|
||||
msgstr "Teilen"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "Seiten teilen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Verschiebung"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Feeds und Kategorien ohne ungelesene Einträge anzeigen"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Tastenkürzel-Hilfe anzeigen"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Melden Sie sich an"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "Etwas Schlimmes ist gerade passiert..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "Raum"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Star"
|
||||
msgstr "Stern"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "Markiert"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Abonnieren"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
msgstr "URL abonnieren"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "Feed abonnieren"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Success"
|
||||
msgstr "Erfolg"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "Zum dunklen Design wechseln"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "Wechseln Sie zum Lichtdesign"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "Die URL für den Feed, den Sie abonnieren möchten. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
msgstr "Thema"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Lesestatus des aktuellen Eintrags umschalten"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Testen Sie CommaFeed mit dem Demokonto: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "Ungelesen"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "Stern entfernen"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Unsubscribe"
|
||||
msgstr "Abbestellen"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "User Name or E-mail"
|
||||
msgstr "Benutzername oder E-Mail"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "User name"
|
||||
msgstr "Benutzername"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
msgstr "Warnung"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Website"
|
||||
msgstr "Webseite"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "Sie haben noch keine Abonnements. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "Datei ist erforderlich"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
829
commafeed-client/src/locales/en/messages.po
Normal file
@@ -0,0 +1,829 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-08-04 18:51+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: en\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr "<0>Complete syntax is available </0><1>here</1>."
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>Have an account?</0><1>Log in!</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>Need an account?</0><1>Sign up!</1>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "API key"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "About"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Actions"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "Add"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
msgstr "Add category"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Add user"
|
||||
msgstr "Add user"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "All"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "An email has been sent if this address was registered. Check your inbox."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analyze feed"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "Are you sure you want to delete your account? There's no turning back!"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr "Asc"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Back"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "Back to log in"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Browser extentions"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Category"
|
||||
msgstr "Category"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "Changing password will generate a new API key"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Check that the feed is working"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
msgstr "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed next unread item"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "CommaFeed version {version} ({revision})"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "Compact"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Confirm"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Confirm password"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "Cozy"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Create tag: {query}"
|
||||
msgstr "Create tag: {query}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr "Ctrl"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
msgstr "Current password"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr "Custom CSS rules that will be applied"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr "Custom JS code that will be executed on page load"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr "Custom code"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
msgstr "Date created"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete"
|
||||
msgstr "Delete"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "Delete Category"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Delete account"
|
||||
msgstr "Delete account"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
msgstr "Delete user"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr "Desc"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr "Detailed"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "Display"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr "Download"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Drag link to bookmark bar"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "E-mail"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "E-mail address"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "Edit user"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Enabled"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Enter"
|
||||
msgstr "Enter"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Enter your current password to change profile settings"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Example: {example}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Expanded"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "Feed URL"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "Feed name"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr "Fetch all my feeds now"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Filtering expression"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
msgstr "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "Forgot password?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Generate an API key in your profile first."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "Generate new API key"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "Generated feed url"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "Go to the API documentation."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
msgstr "Go to the All view"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr "Go to {0}"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "Goodies"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr "Id"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
msgstr "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
msgstr "Import"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "In expanded view, scrolling through entries mark them as read"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Keep unread"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "Keyboard shortcuts"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
msgstr "Language"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "Last login date"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
msgstr "Last refresh"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh message"
|
||||
msgstr "Last refresh message"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Link"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Loading profile..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading settings..."
|
||||
msgstr "Loading settings..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading subscriptions..."
|
||||
msgstr "Loading subscriptions..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading tags..."
|
||||
msgstr "Loading tags..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Log in"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "Logout"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr "Long press"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Manage users"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all as read"
|
||||
msgstr "Mark all as read"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Mark all entries as read"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Mark as read"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Mark as read up to here"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr "Metrics"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr "Middle click"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
msgstr "Move the page down"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page up"
|
||||
msgstr "Move the page up"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navigate to a subscription by entering its name"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "New password"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "Newest first"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Next"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Next refresh"
|
||||
msgstr "Next refresh"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "Next unread item bookmarklet"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "No more entries"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Nothing found"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr "OPML"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "OPML export"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "OPML file"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "Oldest first"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "Oops!"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "Open current entry in a new tab"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Open current entry in a new tab in the background"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Open link"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr "Open link in new background tab"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr "Open link in new tab"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "Open next entry"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "Open previous entry"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "Open/close current entry"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Order"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "Parent"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "Parent Category"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
msgstr "Password Recovery"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Passwords do not match"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Position"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Profile"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr "REST API"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Recover password"
|
||||
msgstr "Recover password"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Refresh"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "Registrations are closed on this CommaFeed instance"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr "Right click"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Save"
|
||||
msgstr "Save"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Scroll smoothly when navigating between entries"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Search"
|
||||
msgstr "Search"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "Search requires at least 3 characters"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Set focus on next entry without opening it"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "Set focus on previous entry without opening it"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "Settings"
|
||||
|
||||
#: src/app/slices/user.ts
|
||||
msgid "Settings saved."
|
||||
msgstr "Settings saved."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Share"
|
||||
msgstr "Share"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "Sharing sites"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Shift"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr "Show entry menu (desktop)"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr "Show entry menu (mobile)"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Show feeds and categories with no unread entries"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Show keyboard shortcut help"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Sign up"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "Something bad just happened..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "Space"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Star"
|
||||
msgstr "Star"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "Starred"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Subscribe"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
msgstr "Subscribe URL"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "Subscribe to the feed"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Success"
|
||||
msgstr "Success"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr "Swipe header to the right"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "Switch to dark theme"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "Switch to light theme"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
msgstr "Tags"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
msgstr "Theme"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Toggle read status of current entry"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Try out CommaFeed with the demo account: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr "Try the demo!"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "Unread"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "Unstar"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Unsubscribe"
|
||||
msgstr "Unsubscribe"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "User Name or E-mail"
|
||||
msgstr "User Name or E-mail"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "User name"
|
||||
msgstr "User name"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
msgstr "Warning"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Website"
|
||||
msgstr "Website"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr "Your feeds have been queued for refresh."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "file is required"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr "{0} (in {1})"
|
||||
829
commafeed-client/src/locales/es/messages.po
Normal file
@@ -0,0 +1,829 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-10-28 13:47+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: es\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>¿Tienes una cuenta?</0><1>¡Inicia sesión!</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>¿Necesitas una cuenta?</0><1>¡Regístrate!</1>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "clave API"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "Sobre"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
msgstr "Acciones"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "Agregar"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
msgstr "Añadir categoría"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Add user"
|
||||
msgstr "Añadir usuario"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr "Administrador"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "Todo"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "Se ha enviado un correo electrónico si se registró esta dirección. "
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "Un archivo opml es un archivo XML que contiene categorías y direcciones URL de fuentes. "
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "Analizar alimentación"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "¿Está seguro de que desea eliminar la categoría <0>{categoryName}</0>?"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "¿Está seguro de que desea eliminar el usuario <0>{userName}</0> ?"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "¿Está seguro de que desea eliminar su cuenta? "
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "¿Está seguro de que desea marcar todas las entradas de <0>{sourceLabel}</0> como leídas?"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "¿Está seguro de que desea marcar las entradas anteriores a {threshold} días de <0>{sourceLabel}</0> como leídas?"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "¿Está seguro de que desea darse de baja de <0>{feedName}</0>?"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr "ASC"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "Las variables disponibles son 'título', 'contenido', 'url', 'autor' y 'categorías' y su contenido se convierte a minúsculas para facilitar la comparación de cadenas."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "Atrás"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "Volver a iniciar sesión"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "Extensiones del navegador"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Category"
|
||||
msgstr "Categoría"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "Cambiar la contraseña generará una nueva clave API"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "Compruebe que el feed funciona"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
msgstr "CommaFeed es un proyecto de código abierto. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "CommaFeed siguiente elemento no leído"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "versión de CommaFeed {versión} ({revisión})"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "Compacto"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "Confirmar"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "Confirmar contraseña"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "Acogedor"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Create tag: {query}"
|
||||
msgstr "Crear etiqueta: {consulta}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
msgstr "Contraseña actual"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
msgstr "Fecha de creación"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete"
|
||||
msgstr "Borrar"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "Borrar categoría"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Delete account"
|
||||
msgstr "Borrar cuenta"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
msgstr "Borrar usuario"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "Pantalla"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr "descargar"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "Arrastra el enlace a la barra de marcadores"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "Correo electrónico"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "dirección de correo electrónico"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "Editar usuario"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "Habilitado"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Enter"
|
||||
msgstr "Entrar"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "Ingrese su contraseña actual para cambiar la configuración del perfil"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "Ejemplo: {ejemplo}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "Expandido"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "Exporte sus suscripciones y categorías como un archivo OPML que se puede importar en otros servicios de lectura de feeds"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "URL de fuente"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "Nombre de alimentación"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "Expresión de filtrado"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "¿Olvidaste la contraseña?"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "Primero genere una clave API en su perfil."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "Generar nueva clave API"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "URL del feed generado"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "Ir a la documentación de la API."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
msgstr "Ir a la vista Todo"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "golosinas"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr "Identificación"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "Si no está vacío, una expresión que se evalúa como 'verdadero' o 'falso'. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "Si encuentra un problema, infórmelo en la página de problemas del proyecto GitHub."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
msgstr "Si le gusta este proyecto, considere una donación para apoyar al desarrollador y ayudar a cubrir los costos de mantener este sitio web en línea."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
msgstr "Importar"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "En la vista ampliada, al desplazarse por las entradas, márquelas como leídas"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "Mantener sin leer"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "atajos de teclado"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "fecha del último inicio de sesión"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
msgstr "Última actualización"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh message"
|
||||
msgstr "Último mensaje de actualización"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "Enlace"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "Cargando perfil..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading settings..."
|
||||
msgstr "Cargando ajustes..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading subscriptions..."
|
||||
msgstr "Cargando suscripciones..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading tags..."
|
||||
msgstr "Cargando etiquetas..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "Iniciar sesión"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "Salir"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "Administrar usuarios"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all as read"
|
||||
msgstr "Marcar todo como leído"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "Marcar todas las entradas como leídas"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "Marcar como leído"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "Marcar como leído hasta aquí"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr "Métricas"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
msgstr "Mover la página hacia abajo"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page up"
|
||||
msgstr "Mover la página hacia arriba"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr "N/D"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "Navegar a una suscripción ingresando su nombre"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "Nueva contraseña"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "más reciente primero"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "Siguiente"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Next refresh"
|
||||
msgstr "Próxima actualización"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "Bookmarklet del siguiente elemento no leído"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "No más entradas"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "Nada encontrado"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "Exportación OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "archivo OPML"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "más antigua primero"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "¡Ups!"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "Abrir la entrada actual en una nueva pestaña"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "Abrir la entrada actual en una nueva pestaña en segundo plano"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "Abrir enlace"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "Abrir siguiente entrada"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "Abrir entrada anterior"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "Abrir/cerrar entrada actual"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "Orden"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "Padre"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "Categoría principal"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
msgstr "Recuperación de contraseña"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Las contraseñas no coinciden"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "Posición"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr "API REST"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Recover password"
|
||||
msgstr "Recuperar contraseña"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "Actualizar"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "Los registros están cerrados en esta instancia de CommaFeed"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Save"
|
||||
msgstr "Guardar"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "Desplazarse suavemente al navegar entre entradas"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Search"
|
||||
msgstr "Buscar"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "La búsqueda requiere al menos 3 caracteres"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "Establezca el foco en la siguiente entrada sin abrirla"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "Poner el foco en la entrada anterior sin abrirla"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "Configuraciones"
|
||||
|
||||
#: src/app/slices/user.ts
|
||||
msgid "Settings saved."
|
||||
msgstr "Ajustes guardados."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Share"
|
||||
msgstr "Compartir"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "Compartir sitios"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "Cambio"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "Mostrar feeds y categorías sin entradas no leídas"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "Mostrar ayuda de atajo de teclado"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "Registrarse"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "Algo malo acaba de pasar..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "Espacio"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Star"
|
||||
msgstr "estrella"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "Destacado"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "Suscribirse"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
msgstr "URL de suscripción"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "Suscríbete a la fuente"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Success"
|
||||
msgstr "Éxito"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "Cambiar a tema oscuro"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "Cambiar a tema claro"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "La URL de la fuente a la que desea suscribirse. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
msgstr "Tema"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "Alternar estado de lectura de la entrada actual"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "Pruebe CommaFeed con la cuenta demo: demo/demo"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "No leído"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Unstar"
|
||||
msgstr "Desmarcar"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Unsubscribe"
|
||||
msgstr "Cancelar suscripción"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "User Name or E-mail"
|
||||
msgstr "Nombre de usuario o correo electrónico"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "User name"
|
||||
msgstr "Nombre de usuario"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
msgstr "Advertencia"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Website"
|
||||
msgstr "Sitio web"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "Todavía no tienes ninguna suscripción. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "archivo requerido"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||
829
commafeed-client/src/locales/fa/messages.po
Normal file
@@ -0,0 +1,829 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2022-10-28 13:47+0200\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: fa\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "<0>Complete syntax is available </0><1>here</1>."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "<0>Have an account?</0><1>Log in!</1>"
|
||||
msgstr "<0>حساب دارید؟</0><1>وارد سیستم شوید!</1>"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "<0>Need an account?</0><1>Sign up!</1>"
|
||||
msgstr "<0>به یک حساب نیاز دارید؟</0><1>ثبت نام کنید!</1>"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "API key"
|
||||
msgstr "کلید API"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "About"
|
||||
msgstr "در مورد"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Actions"
|
||||
msgstr "اعمال"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Add"
|
||||
msgstr "اضافه کنید"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Add category"
|
||||
msgstr "اضافه کردن دسته"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Add user"
|
||||
msgstr "افزودن کاربر"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Admin"
|
||||
msgstr "مدیر"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "All"
|
||||
msgstr "همه"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "An email has been sent if this address was registered. Check your inbox."
|
||||
msgstr "اگر این آدرس ثبت شده باشد ایمیل ارسال شده است. "
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "An opml file is an XML file containing feed URLs and categories. You can get an OPML file by exporting your data from other feed reading services."
|
||||
msgstr "یک فایل opml یک فایل XML است که حاوی آدرسها و دستههای فید است. "
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Analyze feed"
|
||||
msgstr "خوراک را تجزیه و تحلیل کنید"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Are you sure you want to delete category <0>{categoryName}</0>?"
|
||||
msgstr "آیا مطمئن هستید که می خواهید دسته <0>{categoryName}</0> را حذف کنید؟"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Are you sure you want to delete user <0>{userName}</0> ?"
|
||||
msgstr "آیا مطمئن هستید که می خواهید کاربر <0>{userName}</0> را حذف کنید؟"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Are you sure you want to delete your account? There's no turning back!"
|
||||
msgstr "آیا مطمئن هستید که می خواهید حساب خود را حذف کنید؟ "
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark all entries of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "آیا مطمئن هستید که می خواهید همه ورودی های <0>{sourceLabel}</0> را به عنوان خوانده شده علامت گذاری کنید؟"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Are you sure you want to mark entries older than {threshold} days of <0>{sourceLabel}</0> as read?"
|
||||
msgstr "آیا مطمئن هستید که می خواهید ورودی های قدیمی تر از {threshold} روز <0>{sourceLabel}</0> را به عنوان خوانده شده علامت گذاری کنید؟"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Are you sure you want to unsubscribe from <0>{feedName}</0>?"
|
||||
msgstr "آیا مطمئن هستید که می خواهید اشتراک <0>{feedName}</0> را لغو کنید؟"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Asc"
|
||||
msgstr "صعودی"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Available variables are 'title', 'content', 'url' 'author' and 'categories' and their content is converted to lower case to ease string comparison."
|
||||
msgstr "متغیرهای موجود عبارتند از «عنوان»، «محتوا»، «url» «نویسنده» و «دستهها» و محتوای آنها برای سهولت مقایسه رشتهها به حروف کوچک تبدیل میشود."
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Back"
|
||||
msgstr "برگشت"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Back to log in"
|
||||
msgstr "بازگشت برای ورود به سیستم"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Browser extentions"
|
||||
msgstr "گسترش مرورگر"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Cancel"
|
||||
msgstr "لغو"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Category"
|
||||
msgstr "مقوله"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Changing password will generate a new API key"
|
||||
msgstr "تغییر رمز عبور یک کلید API جدید ایجاد می کند"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Check that the feed is working"
|
||||
msgstr "بررسی کنید که خوراک کار می کند"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed is an open-source project. Sources are hosted on <0>GitHub</0>."
|
||||
msgstr "CommaFeed یک پروژه منبع باز است. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed next unread item"
|
||||
msgstr "مورد خوانده نشده بعدی CommaFeed"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "CommaFeed version {version} ({revision})"
|
||||
msgstr "نسخه {نسخه} CommaFeed ({نسخه})"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Compact"
|
||||
msgstr "فشرده"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Confirm"
|
||||
msgstr "تأیید کنید"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Confirm password"
|
||||
msgstr "رمز عبور را تأیید کنید"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Cozy"
|
||||
msgstr "دنج"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Create tag: {query}"
|
||||
msgstr "ایجاد برچسب: {query}"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Ctrl"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Current password"
|
||||
msgstr "رمز عبور فعلی"
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom CSS rules that will be applied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
msgid "Custom JS code that will be executed on page load"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Custom code"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Date created"
|
||||
msgstr "تاریخ ایجاد"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete"
|
||||
msgstr "حذف کنید"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Delete Category"
|
||||
msgstr "حذف دسته"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Delete account"
|
||||
msgstr "حذف حساب"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Delete user"
|
||||
msgstr "حذف کاربر"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Desc"
|
||||
msgstr "توصیف"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Detailed"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Display"
|
||||
msgstr "نمایش"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Download"
|
||||
msgstr "دانلود"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Drag link to bookmark bar"
|
||||
msgstr "پیوند را به نوار نشانک بکشید"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "E-mail"
|
||||
msgstr "ایمیل"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "E-mail address"
|
||||
msgstr "آدرس ایمیل"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Edit user"
|
||||
msgstr "ویرایش کاربر"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Enabled"
|
||||
msgstr "فعال"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Enter"
|
||||
msgstr "وارد شوید"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Enter your current password to change profile settings"
|
||||
msgstr "رمز عبور فعلی خود را برای تغییر تنظیمات نمایه وارد کنید"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Error"
|
||||
msgstr "خطا"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Example: {example}."
|
||||
msgstr "مثال: {مثال}."
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Expanded"
|
||||
msgstr "گسترش یافت"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Export your subscriptions and categories as an OPML file that can be imported in other feed reading services"
|
||||
msgstr "اشتراک ها و دسته های خود را به عنوان یک فایل OPML صادر کنید که می تواند در سایر خدمات خواندن فید وارد شود"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Feed URL"
|
||||
msgstr "URL فید"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Feed name"
|
||||
msgstr "نام فید"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Fetch all my feeds now"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Filtering expression"
|
||||
msgstr "بیان فیلتر"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "For those of you who prefer bitcoin, here is the address: {bitcoinAddress}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Forgot password?"
|
||||
msgstr "رمز عبور را فراموش کرده اید؟"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generate an API key in your profile first."
|
||||
msgstr "ابتدا یک کلید API در نمایه خود ایجاد کنید."
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Generate new API key"
|
||||
msgstr "کلید API جدید ایجاد کنید"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Generated feed url"
|
||||
msgstr "آدرس اینترنتی فید تولید شده"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Go to the API documentation."
|
||||
msgstr "به مستندات API بروید."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Go to the All view"
|
||||
msgstr "به نمای All بروید"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Go to {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Goodies"
|
||||
msgstr "خوبی ها"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Id"
|
||||
msgstr "شناسه"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "If not empty, an expression evaluating to 'true' or 'false'. If false, new entries for this feed will be marked as read automatically."
|
||||
msgstr "اگر خالی نباشد، عبارتی به \"درست\" یا \"نادرست\" ارزیابی می شود. "
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you encounter an issue, please report it on the issues page of the GitHub project."
|
||||
msgstr "اگر با مشکلی مواجه شدید، لطفاً آن را در صفحه مشکلات پروژه GitHub گزارش دهید."
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "If you like this project, please consider a donation to support the developer and help cover the costs of keeping this website online."
|
||||
msgstr "اگر این پروژه را دوست دارید، لطفاً کمک مالی برای حمایت از توسعه دهنده و کمک به پوشش هزینه های آنلاین نگه داشتن این وب سایت در نظر بگیرید."
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "Import"
|
||||
msgstr "واردات"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "In expanded view, scrolling through entries mark them as read"
|
||||
msgstr "در نمای بازشده، پیمایش در ورودیها، آنها را به عنوان خوانده شده علامتگذاری میکند"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Keep unread"
|
||||
msgstr "خوانده نشده نگه دارید"
|
||||
|
||||
#: src/components/content/FeedEntries.tsx
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Keyboard shortcuts"
|
||||
msgstr "میانبرهای صفحه کلید"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Language"
|
||||
msgstr "زبان"
|
||||
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Last login date"
|
||||
msgstr "آخرین تاریخ ورود"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh"
|
||||
msgstr "آخرین به روز رسانی"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Last refresh message"
|
||||
msgstr "آخرین پیام تازه کردن"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/TagDetailsPage.tsx
|
||||
msgid "Link"
|
||||
msgstr "پیوند"
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading profile..."
|
||||
msgstr "بارگیری نمایه..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading settings..."
|
||||
msgstr "بارگیری تنظیمات..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading subscriptions..."
|
||||
msgstr "بارگیری اشتراک ها..."
|
||||
|
||||
#: src/hooks/useAppLoading.ts
|
||||
msgid "Loading tags..."
|
||||
msgstr "بارگیری برچسب ها..."
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Log in"
|
||||
msgstr "وارد شوید"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Logout"
|
||||
msgstr "خروج"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Long press"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
msgid "Manage users"
|
||||
msgstr "کاربران را مدیریت کنید"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
msgid "Mark all as read"
|
||||
msgstr "همه را به عنوان خوانده شده علامت گذاری کنید"
|
||||
|
||||
#: src/components/header/MarkAllAsReadButton.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Mark all entries as read"
|
||||
msgstr "همه ورودی ها را به عنوان خوانده شده علامت گذاری کنید"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read"
|
||||
msgstr "علامت گذاری به عنوان خوانده شده"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Mark as read up to here"
|
||||
msgstr "تا اینجا به عنوان خوانده شده علامت بزنید"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Metrics"
|
||||
msgstr "متریک"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Middle click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page down"
|
||||
msgstr "صفحه را به پایین ببرید"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Move the page up"
|
||||
msgstr "صفحه را به بالا ببرید"
|
||||
|
||||
#: src/components/RelativeDate.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/admin/AdminUsersPage.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Name"
|
||||
msgstr "نام"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Navigate to a subscription by entering its name"
|
||||
msgstr "با وارد کردن نام اشتراک، به آن بروید"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "New password"
|
||||
msgstr "رمز عبور جدید"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Newest first"
|
||||
msgstr "ابتدا جدیدترین"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Next"
|
||||
msgstr "بعد"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Next refresh"
|
||||
msgstr "رفرش بعدی"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Next unread item bookmarklet"
|
||||
msgstr "بوکمارک مورد خوانده نشده بعدی"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "No more entries"
|
||||
msgstr "ورودی دیگری وجود ندارد"
|
||||
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Nothing found"
|
||||
msgstr "چیزی پیدا نشد"
|
||||
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "OPML"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "OPML export"
|
||||
msgstr "صادرات OPML"
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "OPML file"
|
||||
msgstr "فایل OPML"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Oldest first"
|
||||
msgstr "قدیمی ترین اول"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Oops!"
|
||||
msgstr "اوه!"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab"
|
||||
msgstr "ورودی فعلی را در یک برگه جدید باز کنید"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open current entry in a new tab in the background"
|
||||
msgstr "ورودی فعلی را در یک برگه جدید در پس زمینه باز کنید"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Open link"
|
||||
msgstr "پیوند را باز کنید"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new background tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
msgid "Open link in new tab"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open next entry"
|
||||
msgstr "ورودی بعدی را باز کنید"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open previous entry"
|
||||
msgstr "ورودی قبلی را باز کنید"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Open/close current entry"
|
||||
msgstr "باز کردن/بستن ورودی فعلی"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Order"
|
||||
msgstr "سفارش"
|
||||
|
||||
#: src/components/content/add/AddCategory.tsx
|
||||
msgid "Parent"
|
||||
msgstr "پدر و مادر"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
msgid "Parent Category"
|
||||
msgstr "دسته والد"
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Password"
|
||||
msgstr "رمز عبور"
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Password Recovery"
|
||||
msgstr "بازیابی رمز عبور"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "Passwords do not match"
|
||||
msgstr "گذرواژه ها مطابقت ندارند"
|
||||
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Position"
|
||||
msgstr "موقعیت"
|
||||
|
||||
#: src/pages/app/SettingsPage.tsx
|
||||
msgid "Profile"
|
||||
msgstr "نمایه"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "REST API"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/auth/PasswordRecoveryPage.tsx
|
||||
msgid "Recover password"
|
||||
msgstr "بازیابی رمز عبور"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Refresh"
|
||||
msgstr "تازه کردن"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
msgid "Registrations are closed on this CommaFeed instance"
|
||||
msgstr "ثبت نام در این نمونه CommaFeed بسته شده است"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Right click"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/admin/UserEdit.tsx
|
||||
#: src/components/settings/CustomCodeSettings.tsx
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
#: src/pages/app/CategoryDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Save"
|
||||
msgstr "ذخیره کنید"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Scroll smoothly when navigating between entries"
|
||||
msgstr "هنگام پیمایش بین ورودیها به آرامی حرکت کنید"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/header/Header.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
#: src/components/sidebar/TreeSearch.tsx
|
||||
msgid "Search"
|
||||
msgstr "جستجو"
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Search requires at least 3 characters"
|
||||
msgstr "جستجو به حداقل 3 کاراکتر نیاز دارد"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on next entry without opening it"
|
||||
msgstr "فوکوس را روی ورودی بعدی بدون باز کردن آن تنظیم کنید"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Set focus on previous entry without opening it"
|
||||
msgstr "فوکوس را روی ورودی قبلی بدون باز کردن آن تنظیم کنید"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Settings"
|
||||
msgstr "تنظیمات"
|
||||
|
||||
#: src/app/slices/user.ts
|
||||
msgid "Settings saved."
|
||||
msgstr "تنظیمات ذخیره شد."
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Share"
|
||||
msgstr "به اشتراک بگذارید"
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Sharing sites"
|
||||
msgstr "اشتراک گذاری سایت ها"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Shift"
|
||||
msgstr "شیفت"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (desktop)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show entry menu (mobile)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/settings/DisplaySettings.tsx
|
||||
msgid "Show feeds and categories with no unread entries"
|
||||
msgstr "فیدها و دسته ها را بدون ورودی خوانده نشده نشان دهید"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Show keyboard shortcut help"
|
||||
msgstr "نمایش راهنمایی میانبر صفحه کلید"
|
||||
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/auth/RegistrationPage.tsx
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Sign up"
|
||||
msgstr "ثبت نام کنید"
|
||||
|
||||
#: src/pages/ErrorPage.tsx
|
||||
msgid "Something bad just happened..."
|
||||
msgstr "اتفاق بدی افتاد..."
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Space"
|
||||
msgstr "فضا"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Star"
|
||||
msgstr "ستاره"
|
||||
|
||||
#: src/app/constants.ts
|
||||
#: src/components/sidebar/Tree.tsx
|
||||
msgid "Starred"
|
||||
msgstr "ستاره دار"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
#: src/pages/app/AddPage.tsx
|
||||
msgid "Subscribe"
|
||||
msgstr "مشترک شوید"
|
||||
|
||||
#: src/pages/app/AboutPage.tsx
|
||||
msgid "Subscribe URL"
|
||||
msgstr "URL اشتراک"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "Subscribe to the feed"
|
||||
msgstr "در فید مشترک شوید"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Success"
|
||||
msgstr "موفقیت"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Swipe header to the right"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to dark theme"
|
||||
msgstr "تغییر به تم تیره"
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Switch to light theme"
|
||||
msgstr "روی زمینه روشن"
|
||||
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Tags"
|
||||
msgstr "برچسب ها"
|
||||
|
||||
#: src/components/content/add/Subscribe.tsx
|
||||
msgid "The URL for the feed you want to subscribe to. You can also use the website's url directly and CommaFeed will try to find the feed in the page."
|
||||
msgstr "URL فیدی که می خواهید در آن مشترک شوید. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Theme"
|
||||
msgstr "تم"
|
||||
|
||||
#: src/components/KeyboardShortcutsHelp.tsx
|
||||
msgid "Toggle read status of current entry"
|
||||
msgstr "وضعیت خواندن ورودی فعلی را تغییر دهید"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "Try out CommaFeed with the demo account: demo/demo"
|
||||
msgstr "CommaFeed را با حساب آزمایشی امتحان کنید: دمو/دمو"
|
||||
|
||||
#: src/pages/WelcomePage.tsx
|
||||
msgid "Try the demo!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/header/Header.tsx
|
||||
msgid "Unread"
|
||||
msgstr "خوانده نشده"
|
||||
|
||||
#: src/components/content/FeedEntryContextMenu.tsx
|
||||
#: src/components/content/FeedEntryFooter.tsx
|
||||
msgid "Unstar"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Unsubscribe"
|
||||
msgstr "لغو اشتراک"
|
||||
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
#: src/pages/auth/LoginPage.tsx
|
||||
msgid "User Name or E-mail"
|
||||
msgstr "نام کاربری یا ایمیل"
|
||||
|
||||
#: src/components/settings/ProfileSettings.tsx
|
||||
msgid "User name"
|
||||
msgstr "نام کاربری"
|
||||
|
||||
#: src/components/Alert.tsx
|
||||
msgid "Warning"
|
||||
msgstr "هشدار"
|
||||
|
||||
#: src/pages/app/FeedDetailsPage.tsx
|
||||
msgid "Website"
|
||||
msgstr "وب سایت"
|
||||
|
||||
#: src/pages/app/FeedEntriesPage.tsx
|
||||
msgid "You don't have any subscriptions yet. Why not try adding one by clicking on the + sign at the top of the page?"
|
||||
msgstr "شما هنوز هیچ اشتراکی ندارید. "
|
||||
|
||||
#: src/components/header/ProfileMenu.tsx
|
||||
msgid "Your feeds have been queued for refresh."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/content/add/ImportOpml.tsx
|
||||
msgid "file is required"
|
||||
msgstr "فایل مورد نیاز است"
|
||||
|
||||
#: src/components/content/add/CategorySelect.tsx
|
||||
msgid "{0} (in {1})"
|
||||
msgstr ""
|
||||