diff --git a/Pipfile b/Pipfile index 2400201..b5d1cc1 100755 --- a/Pipfile +++ b/Pipfile @@ -22,6 +22,7 @@ aiosqlite = "*" "colorgram.py" = "*" spotipy = "*" aiofiles = "*" +i18nice = {extras = ["yaml"], version = "*"} [dev-packages] requests-testadapter = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 39da0e0..4a75876 100755 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "081f2673b410f7b93d1b39894ab05e62b449bee4eae89ad1a5406b231c543ad6" + "sha256": "2e656e2e2f8a5084d045b7c9f28b12f1509cf734d38d4cf900fed7b73f872965" }, "pipfile-spec": 6, "requires": { @@ -26,93 +26,93 @@ }, "aiohappyeyeballs": { "hashes": [ - "sha256:40a16ceffcf1fc9e142fd488123b2e218abc4188cf12ac20c67200e1579baa42", - "sha256:7e1ae8399c320a8adec76f6c919ed5ceae6edd4c3672f4d9eae2b27e37c80ff6" + "sha256:337ce4dc0e99eb697c3c5a77d6cb3c52925824d9a67ac0dea7c55b8a2d60b222", + "sha256:e794cd29ba6a14078092984e43688212a19081de3a73b6796c2fdeb3706dd6ce" ], - "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==2.3.4" + "markers": "python_version >= '3.8'", + "version": "==2.3.7" }, "aiohttp": { "hashes": [ - "sha256:01c3f1eb280008e51965a8d160a108c333136f4a39d46f516c64d2aa2e6a53f2", - "sha256:028faf71b338f069077af6315ad54281612705d68889f5d914318cbc2aab0d50", - "sha256:03c0c380c83f8a8d4416224aafb88d378376d6f4cadebb56b060688251055cd4", - "sha256:0df51a3d70a2bfbb9c921619f68d6d02591f24f10e9c76de6f3388c89ed01de6", - "sha256:120548d89f14b76a041088b582454d89389370632ee12bf39d919cc5c561d1ca", - "sha256:1988b370536eb14f0ce7f3a4a5b422ab64c4e255b3f5d7752c5f583dc8c967fc", - "sha256:1a07c76a82390506ca0eabf57c0540cf5a60c993c442928fe4928472c4c6e5e6", - "sha256:1c2b104e81b3c3deba7e6f5bc1a9a0e9161c380530479970766a6655b8b77c7c", - "sha256:1c577cdcf8f92862363b3d598d971c6a84ed8f0bf824d4cc1ce70c2fb02acb4a", - "sha256:1f8605e573ed6c44ec689d94544b2c4bb1390aaa723a8b5a2cc0a5a485987a68", - "sha256:21778552ef3d44aac3278cc6f6d13a6423504fa5f09f2df34bfe489ed9ded7f5", - "sha256:2212296cdb63b092e295c3e4b4b442e7b7eb41e8a30d0f53c16d5962efed395d", - "sha256:222821c60b8f6a64c5908cb43d69c0ee978a1188f6a8433d4757d39231b42cdb", - "sha256:256ee6044214ee9d66d531bb374f065ee94e60667d6bbeaa25ca111fc3997158", - "sha256:2a384dfbe8bfebd203b778a30a712886d147c61943675f4719b56725a8bbe803", - "sha256:2fa643ca990323db68911b92f3f7a0ca9ae300ae340d0235de87c523601e58d9", - "sha256:41d8dab8c64ded1edf117d2a64f353efa096c52b853ef461aebd49abae979f16", - "sha256:440954ddc6b77257e67170d57b1026aa9545275c33312357472504eef7b4cc0b", - "sha256:47b4c2412960e64d97258f40616efddaebcb34ff664c8a972119ed38fac2a62c", - "sha256:4a9ce70f5e00380377aac0e568abd075266ff992be2e271765f7b35d228a990c", - "sha256:4dcb127ca3eb0a61205818a606393cbb60d93b7afb9accd2fd1e9081cc533144", - "sha256:4e9e9171d2fe6bfd9d3838a6fe63b1e91b55e0bf726c16edf265536e4eafed19", - "sha256:51d03e948e53b3639ce4d438f3d1d8202898ec6655cadcc09ec99229d4adc2a9", - "sha256:54b7f4a20d7cc6bfa4438abbde069d417bb7a119f870975f78a2b99890226d55", - "sha256:587237571a85716d6f71f60d103416c9df7d5acb55d96d3d3ced65f39bff9c0c", - "sha256:5951c328f9ac42d7bce7a6ded535879bc9ae13032818d036749631fa27777905", - "sha256:5a95151a5567b3b00368e99e9c5334a919514f60888a6b6d2054fea5e66e527e", - "sha256:5c12310d153b27aa630750be44e79313acc4e864c421eb7d2bc6fa3429c41bf8", - "sha256:5cd57ad998e3038aa87c38fe85c99ed728001bf5dde8eca121cadee06ee3f637", - "sha256:615348fab1a9ef7d0960a905e83ad39051ae9cb0d2837da739b5d3a7671e497a", - "sha256:67f7639424c313125213954e93a6229d3a1d386855d70c292a12628f600c7150", - "sha256:68164d43c580c2e8bf8e0eb4960142919d304052ccab92be10250a3a33b53268", - "sha256:68cc24f707ed9cb961f6ee04020ca01de2c89b2811f3cf3361dc7c96a14bfbcc", - "sha256:6b14c19172eb53b63931d3e62a9749d6519f7c121149493e6eefca055fcdb352", - "sha256:777e23609899cb230ad2642b4bdf1008890f84968be78de29099a8a86f10b261", - "sha256:786299d719eb5d868f161aeec56d589396b053925b7e0ce36e983d30d0a3e55c", - "sha256:7ccf1f0a304352c891d124ac1a9dea59b14b2abed1704aaa7689fc90ef9c5be1", - "sha256:88596384c3bec644a96ae46287bb646d6a23fa6014afe3799156aef42669c6bd", - "sha256:89b47c125ab07f0831803b88aeb12b04c564d5f07a1c1a225d4eb4d2f26e8b5e", - "sha256:8b0d058e4e425d3b45e8ec70d49b402f4d6b21041e674798b1f91ba027c73f28", - "sha256:8c81ff4afffef9b1186639506d70ea90888218f5ddfff03870e74ec80bb59970", - "sha256:8db9b749f589b5af8e4993623dbda6716b2b7a5fcb0fa2277bf3ce4b278c7059", - "sha256:8e5a26d7aac4c0d8414a347da162696eea0629fdce939ada6aedf951abb1d745", - "sha256:8fbf8c0ded367c5c8eaf585f85ca8dd85ff4d5b73fb8fe1e6ac9e1b5e62e11f7", - "sha256:93094eba50bc2ad4c40ff4997ead1fdcd41536116f2e7d6cfec9596a8ecb3615", - "sha256:9c186b270979fb1dee3ababe2d12fb243ed7da08b30abc83ebac3a928a4ddb15", - "sha256:9cb54f5725b4b37af12edf6c9e834df59258c82c15a244daa521a065fbb11717", - "sha256:9fbff00646cf8211b330690eb2fd64b23e1ce5b63a342436c1d1d6951d53d8dd", - "sha256:a57e73f9523e980f6101dc9a83adcd7ac0006ea8bf7937ca3870391c7bb4f8ff", - "sha256:a702bd3663b5cbf3916e84bf332400d24cdb18399f0877ca6b313ce6c08bfb43", - "sha256:a77c79bac8d908d839d32c212aef2354d2246eb9deb3e2cb01ffa83fb7a6ea5d", - "sha256:abda4009a30d51d3f06f36bc7411a62b3e647fa6cc935ef667e3e3d3a7dd09b1", - "sha256:b023b68c61ab0cd48bd38416b421464a62c381e32b9dc7b4bdfa2905807452a4", - "sha256:b07286a1090483799599a2f72f76ac396993da31f6e08efedb59f40876c144fa", - "sha256:b0de63ff0307eac3961b4af74382d30220d4813f36b7aaaf57f063a1243b4214", - "sha256:b7d5bb926805022508b7ddeaad957f1fce7a8d77532068d7bdb431056dc630cd", - "sha256:b9db600a86414a9a653e3c1c7f6a2f6a1894ab8f83d11505247bd1b90ad57157", - "sha256:b9fb6508893dc31cfcbb8191ef35abd79751db1d6871b3e2caee83959b4d91eb", - "sha256:bc3ea6ef2a83edad84bbdb5d96e22f587b67c68922cd7b6f9d8f24865e655bcf", - "sha256:bde0693073fd5e542e46ea100aa6c1a5d36282dbdbad85b1c3365d5421490a92", - "sha256:bf66149bb348d8e713f3a8e0b4f5b952094c2948c408e1cfef03b49e86745d60", - "sha256:bfe33cba6e127d0b5b417623c9aa621f0a69f304742acdca929a9fdab4593693", - "sha256:c8fb76214b5b739ce59e2236a6489d9dc3483649cfd6f563dbf5d8e40dbdd57d", - "sha256:cb8b79a65332e1a426ccb6290ce0409e1dc16b4daac1cc5761e059127fa3d134", - "sha256:d6bbe2c90c10382ca96df33b56e2060404a4f0f88673e1e84b44c8952517e5f3", - "sha256:d8311d0d690487359fe2247ec5d2cac9946e70d50dced8c01ce9e72341c21151", - "sha256:d8a8221a63602008550022aa3a4152ca357e1dde7ab3dd1da7e1925050b56863", - "sha256:de1a91d5faded9054957ed0a9e01b9d632109341942fc123947ced358c5d9009", - "sha256:df31641e3f02b77eb3c5fb63c0508bee0fc067cf153da0e002ebbb0db0b6d91a", - "sha256:e7168782621be4448d90169a60c8b37e9b0926b3b79b6097bc180c0a8a119e73", - "sha256:e7b55d9ede66af7feb6de87ff277e0ccf6d51c7db74cc39337fe3a0e31b5872d", - "sha256:e7dbf637f87dd315fa1f36aaed8afa929ee2c607454fb7791e74c88a0d94da59", - "sha256:f5293726943bdcea24715b121d8c4ae12581441d22623b0e6ab12d07ce85f9c4", - "sha256:f5dd109a925fee4c9ac3f6a094900461a2712df41745f5d04782ebcbe6479ccb", - "sha256:f6979b4f20d3e557a867da9d9227de4c156fcdcb348a5848e3e6190fd7feb972", - "sha256:f9f8beed277488a52ee2b459b23c4135e54d6a819eaba2e120e57311015b58e9" + "sha256:03c5a3143d4a82c43a3d82ac77d9cdef527a72f1c04dcca7b14770879f33d196", + "sha256:0fd1f57aac7d01c9c768675d531976d20d5b79d9da67fac87e55d41b4ade05f9", + "sha256:122c26f0976225aba46f381e3cabb5ef89a08af6503fc30493fb732e578cfa55", + "sha256:15b36a644d1f44ea3d94a0bbb71e75d5f394a3135dc388a209466e22b711ce64", + "sha256:1d697023b16c62f9aeb3ffdfb8ec4ac3afd477388993b9164b47dadbd60e7062", + "sha256:2015e4b40bd5dedc8155c2b2d24a2b07963ae02b5772373d0b599a68e38a316b", + "sha256:201ddf1471567568be381b6d4701e266a768f7eaa2f99ef753f2c9c5e1e3fb5c", + "sha256:23a5f97e7dd22e181967fb6cb6c3b11653b0fdbbc4bb7739d9b6052890ccab96", + "sha256:2ce101c447cf7ba4b6e5ab07bfa2c0da21cbab66922f78a601f0b84fd7710d72", + "sha256:2d64a5a7539320c3cecb4bca093ea825fcc906f8461cf8b42a7bf3c706ce1932", + "sha256:2e685afb0e3b7b861d89cb3690d89eeda221b43095352efddaaa735c6baf87f3", + "sha256:326fb5228aadfc395981d9b336d56a698da335897c4143105c73b583d7500839", + "sha256:347bbdc48411badc24fe3a13565820bc742db3aa2f9127cd5f48c256caf87e29", + "sha256:38bb515f1affc36d3d97b02bf82099925a5785c4a96066ff4400a83ad09d3d5d", + "sha256:394ddf9d216cf0bd429b223239a0ab628f01a7a1799c93ce4685eedcdd51b9bc", + "sha256:438c6e1492d060b21285f4b6675b941cf96dd9ef3dfdd59940561029b82e3e1f", + "sha256:45bb655cb8b3a61e19977183a4e0962051ae90f6d46588ed4addb8232128141c", + "sha256:46cc9069da466652bb7b8b3fac1f8ce2e12a9dc0fb11551faa420c4cdbc60abf", + "sha256:4ad284cee0fdcdc0216346b849fd53d201b510aff3c48aa3622daec9ada4bf80", + "sha256:4b34e5086e1ead3baa740e32adf35cc5e42338e44c4b07f7b62b41ca6d6a5bfd", + "sha256:50ac670f3fc13ce95e4d6d5a299db9288cc84c663aa630142444ef504756fcf7", + "sha256:5115490112f39f16ae87c1b34dff3e2c95306cf456b1d2af5974c4ac7d2d1ec7", + "sha256:58df59234be7d7e80548b9482ebfeafdda21948c25cb2873c7f23870c8053dfe", + "sha256:5c4a71d4a5e0cbfd4bfadd13cb84fe2bc76c64d550dc4f22c22008c9354cffb3", + "sha256:5cc75ff5efbd92301e63a157fddb18a6964a3f40e31c77d57e97dbb9bb3373b4", + "sha256:5f52225af7f91f27b633f73473e9ef0aa8e2112d57b69eaf3aa4479e3ea3bc0e", + "sha256:6075e27e7e54fbcd1c129c5699b2d251c885c9892e26d59a0fb7705141c2d14b", + "sha256:625a4a9d4b9f80e7bbaaf2ace06341cf701b2fee54232843addf0bb7304597fb", + "sha256:660ad010b8fd0b26e8edb8ae5c036db5b16baac4278198ad238b11956d920b3d", + "sha256:705c311ecf2d30fbcf3570d1a037c657be99095694223488140c47dee4ef2460", + "sha256:71944d4f4090afc07ce96b7029d5a574240e2f39570450df4af0d5b93a5ee64a", + "sha256:77071795efd6ba87f409001141fb05c94ee962b9fca6c8fa1f735c2718512de4", + "sha256:7d202ec55e61f06b1a1eaf317fba7546855cbf803c13ce7625d462fb8c88e238", + "sha256:81037ddda8cc0a95c6d8c1b9029d0b19a62db8770c0e239e3bea0109d294ab66", + "sha256:8593040bcc8075fc0e817a602bc5d3d74c7bd717619ffc175a8ba0188edebadf", + "sha256:8616dd5ed8b3b4029021b560305041c62e080bb28f238c27c2e150abe3539587", + "sha256:8a24ac7164a824ef2e8e4e9a9f6debb1f43c44ad7ad04efc6018a6610555666d", + "sha256:8ba0fbc56c44883bd757ece433f9caadbca67f565934afe9bc53ba3bd99cc368", + "sha256:92021bf0a4b9ad16851a6c1ca3c86e5b09aecca4f7a2576430c6bbf3114922b1", + "sha256:938e37fd337343c67471098736deb33066d72cec7d8927b9c1b6b4ea807ade9e", + "sha256:93a19cd1e9dc703257fda78b8e889c3a08eabaa09f6ff0d867850b03964f80d1", + "sha256:93ee83008d3e505db9846a5a1f48a002676d8dcc90ee431a9462541c9b81393c", + "sha256:93f1a0e12c321d923c024b56d7dcd8012e60bf30a4b3fb69a88be15dcb9ab80b", + "sha256:96b2e7c110a941c8c1a692703b8ac1013e47f17ee03356c71d55c0a54de2ce38", + "sha256:9b58b2ef7f28a2462ba86acbf3b20371bd80a1faa1cfd82f31968af4ac81ef25", + "sha256:9bcdd19398212785a9cb82a63a4b75a299998343f3f5732dfd37c1a4275463f9", + "sha256:9d7958ba22854b3f00a7bbb66cde1dc759760ce8a3e6dfe9ea53f06bccaa9aa2", + "sha256:9dc26781fb95225c6170619dece8b5c6ca7cfb1b0be97b7ee719915773d0c2a9", + "sha256:9eb3df1aa83602be9a5e572c834d74c3c8e382208b59a873aabfe4c493c45ed0", + "sha256:abd5673e3391564871ba6753cf674dcf2051ef19dc508998fe0758a6c7b429a0", + "sha256:b06e1a66bf0a1a2d0f12aef25843dfd2093df080d6c1acbc43914bb9c8f36ed3", + "sha256:b71722b527445e02168e2d1cf435772731874671a647fa159ad000feea7933b6", + "sha256:b95e1694d234f27b4bbf5bdef56bb751974ac5dbe045b1e462bde1fe39421cbe", + "sha256:bf61884a604c399458c4a42c8caea000fbcc44255ed89577ff50cb688a0fe8e2", + "sha256:c031de4dfabe7bb6565743745ab43d20588944ddfc7233360169cab4008eee2f", + "sha256:c253e81f12da97f85d45441e8c6da0d9c12e07db4a7136b0a955df6fc5e4bf51", + "sha256:c2f9f07fe6d0d51bd2a788cbb339f1570fd691449c53b5dec83ff838f117703e", + "sha256:c3fd3b8f0164fb2866400cd6eb9e884ab0dc95f882cf8b25e560ace7350c552d", + "sha256:c774f08afecc0a617966f45a9c378456e713a999ee60654d9727617def3e4ee4", + "sha256:c9e9e9a51dd12f2f71fdbd7f7230dcb75ed8f77d8ac8e07c73b599b6d7027e5c", + "sha256:d0665e2a346b6b66959f831ffffd8aa71dd07dd2300017d478f5b47573e66cfe", + "sha256:dc990e73613c78ab2930b60266135066f37fdfce6b32dd604f42c5c377ee880a", + "sha256:dca39391f45fbb28daa6412f98c625265bf6b512cc41382df61672d1b242f8f4", + "sha256:dd33f4d571b4143fc9318c3d9256423579c7d183635acc458a6db81919ae5204", + "sha256:dfe48f477e02ef5ab247c6ac431a6109c69b5c24cb3ccbcd3e27c4fb39691fe4", + "sha256:e5991b80886655e6c785aadf3114d4f86e6bec2da436e2bb62892b9f048450a4", + "sha256:e99bf118afb2584848dba169a685fe092b338a4fe52ae08c7243d7bc4cc204fe", + "sha256:eb898c9ad5a1228a669ebe2e2ba3d76aebe1f7c10b78f09a36000254f049fc2b", + "sha256:ebddbfea8a8d6b97f717658fa85a96681a28990072710d3de3a4eba5d6804a37", + "sha256:f6acd1a908740f708358d240f9a3243cec31a456e3ded65c2cb46f6043bc6735", + "sha256:f6fe78b51852e25d4e20be51ef88c2a0bf31432b9f2223bdbd61c01a0f9253a7", + "sha256:fc98d93d11d860ac823beb6131f292d82efb76f226b5e28a3eab1ec578dfd041", + "sha256:fe4d63f42d9c604521b208b754abfafe01218af4a8f6332b43196ee8fe88bbd5", + "sha256:fef7b7bd3a6911b4d148332136d34d3c2aee3d54d354373b1da6d96bc08089a5", + "sha256:ff371ae72a1816c3eeba5c9cff42cb739aaa293fec7d78f180d1c7ee342285b6", + "sha256:fff8606149098935188fe1e135f7e7991e6a36d6fe394fd15939fc57d0aff889" ], "index": "pypi", - "version": "==3.10.1" + "version": "==3.10.4" }, "aiosignal": { "hashes": [ @@ -148,11 +148,11 @@ }, "attrs": { "hashes": [ - "sha256:377b47448cb61fea38533f671fba0d0f8a96fd58facd4dc518e3dac9dbea0905", - "sha256:adbdec84af72d38be7628e353a09b6a6790d15cd71819f6e9d7b0faa8a125745" + "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" ], "markers": "python_version >= '3.7'", - "version": "==24.1.0" + "version": "==24.2.0" }, "beautifulsoup4": { "hashes": [ @@ -494,6 +494,17 @@ "index": "pypi", "version": "==1.1" }, + "i18nice": { + "extras": [ + "yaml" + ], + "hashes": [ + "sha256:28fa4563390116dd6b16f638c400595d35ce55cd35486dd743daa8a3d31b7d2c", + "sha256:baec8d78aa3f8cf4a928586abe5ee369f93008bea59dd69c273ef827611847a5" + ], + "index": "pypi", + "version": "==0.15.5" + }, "idna": { "hashes": [ "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", @@ -504,151 +515,147 @@ }, "lxml": { "hashes": [ - "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3", - "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a", - "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0", - "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b", - "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f", - "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6", - "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73", - "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d", - "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad", - "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b", - "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a", - "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5", - "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab", - "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316", - "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6", - "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df", - "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca", - "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264", - "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8", - "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f", - "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b", - "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3", - "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5", - "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed", - "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab", - "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5", - "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726", - "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d", - "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632", - "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706", - "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8", - "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472", - "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835", - "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf", - "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db", - "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d", - "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545", - "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9", - "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be", - "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe", - "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905", - "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438", - "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db", - "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776", - "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c", - "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed", - "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd", - "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484", - "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d", - "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6", - "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30", - "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182", - "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61", - "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425", - "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb", - "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1", - "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511", - "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e", - "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207", - "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b", - "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585", - "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56", - "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391", - "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85", - "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147", - "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18", - "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1", - "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa", - "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48", - "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3", - "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184", - "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67", - "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7", - "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34", - "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706", - "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8", - "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c", - "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115", - "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009", - "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466", - "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526", - "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d", - "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525", - "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14", - "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3", - "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0", - "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b", - "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1", - "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f", - "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf", - "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf", - "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0", - "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b", - "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff", - "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88", - "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2", - "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40", - "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716", - "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2", - "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2", - "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a", - "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734", - "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87", - "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48", - "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36", - "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b", - "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07", - "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c", - "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573", - "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001", - "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9", - "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3", - "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce", - "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3", - "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04", - "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927", - "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083", - "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d", - "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32", - "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9", - "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f", - "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2", - "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c", - "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d", - "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393", - "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8", - "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6", - "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66", - "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5", - "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97", - "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196", - "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836", - "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae", - "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297", - "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421", - "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6", - "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981", - "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30", - "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30", - "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f", - "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324", - "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b" + "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e", + "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229", + "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3", + "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5", + "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70", + "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15", + "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", + "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", + "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22", + "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf", + "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22", + "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", + "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727", + "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e", + "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", + "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f", + "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f", + "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", + "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", + "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de", + "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875", + "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42", + "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e", + "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6", + "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391", + "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc", + "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b", + "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237", + "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4", + "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86", + "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f", + "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a", + "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8", + "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f", + "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903", + "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03", + "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", + "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", + "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7", + "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab", + "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", + "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22", + "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", + "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b", + "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3", + "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be", + "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469", + "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", + "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a", + "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c", + "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a", + "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", + "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94", + "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442", + "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", + "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84", + "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", + "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9", + "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", + "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be", + "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", + "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", + "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21", + "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa", + "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16", + "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d", + "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe", + "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83", + "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba", + "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040", + "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763", + "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8", + "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", + "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2", + "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a", + "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b", + "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce", + "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c", + "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577", + "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", + "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71", + "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512", + "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540", + "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", + "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2", + "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", + "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce", + "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e", + "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2", + "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27", + "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1", + "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d", + "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1", + "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330", + "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920", + "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99", + "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff", + "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18", + "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", + "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c", + "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", + "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080", + "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19", + "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d", + "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70", + "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32", + "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", + "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2", + "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79", + "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", + "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5", + "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", + "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", + "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", + "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b", + "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753", + "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9", + "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", + "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033", + "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", + "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656", + "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab", + "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", + "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", + "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", + "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", + "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11", + "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c", + "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", + "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005", + "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654", + "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80", + "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e", + "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec", + "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7", + "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965", + "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945", + "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8" ], "index": "pypi", - "version": "==5.2.2" + "version": "==5.3.0" }, "marshmallow": { "hashes": [ @@ -999,6 +1006,64 @@ "markers": "python_version >= '3.8'", "version": "==1.0.1" }, + "pyyaml": { + "hashes": [ + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "version": "==6.0.2" + }, "redis": { "hashes": [ "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870", @@ -1025,11 +1090,11 @@ }, "soupsieve": { "hashes": [ - "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", - "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" + "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", + "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9" ], "markers": "python_version >= '3.8'", - "version": "==2.5" + "version": "==2.6" }, "spotipy": { "hashes": [ @@ -1049,65 +1114,76 @@ }, "time-machine": { "hashes": [ - "sha256:037ff158179517fa9ae045c5ac8e995a4d465660f4d4b53510630e2ab2aa4eab", - "sha256:03dcbda69bdc1186fe93e5fc095493e577ecf82390bb6b86d2a445727c3e722d", - "sha256:0573432aadc97b07e2be6756476e9ba3f5864aa4453c473a03da72ae8b6c5145", - "sha256:098b709455bc9f95e5cc42a2cf42373a4f2aa3f6d5e79e4fe9a7c3f44834cdb7", - "sha256:10c7cf6134e32e1074d37319f8b7662cc200ee9dd813a48b7520dd4aa49131a9", - "sha256:134ec3c5050ddbc6926da11a17c2d632cef8bb3f164098084f6f267f913c9304", - "sha256:146aee86d237aa3a0ad1287718f1228107d21f3cd775c40f121a4670b3dee02c", - "sha256:1a6627ce920f1b4b73b2a4957e53f2740d684535af6924f62085005e6e3181cb", - "sha256:1bbbb04a8e5f0381b75847c96356c7b55348bfac54bee024bd61dfbf33176c11", - "sha256:1c6e9b6df0e6ab34776e04ce936f1f6099e8d3983ce0cc60aca2d3cf2d5ef27b", - "sha256:1ea4319010914c8d69bd16d9839a5c2f1df104b5a4704882bc44599d81611582", - "sha256:22db0f8af1686b5d96be39dd21ddb7de13caf5a45f3fca6c41d61007e08c0eb0", - "sha256:24034c253b37c125842cf9bbd112786c4381a067b1c1cb224615688101066f5f", - "sha256:25edfd2d8c62cbe25ea2c80463c4ab7e3386792a7fe0d70909d52dbfc9aa4c6d", - "sha256:27d12a3eaca2f7b10da33774a8edd3a6b97358a3bed9ffecefc88d7e3d7b5f5f", - "sha256:2f2eb7ccf5f1c706f335a998ce8b009b3f968d625a4ffcf1b16ddef38fa283bc", - "sha256:30d1e3c18e7dcf5981e7e0fa3ed8b4bfbe6b1dc430442838283455049996f9e0", - "sha256:32b5b44372d1f025b4fcc4209cbdc5d3e10a3e07a8334b297bb0ba4a827906e4", - "sha256:34c35287b6667a6c233ed0658649d52854858bb6a8ee30d2aa680bf2288a166d", - "sha256:3f985a98704e81e0183043db5889f17fa68daea1ad230e9c8feb3bb303a518c1", - "sha256:4386f303a4b4bc12d3b0266e88deb64c11109474ad32ba71c18bc4812cbb3e1f", - "sha256:51a0b17ddd29e7106f84db7539f6a92153c3617754f691c851af6b1cf524f60c", - "sha256:55f373873583c93e2107e4e9e4db4cb4d637df75d82c57aaa6349c4993305b77", - "sha256:576179845483203182e4d423db1c6c27b3a8b569a3e3df9980a785adefc3ef6f", - "sha256:603fb67082f1795f1bd352dccad5c6884e56cfb7a115ac6edb03bb9434ec5698", - "sha256:6327866c00c64ce1c18b1c0444e61bd65c267d4929d2be787fa11da0455823c3", - "sha256:69428e17e2b9ab04ccbd178f18aedbb4fa4e7f53807ee067fe3c55fca286a6df", - "sha256:6e5150cdf1e128c4b3bea214204b4d7747456d9c7ce8e3d83c204e59f9640b72", - "sha256:71f42b2257ce71ce9b90320072e327edeeb6368ccd0602acd979033e172df656", - "sha256:720071c6fd7edae7149dc3b336de0bfb03d4fb66b13abd96e6145c4bef7c1b40", - "sha256:76004bd92f23e3863ace7fd4ac0751134ea13953ec11bd8f47a8fec1f8dc89ff", - "sha256:7726801fa7d744fb0faab7131bf2a6bd2c56e2cf01c7215cfef6987968652392", - "sha256:826a3608420e08f0c4bc404dce6141d8ec80d3729e0278a6e0d5ae4532f76247", - "sha256:875456bb4389112e1e827492cb47965910fa2dfe00c4d521670baf0125d7a454", - "sha256:8cca04142f39564722648b03ad061c411b6a83f01549c59248d604f2ac76789b", - "sha256:9219e488ab0637120ebbfb2183e1c676f3de79ce6b11666ec0383d71e82803be", - "sha256:93ad7844a67ae29043b78ab3148d0fa59f00e68f762eb8982110ac27f684dd62", - "sha256:a665fa8f4484850c8df0d33edaa781b37a7cd2d615479f0e5467599a49e5f6c0", - "sha256:a8293386d8ac68ecf6a432f8c2ca7251e108e160093954b14225dbed856c0d55", - "sha256:c1076e8435f27f25e55c659cf0de9a20ffc12265a1f8e00641512fb023c60fab", - "sha256:c2e8a877c1c2a39011979680bbd44b05e2d7fef45000cdcef3f1b7c1c56d53de", - "sha256:c2f05834faf501fa14d5a0318f736965b7ea58dd3a11c22bf8e9eca4889d5955", - "sha256:c80664830c774d60e26a267bc25c59151f281b2befc1b40a7526fc7633286401", - "sha256:ca63bd68fe1b31a1135c535bb579dd96ddaa1f802d9cbf638cc344f18701575f", - "sha256:caaf7700e6b47799c94bf4b4fb9b5cc067f463ec29f5fdc38a66628e3b062a4c", - "sha256:cb6f03ae4ee4c854d1534768fb579d4ca6b680373ad8ab35cc9008289c9efec9", - "sha256:cc19096db9465905662d680b1667cbe37c4ca9cdfbeb30680d45687fdc449c14", - "sha256:e24f8b526c1f1c17b478fe68360afba8a609c3547b7a51e0ca350ac8a2959961", - "sha256:e3b76ef7c02bbf3dce58a7c4a5c73ed919483a946150e7dda89ea1be0314811c", - "sha256:f1bc051f7a3204fb8aceac0f4aa01bdc3a5c936dd0d7334ae1b791862ced89b3", - "sha256:f4c5ff83704abbc48083e899df712861d0acd31abe6b0f1f0795e1b15f521c90", - "sha256:f739a7660a97869333ff960e7e03c6047910e19bccc3adc86954050ec9c8e074", - "sha256:f9c5d5b8a8667d85a37f07c0b6f85fa551fb65e8b6e647b2dee29c517a249f0c", - "sha256:fa488e27fb6f7efbfbb41586533963cebff3ce396b3e8cd7b013ed30e4f830df", - "sha256:faa7c67a1dafa29d17ca098b61a717419dd5c7ebb21f4f644f4a859983013273", - "sha256:fda6fc706a2d78cc8688018d17fb52ea80169fb9fd0f70642d218bd676049f9d" + "sha256:008bd668d933b1a029c81805bcdc0132390c2545b103cf8e6709e3adbc37989d", + "sha256:014589d0edd4aa14f8d63985745565e8cbbe48461d6c004a96000b47f6b44e78", + "sha256:0302568338c8bd333ed0698231dbb781b70ead1a5579b4ac734b9bf88313229f", + "sha256:0630a32e9ebcf2fac3704365b31e271fef6eabd6fedfa404cd8dbd244f7fc84d", + "sha256:09fd839a321a92aa8183206c383b9725eaf4e0a28a70e4cb87db292b352eeefb", + "sha256:0b2d28daf4cabc698aafb12135525d87dc1f2f893cbd29a8a6fe0d8d36d1342c", + "sha256:1168eebd7af7e6e3e2fd378c16ca917b97dd81c89a1f1f9e1daa985c81699d90", + "sha256:18fc4740073e67071472c48355775ec6d1b93af5c675524b7de2474e0dcd8741", + "sha256:1dee3a0dd1866988c49a5d00564404db9bcdf49ca92f9c4e8b6c99609d64e698", + "sha256:245ef73f9927b7d4909d554a6a0284dbc5dee9730adea599e430b37c9e9fa203", + "sha256:29b988b1f09f2a083b12b6b054787b799ae91ee15bb0e9de3e48f880e4d68674", + "sha256:31af56399bf7c9ef76a3f7b6d9471dffa8f06ee373c194a374b69523f9061de9", + "sha256:3862dda89bdb05f9d521b08fdcb24b19a7dd9f559ae324f4301ba7a07b6eea64", + "sha256:3b177d334a35bf2ce103bfe4e0e416e4ee824dd33386ea73fa7491c17cc61897", + "sha256:3f7eadd820e792de33a9ec91f8178a2b9088e4e8b9a166953419ddc4ec5f7cfe", + "sha256:4428bdae507996aa3fdeb4727bca09e26306fa64a502e7335207252684516cbf", + "sha256:4601fe7a6b74c6fd9207e614d9db2a20dd4befd4d314677a0feac13a67189707", + "sha256:4cd9f057457d12604be18b623bcd5ae7d0b917ad66cb510ee1135d5f123666e2", + "sha256:4e83fd6112808d1d14d1a57397c6fa3bd71bb2f3b8800036e12366e3680819b9", + "sha256:52468a0784544eba708c0ae6bc5e8c5dcfd685495a60f7f74028662c984bd9cd", + "sha256:5d4073b754f90b19f28d036ec5143d3fca3a75e4d4241d78790a6178b00bb373", + "sha256:5f7add997684bc6141e1c80f6ba0c38ffe316ba277a4074e61b1b7b4f5a172bf", + "sha256:5ff655716cd13a242eef8cf5d368074e8b396ff86508a5933e7cff4f2b3eb3c2", + "sha256:617c9a92d8d8f60d5ef39e76596620503752a09f834a218e5b83be352fdd6c91", + "sha256:6425001e50a0c82108caed438233066cea04d42a8fc9c49bfcf081a5b96e5b4e", + "sha256:658ea8477fa020f08435fb7277635eb0b50cd5206b9d4cbe10e9a5466b01f855", + "sha256:65d395211736d9844537a530287a7c64b9fda1d353e899a0e1723986a0859154", + "sha256:660810cd27a8a94cb5e845e8f28a95e70b01ff0c45466d394c4a0cba5a0ae279", + "sha256:671e88a6209a1cf415dc0f8c67d2b2d3b55b436cc63801a518f9800ebd752959", + "sha256:674097dd54a0bbd555e7927092c74428c4c07268ad52bca38cfccc3214707e50", + "sha256:6f021aa2dbd8fbfe54d3fa2258518129108b7496922b3bcff2cf5991078eec67", + "sha256:704abc7f3403584cca9c01c5809812e0bd70632ea4251389fae4f45e11aad94f", + "sha256:73a8c8160d2a170dadcad5b82fb5ee53236a19cec0996651cf4d21da0a2574d5", + "sha256:768d33b484a35da93731cc99bdc926b539240a78673216cdc6306833d9072350", + "sha256:79bf1ef6850182e09d86e61fa31717da56014a3b2234afb025fca1f2a43ac07b", + "sha256:838a6d117739f1ae6ecc45ec630fa694f41a85c0d07b1f3b1db2a6cc52c1808b", + "sha256:8817b0f7d7830215261b18db83c9c3ef1da6bb64da5c292d7c70b9a46e5a6745", + "sha256:892d016789b59950989b2db188dcd46cf16d34e8daf2343e33b679b0c5fd1001", + "sha256:899f1a856b3bebb82b6cbc3c0014834b583b83f246b28e462a031ec1b766130b", + "sha256:8c2b1c91b437133c672e374857eccb1dd2c2d9f8477ae3b35138382d5ef19846", + "sha256:9479530e3fce65f6149058071fa4df8150025f15b43b103445f619842981a87c", + "sha256:95c8e7036cf442480d0bf6f5fde371e1eb6dbbf5391d7bdb8db73bd8a732b538", + "sha256:97dc6793e512a62ba9eab250134a2e67372c16ae9948e73d27c2ef355356e2e1", + "sha256:9a6a9342fae113b12aab42c790880c549d9ba695b8deff27ee08096eedd67569", + "sha256:a22f47c34ee1fcf7d93a8c5c93135499aac879d9d5d8f820bd28571a30fdabcd", + "sha256:a731c03bc00552ee6cc685a59616d36003124e7e04c6ddf65c2c47f1c3d85480", + "sha256:b095a1de40ca1afaeae8df3f45e26b645094a1912e6e6871e725fcf06ecdb74a", + "sha256:b48abd7745caec1a78a16a048966cde14ff6ccb04d471a7201532648d3f77d14", + "sha256:b5f3ab4185c1f72010846ca9fccb08349e23a2b52982a18d9870e848ce9f1c86", + "sha256:b684f8ecdeacd6baabc17b15ac1b054ca62029193e6c5367ef00b3516671de80", + "sha256:b7b647684eb2e1fd1e5e6b101249d5fe9d6117c117b5e336ad8dd75af48d2d1f", + "sha256:bcbb25029ee8756f10c6473cea5ef21707a1d9a8752cdf29fad3a5f34aa4a313", + "sha256:c0473dfa8f17c6a9a250b2bd6a5b62af3aa7d22518f701649115f1085d5e35ab", + "sha256:c08800c28160f4d32ca510128b4e201a43c813e7a2dd53178fa79ebe050eba13", + "sha256:c344eb09fcfbf71e5b5847d4f188fec98e1c3a976125ef571eac5f1c39e7a5e5", + "sha256:c596920d6017702a36e3a43fd8110a84e87d6229f30b84bd5640cbae9b5145da", + "sha256:c947135750d20f35acac290c34f1acf5771fc166a3fbc0e3816a97c756aaa5f5", + "sha256:d24d2ec74923b49bce7618e3e7762baa6be74e624d9829d5632321de102bf386", + "sha256:d828721dcbcb94b904a6b25df67c2513ecd24cd9e36694f38b9f0fa71c7c6103", + "sha256:ddad27a62df2ea47b7b483009fbfcf167a71d702cbd8e2eefd9ddc1c93146658", + "sha256:df6f618b98f0848fd8d07039541e10f23db679d8283f8719e870a98e1ef8e639", + "sha256:e1790481a6b9ce38888f22ce30710244067898c3ac4805a0e061e381f3db3506", + "sha256:e6776840aea3ff5ab6924b50117957da62db51b109b3b491c0d5817a804b1a8e", + "sha256:e99689f6c6b9ca6e2fc7a75d140e38c5a7985dab61fe1f4e506268f7e9844e05", + "sha256:ebd2e63baa117ded04b978813fcd1279d3fc6be2149c9cac75c716b6f1db774c", + "sha256:f50f10058b884d45cd8a50423bf561b1f9f9df7058abeb8b318700c8bcf4bb54", + "sha256:f5b94cba3edfc54bcb3ab5be616a2f50fa48be438e5af970824efdf882d1bc31" ], "markers": "implementation_name != 'pypy'", - "version": "==2.14.2" + "version": "==2.15.0" }, "tqdm": { "hashes": [ @@ -1324,11 +1400,11 @@ }, "yt-dlp": { "hashes": [ - "sha256:4318aa523694611562f01419c8d526b662a72df34ef8ba454016b34c8366c158", - "sha256:d0d927038e30a05f6eab26ff6189628456ea21bb159a3d9dc2e855eef2810eac" + "sha256:ab507ff600bd9269ad4d654e309646976778f0e243eaa2f6c3c3214278bb2922", + "sha256:e8551f26bc8bf67b99c12373cc87ed2073436c3437e53290878d0f4b4bb1f663" ], "index": "pypi", - "version": "==2024.8.1" + "version": "==2024.8.6" } }, "develop": { diff --git a/sausage_bot/__main__.py b/sausage_bot/__main__.py index 0b7b46c..4851ea8 100755 --- a/sausage_bot/__main__.py +++ b/sausage_bot/__main__.py @@ -3,6 +3,7 @@ "Set's up the bot, have a few generic commands and controls cogs" import discord from discord.ext import commands +from discord.app_commands import locale_str import os import asyncio from tabulate import tabulate @@ -10,9 +11,25 @@ from sausage_bot.util.args import args from sausage_bot.util import config, envs, file_io, cogs, db_helper from sausage_bot.util import discord_commands +from sausage_bot.util.i18n import I18N, available_languages, set_language +from sausage_bot.util.i18n import MyTranslator from sausage_bot.util.log import log +async def locales_autocomplete( + interaction: discord.Interaction, + current: str, +) -> list[discord.app_commands.Choice[str]]: + locales = available_languages() + log.debug(f'locales: {locales}') + return [ + discord.app_commands.Choice( + name=locale, value=locale + ) + for locale in locales if current.lower() in locale.lower() + ] + + # Create necessary folders before starting check_and_create_folders = [ envs.DB_DIR, @@ -42,10 +59,27 @@ async def on_ready(): When the bot is ready, it will notify in the log. #autodoc skip# ''' + # Create locale db if not exists + log.verbose('Checking locale db') + await db_helper.prep_table( + table_in=envs.locale_db_schema, + old_inserts=['en'] + ) + locale_db = await db_helper.get_output( + template_info=envs.locale_db_schema, + single=True + ) + log.debug(f'Setting locale to `{locale_db}`') + I18N.set('locale', locale_db) + await config.bot.tree.set_translator(MyTranslator()) for guild in config.bot.guilds: if guild.name == config.env('DISCORD_GUILD'): - log.log('{} has connected to `{}`'.format( - config.bot.user, guild.name)) + log.log( + I18N.t('main.msg.bot_connected', + bot=config.bot.user, + server=guild.name + ) + ) break log.verbose('Checking cog tasks db') @@ -68,7 +102,7 @@ async def on_ready(): type=discord.ActivityType.watching, name=config.env( 'BOT_WATCHING', - default='some random youtube video' + default=I18N.t('main.msg.bot_watching') ) ) ) @@ -85,15 +119,22 @@ async def on_ready(): } channel_out = await guild.create_text_channel( name=str(bot_channel), - topic=f'Incoming log messages from {config.bot.user.name}', + topic=I18N.t('main.msg.create_log_channel_logging', botname=config.bot.user.name), overwrites=overwrites ) channel_out.set_permissions() +sync_group = discord.app_commands.Group( + name="sync", description=locale_str( + I18N.t('stats.commands.groups.stats') + ) +) + + @commands.check_any(commands.is_owner()) -@config.bot.tree.command( - name='syncglobal', description='Owner only' +@sync_group.command( + name='global', description=locale_str(I18N.t('main.owner_only')) ) async def sync_global(interaction: discord.Interaction): await config.bot.tree.sync() @@ -108,6 +149,7 @@ async def sync_global(interaction: discord.Interaction): if _cmd != '': _cmd += '\n' await interaction.response.send_message( + # TODO i18n f'Commands synched!\n{_cmd}', ephemeral=True ) @@ -116,7 +158,7 @@ async def sync_global(interaction: discord.Interaction): @commands.is_owner() @config.bot.tree.command( - name='syncdev', description='Owner only' + name='syncdev', description=locale_str(I18N.t('main.owner_only')), ) async def sync_dev(interaction: discord.Interaction): await interaction.response.defer(ephemeral=True) @@ -180,7 +222,7 @@ async def clear_commands(ctx): @commands.is_owner() @config.bot.tree.command( - name='version', description='Owner only' + name='version', description=locale_str(I18N.t('main.owner_only')) ) async def get_version(interaction: discord.Interaction): await interaction.response.defer(ephemeral=True) @@ -200,7 +242,7 @@ async def get_version(interaction: discord.Interaction): # Commands @commands.is_owner() @config.bot.tree.command( - name='ping', description='Sjekk latency' + name='ping', description=locale_str(I18N.t('main.commands.ping.command')) ) async def ping(interaction: discord.Interaction): 'Checks the bot latency' @@ -216,22 +258,22 @@ async def ping(interaction: discord.Interaction): ) @config.bot.tree.command( name='delete', - description='Delete `amount` number of messages in the chat' + description=locale_str(I18N.t('main.commands.delete.command')) ) async def delete(interaction: discord.Interaction, amount: int): 'Delete `amount` number of messages in the chat' if amount <= 0: await interaction.response.send_message( - 'The number must be bigger than 0', - ephemeral=True + I18N.t('main.commands.delete.less_than_0'), ) else: await interaction.response.defer(ephemeral=True) await interaction.channel.purge( - limit=amount, reason='Massesletting via bot' + limit=amount, reason=I18N.t('main.commands.delete.log_confirm') ) await interaction.followup.send( - f'Deleted {amount} messages', + I18N.t('main.commands.delete.msg_confirm', + amount=amount), ephemeral=True ) return @@ -243,7 +285,7 @@ async def delete(interaction: discord.Interaction, amount: int): ) @config.bot.tree.command( name='kick', - description='Kick a user with reason' + description=locale_str(I18N.t('main.commands.kick.command')) ) async def kick( interaction: discord.Interaction, member: discord.Member = None, @@ -263,12 +305,18 @@ async def kick( try: await member.kick(reason=reason) await interaction.followup.send( - f'{member} has been kicked', + I18N.t( + 'main.commands.kick.msg_confirm', + member=member + ), ephemeral=True ) - except Exception as failkick: + except Exception as _error: await interaction.followup.send( - f'Failed to kick: {failkick}', + I18N.t( + 'main.commands.kick.msg_failed', + error=_error + ), ephemeral=True ) @@ -279,7 +327,7 @@ async def kick( ) @config.bot.tree.command( name='ban', - description='Ban a user with reason' + description=locale_str(I18N.t('main.commands.ban.command')) ) async def ban( interaction: discord.Interaction, member: discord.Member = None, @@ -299,20 +347,25 @@ async def ban( try: await member.ban(reason=reason) await interaction.followup.send( - f'{member} has been banned', + I18N.t( + 'main.commands.ban.msg_confirm', + member=member, + ), ephemeral=True ) - except Exception as failban: + except Exception as _error: await interaction.followup.send( - f'Failed to ban: {failban}', + I18N.t( + 'main.commands.ban.msg_failed', + error=_error + ), ephemeral=True ) @commands.check_any(commands.is_owner()) @config.bot.tree.command( - name='say', description='Sender melding til en kanal. Fyll inn ' - '`message_id` hvis det skal svares på en melding' + name='say', description=locale_str(I18N.t('main.commands.say.command')) ) async def say( interaction: discord.Interaction, channel: discord.TextChannel, @@ -331,17 +384,27 @@ async def say( else: await channel.send(message) await interaction.followup.send( - f'Melding sent til `#{channel.name}`', ephemeral=True + I18N.t( + 'main.commands.say.msg_confirm', + channel=channel.name + ), + ephemeral=True ) except discord.Forbidden: await interaction.followup.send( - 'Jeg har ikke tilgang til å sende melding ' - f'i `#{channel.name}`.', + I18N.t( + 'main.commands.say.msg_forbidden', + channel=channel.name + ), ephemeral=True ) - except Exception as e: + except Exception as _error: await interaction.followup.send( - f'An error occurred: {e}', ephemeral=True + I18N.t( + 'main.commands.say.msg_error', + error=_error + ), + ephemeral=True ) @@ -351,12 +414,13 @@ async def say( ) @config.bot.tree.command( name='sayagain', - description='Endre en tidligere melding sendt med /say' + description=locale_str(I18N.t('main.commands.say_again.command')) ) async def say_again( interaction: discord.Interaction, msg_id: str, *, text: str ): 'Make the bot rephrase a previous message' + # TODO Legg til modal her await interaction.response.defer(ephemeral=True) guild = discord_commands.get_guild() if guild is None: @@ -384,7 +448,7 @@ async def say_again( commands.has_permissions(administrator=True) ) @config.bot.tree.command( - name="tasks", description="List tasks and their status" + name="tasks", description=locale_str(I18N.t('main.commands.tasks.command')) ) async def get_tasks_list(interaction: discord.Interaction): ''' @@ -410,7 +474,41 @@ async def get_tasks_list(interaction: discord.Interaction): return +@commands.check_any(commands.is_owner()) +@config.bot.tree.command( + name='language', description=locale_str(I18N.t('main.owner_only')) +) +@discord.app_commands.autocomplete(language=locales_autocomplete) +async def language( + interaction: discord.Interaction, language: str +): + await interaction.response.defer(ephemeral=True) + log.verbose(f'Setting language to {language}') + await set_language(language) + log.verbose('Syncing commands') + await config.bot.tree.sync() + await interaction.followup.send( + I18N.t( + 'main.commands.language.confirm_language_set', + language=language + ), + ephemeral=True) + return + + +@commands.check_any(commands.is_owner()) +@config.bot.tree.command( + name='test', description=locale_str(I18N.t('main.owner_only')) +) +async def test(interaction: discord.Interaction): + await interaction.response.send_message( + I18N.t('dilemmas.commands.count.msg_confirm', count=0), + ephemeral=True + ) + return + + try: config.bot.run(config.DISCORD_TOKEN) -except Exception as e: - log.error(f'Could not start bot: {e}') +except Exception as _error: + log.error(f'Could not start bot: {_error}') diff --git a/sausage_bot/cogs/autoevent.py b/sausage_bot/cogs/autoevent.py index 0680d78..352c918 100755 --- a/sausage_bot/cogs/autoevent.py +++ b/sausage_bot/cogs/autoevent.py @@ -4,8 +4,9 @@ This cog can take links to soccer games on predefined sites, and make them into an event for the server. ''' -from discord.ext import commands import discord +from discord.ext import commands +from discord.app_commands import locale_str, describe import os import re import typing @@ -13,6 +14,7 @@ from sausage_bot.util import envs, config, datetime_handling, net_io from sausage_bot.util import discord_commands +from sausage_bot.util.i18n import I18N from sausage_bot.util.log import log @@ -45,14 +47,15 @@ async def event_names_autocomplete( class AutoEvent(commands.Cog): '#autodoc skip#' + def __init__(self, bot): self.bot = bot super().__init__() group = discord.app_commands.Group( - name="autoevent", - description='Administer match events on the ' - 'discord server based on a url from a supported website.' + name="autoevent", description=locale_str( + I18N.t('autoevent.commands.autoevent.cmd') + ) ) @commands.check_any( @@ -60,7 +63,15 @@ def __init__(self, bot): commands.has_permissions(manage_events=True) ) @group.command( - name="add", description="Add a scheduled event" + name="add", description=locale_str( + I18N.t('autoevent.commands.add.cmd') + ) + ) + @describe( + url=I18N.t('autoevent.commands.add.desc.url'), + channel=I18N.t('autoevent.commands.add.desc.channel'), + text=I18N.t('autoevent.commands.add.desc.text'), + event_image=I18N.t('autoevent.commands.add.desc.event_image') ) async def event_add( self, interaction: discord.Interaction, url: str, @@ -69,23 +80,12 @@ async def event_add( ): ''' Add a scheduled event - - Parameters - ------------ - url: str - URL to match page from nifs, vglive or tv2.no/livesport - channel: discord.VoiceChannel - Voice channel to run event on - text: str - Additional text to the event's description (default: None) - event_image - Image for event (800 x 320) ''' await interaction.response.defer(ephemeral=True) if url is None: # Delete command message await interaction.followup.send( - envs.TOO_FEW_ARGUMENTS + I18N.t('common.too_few_arguments') ) return else: @@ -110,12 +110,17 @@ async def event_add( rel_start = _dt['rel_start'] start_event = _dt['start_event'] end_dt = _dt['end_dt'] - description = f'Turnering: {tournament}\n'\ - f'Når: {start_text} ({rel_start})' + desc_tournament = I18N.t( + 'autoevent.commands.add.description.tournament') + desc_when = I18N.t('autoevent.commands.add.description.when') + desc_where = I18N.t('autoevent.commands.add.description.where') + desc_reminder = I18N.t( + 'autoevent.commands.add.description.reminder') + description = f'{desc_tournament}: {tournament}\n'\ + f'{desc_when}: {start_text} ({rel_start})' if stadium is not None: - description += f'\nHvor: {stadium}' - description += '\n\nHusk at eventet er åpent en halvtime '\ - 'før kampstart' + description += f'\n{desc_where}: {stadium}' + description += f'\n\n{desc_reminder}' if text: description += f'\n\n{text}' if event_image: @@ -137,25 +142,28 @@ async def event_add( start_time=start_event, end_time=end_dt, privacy_level=discord.PrivacyLevel(2), - reason='autogenerated event' + reason=I18N.t('autoevent.commands.add.log_confirm') ) await interaction.followup.send( - f'Opprettet event for {home} - {away} (id: ' - f'{created_event.id})', + I18N.t('autoevent.commands.add.msg_confirm', + home=home, + away=away, + id=created_event.id + ), ephemeral=True ) except (discord.HTTPException) as e: - log.error( - envs.AUTOEVENT_HTTP_EXCEPTION_ERROR.format(e.text) + log.error('Got an error when posting event: {}'.format( + e.text) + ) + await interaction.followup.send( + I18N.t( + 'autoevent.commands.add.msg_failed', + error_in=e.text + ), + ephemeral=True ) - if 'Cannot schedule event in the past' in str(e): - log.log(envs.AUTOEVENT_EVENT_START_IN_PAST) - # Delete command message - await interaction.followup.send( - envs.AUTOEVENT_EVENT_START_IN_PAST, - ephemeral=True - ) - return + return @commands.check_any( commands.is_owner(), @@ -163,11 +171,20 @@ async def event_add( ) @discord.app_commands.autocomplete(event=event_names_autocomplete) @group.command( - name="remove", description="Remove a scheduled event" + name="remove", description=locale_str( + I18N.t('autoevent.commands.remove.cmd') + ) + ) + @describe( + event=I18N.t('autoevent.commands.remove.desc.event'), + remove_all=I18N.t('autoevent.commands.remove.desc.remove_all') ) async def event_remove( self, interaction: discord.Interaction, - event: str = None, remove_all: typing.Literal['Yes'] = None + remove_all: typing.Literal[ + I18N.t('common.literal_yes_no.yes'), + I18N.t('common.literal_yes_no.no') + ] = None, event: str = None ): ''' Removes a scheduled event that has not started yet @@ -184,21 +201,28 @@ async def event_remove( log.debug(f'Got `event_dict`: {event_dict}') # Delete all events _guild = discord_commands.get_guild() - if remove_all: - if remove_all.lower() == 'yes': - for event in event_dict: - _id = event_dict[event]['id'] - # Delete event - _event = _guild.get_scheduled_event(int(_id)) - await _event.delete() - # TODO var msg - await interaction.followup.send('All events removed') - if event is not None: - # Delete event - _event = _guild.get_scheduled_event(int(event)) - await _event.delete() - # TODO var msg - await interaction.followup.send('Event removed') + if remove_all == I18N.t('common.literal_yes_no.yes'): + for event in event_dict: + _id = event_dict[event]['id'] + # Delete event + _event = _guild.get_scheduled_event(int(_id)) + await _event.delete() + await interaction.followup.send( + I18N.t('autoevent.commands.remove.msg_all_confirm') + ) + elif remove_all == I18N.t('common.literal_yes_no.no'): + if event is not None: + # Delete event + _event = _guild.get_scheduled_event(int(event)) + await _event.delete() + await interaction.followup.send( + I18N.t('autoevent.commands.remove.msg_one_confirm') + ) + else: + log.error('No event given') + await interaction.followup.send( + I18N.t('autoevent.commands.remove.msg_no_event') + ) return @commands.check_any( @@ -206,7 +230,9 @@ async def event_remove( commands.has_permissions(manage_events=True) ) @group.command( - name="list", description="List all the planned events" + name="list", description=locale_str( + I18N.t('autoevent.commands.list.cmd') + ) ) async def list_events(self, interaction: discord.Interaction): ''' @@ -215,7 +241,7 @@ async def list_events(self, interaction: discord.Interaction): await interaction.response.defer(ephemeral=True) events = await discord_commands.get_sorted_scheduled_events() if events is None: - msg_out = envs.AUTOEVENT_NO_EVENTS_LISTED + msg_out = I18N.t('autoevent.commands.list.msg_no_events') else: msg_out = events await interaction.followup.send( @@ -227,45 +253,41 @@ async def list_events(self, interaction: discord.Interaction): commands.has_permissions(manage_events=True) ) @group.command( - name="sync", description="Create a timer for an event" + name="sync", description=locale_str( + I18N.t('autoevent.commands.sync.cmd') + ) + ) + @describe( + sync_time=I18N.t('autoevent.commands.sync.desc.sync_time'), + countdown=I18N.t('autoevent.commands.sync.desc.countdown') ) async def event_sync( - self, interaction: discord.Interaction, start_time: str, + self, interaction: discord.Interaction, sync_time: str, countdown: int ): ''' Create a timer in the active channel to make it easier for people attending an event to sync something that they're watching - - Parameters - ------------ - start_time: str - A start time for the timer or a command for deleting - the timer - countdown: int - How many seconds should it count down before hitting the - `start_time` - - Examples - ------------ - >>> /autoevent sync 02:00 10 - Countdown to 02:00 with 10 seconds to go ''' await interaction.response.defer(ephemeral=True) - # Check that `start_time` is a decent time - re_check = re.match(r'^(\d{1,2})[-:.,;_]+(\d{1,2})', str(start_time)) + # Check that `sync_time` is a decent time + re_check = re.match(r'^(\d{1,2})[-:.,;_]+(\d{1,2})', str(sync_time)) if re_check: timer_epoch = await datetime_handling.get_dt() + int(countdown) rel_start = f'' timer_msg = await interaction.followup.send( - f'Sync til {re_check.group(1)}:{re_check.group(2)} {rel_start}' + I18N.t('autoevent.commands.sync.msg_confirm', + time1=re_check.group(1), + time2=re_check.group(2), + rel_start=rel_start + ) ) - await asyncio.sleep(int(countdown)-1) + await asyncio.sleep(int(countdown)) await timer_msg.delete() else: await interaction.followup.send( - envs.AUTOEVENT_START_TIME_NOT_CORRECT_FORMAT, + I18N.t('autoevent.commands.sync.not_correct_format'), ephemeral=True ) return @@ -276,7 +298,13 @@ async def event_sync( ) @discord.app_commands.autocomplete(event=event_names_autocomplete) @group.command( - name="announce", description="Announce an event" + name="announce", description=locale_str( + I18N.t('autoevent.commands.announce.cmd') + ) + ) + @describe( + event=locale_str(I18N.t('autoevent.commands.announce.desc.event')), + channel=locale_str(I18N.t('autoevent.commands.announce.desc.channel')) ) async def event_announce( self, interaction: discord.Interaction, event: str, @@ -284,13 +312,6 @@ async def event_announce( ): ''' Announce an event in a specific channel - - Parameters - ------------ - event_id: str - The ID for the event (default: None) - channel: str - The channel to announce in (default: None) ''' await interaction.response.defer(ephemeral=True) # Get event @@ -300,11 +321,10 @@ async def event_announce( format='epoch', dt=_event.start_time.astimezone() ) - rel_start = f'' - announce_text = 'Minner om eventen som '\ - 'begynner {}, 30 min før kampstart'.format( - rel_start - ) + announce_text = I18N.t( + 'autoevent.commands.announce.annouce_text', + rel_start=rel_start + ) try: # Announce to channel async with channel.typing(): @@ -312,17 +332,32 @@ async def event_announce( await channel.send(announce_text) await channel.send(_event.url) await interaction.followup.send( - f'Melding sent til `#{channel.name}`', ephemeral=True + I18N.t( + 'autoevent.commands.announce.msg_confirm', + channel=channel.name + ), + ephemeral=True ) except discord.Forbidden: await interaction.followup.send( - 'Jeg har ikke tilgang til å sende melding ' - f'i `#{channel.name}`.', + I18N.t( + 'autoevent.commands.announce.msg_forbidden', + channel=channel.name + ), ephemeral=True ) - except Exception as e: + except Exception as _error: + log.error( + 'An error occurred when announcing event: {}'.format( + _error + ) + ) await interaction.followup.send( - f'An error occurred: {e}', ephemeral=True + I18N.t( + 'commands.announce.msg_error', + error=_error + ), + ephemeral=True ) return diff --git a/sausage_bot/cogs/dilemmas.py b/sausage_bot/cogs/dilemmas.py index c8b7eb0..e253cb2 100755 --- a/sausage_bot/cogs/dilemmas.py +++ b/sausage_bot/cogs/dilemmas.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -from discord.ext import commands import discord +from discord.ext import commands +from discord.app_commands import locale_str, describe import uuid from sausage_bot.util import envs, db_helper, file_io +from sausage_bot.util.i18n import I18N from sausage_bot.util.log import log @@ -20,7 +22,9 @@ def __init__(self, bot): ) @group.command( - name="post", description="Post a random dilemma" + name="post", description=locale_str(I18N.t( + 'dilemmas.commands.post.cmd' + )) ) async def dilemmas(self, interaction: discord.Interaction) -> None: def prettify(dilemmas_in): @@ -47,6 +51,7 @@ async def get_random_dilemma(): ) if len(no_of_dilemmas) <= 0: await interaction.followup.send( + I18N.t('dilemmas.commands.post.no_dilemmas_in_db'), envs.DILEMMAS_NO_DILEMMAS_IN_DB, ephemeral=True ) @@ -75,7 +80,12 @@ async def get_random_dilemma(): commands.has_permissions(administrator=True) ) @group.command( - name="add", description="Add a dilemma" + name="add", description=locale_str( + I18N.t('dilemmas.commands.add.cmd') + ) + ) + @describe( + dilemmas_in=I18N.t('dilemmas.commands.add.desc.dilemmas_in') ) async def dilemmas_add( self, interaction: discord.Interaction, dilemmas_in: str @@ -86,12 +96,16 @@ async def dilemmas_add( [(str(uuid.uuid4()), dilemmas_in)] ) await interaction.followup.send( - 'Added the following dilemma: {}'.format(dilemmas_in) + I18N.t( + 'dilemmas.commands.add.msg_confirm', + dilemmas_in=dilemmas_in) ) return @group.command( - name="count", description="Count the numbers of dilemmas" + name="count", description=locale_str( + I18N.t('dilemmas.commands.count.cmd') + ) ) async def count(self, interaction: discord.Interaction) -> None: await interaction.response.defer(ephemeral=True) @@ -101,9 +115,9 @@ async def count(self, interaction: discord.Interaction) -> None: select=('id') )) await interaction.followup.send( - '{}{}'.format( - envs.DILEMMAS_COUNT.format(no_of_dilemmas), - 's' if no_of_dilemmas > 1 else '' + I18N.t( + 'dilemmas.commands.count.msg_confirm', + count=no_of_dilemmas, ), ephemeral=True ) return diff --git a/sausage_bot/cogs/log_maintenance.py b/sausage_bot/cogs/log_maintenance.py index afe6d5b..8c5448e 100755 --- a/sausage_bot/cogs/log_maintenance.py +++ b/sausage_bot/cogs/log_maintenance.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- import os -from discord.ext import commands, tasks import discord +from discord.ext import commands, tasks +from discord.app_commands import locale_str, describe from tabulate import tabulate from sausage_bot.util import envs, file_io from sausage_bot.util import discord_commands, db_helper +from sausage_bot.util.i18n import I18N from sausage_bot.util.log import log @@ -40,19 +42,24 @@ def __init__(self, bot): super().__init__() log_group = discord.app_commands.Group( - name="log", description='Administer log' + name="log", + description=locale_str(I18N.t('log_maintenance.commands.log.cmd')) ) log_maintenance_group = discord.app_commands.Group( - name="maintenance", description='Administer log task', - parent=log_group + name="maintenance", description=locale_str( + I18N.t('log_maintenance.commands.maintenance.cmd') + ), parent=log_group ) log_settings_group = discord.app_commands.Group( - name="settings", description='Administer log settings', - parent=log_group + name="settings", description=locale_str( + I18N.t('log_maintenance.commands.settings.cmd') + ), parent=log_group ) @log_maintenance_group.command( - name='start', description='Start log maintenance' + name='start', description=locale_str( + I18N.t('log_maintenance.commands.start.cmd') + ) ) async def log_maintenance_start( self, interaction: discord.Interaction @@ -69,11 +76,13 @@ async def log_maintenance_start( updates=('status', 'started') ) await interaction.followup.send( - 'Log maintenance started' + I18N.t('log_maintenance.commands.start.msg_confirm') ) @log_maintenance_group.command( - name='stop', description='Stop log maintenance' + name='stop', description=locale_str( + I18N.t('log_maintenance.commands.stop.cmd') + ) ) async def rss_posting_stop( self, interaction: discord.Interaction @@ -90,7 +99,7 @@ async def rss_posting_stop( updates=('status', 'stopped') ) await interaction.followup.send( - 'Log maintenance stopped' + I18N.t('log_maintenance.commands.stop.msg_confirm') ) @commands.check_any( @@ -111,7 +120,11 @@ async def list_settings( template_info=envs.log_db_schema, select=('setting', 'value', 'value_help') ) - headers = ['Setting', 'Value', 'Value type'] + headers = [ + I18N.t('log_maintenance.commands.list.headers.setting'), + I18N.t('log_maintenance.commands.list.headers.value'), + I18N.t('log_maintenance.commands.list.headers.value_type') + ] await interaction.followup.send( content='```{}```'.format( tabulate(settings_in_db, headers=headers) @@ -126,7 +139,15 @@ async def list_settings( name_of_setting=name_of_settings_autocomplete ) @log_settings_group.command( - name='setting', description='Change a setting for this cog' + name='change', description=locale_str( + I18N.t('log_maintenance.commands.setting.cmd') + ) + ) + @describe( + name_of_setting=I18N.t( + 'log_maintenance.commands.setting.desc.name_of_setting' + ), + value_in=I18N.t('log_maintenance.commands.setting.desc.value_in') ) async def log_setting( self, interaction: discord.Interaction, name_of_setting: str, @@ -134,13 +155,6 @@ async def log_setting( ): ''' Change a setting for this cog - - Parameters - ------------ - name_of_setting: str - The names of the role to add (default: None) - value_in: str - The value of the settings (default: None) ''' await interaction.response.defer(ephemeral=True) settings_in_db = await db_helper.get_output( @@ -152,11 +166,15 @@ async def log_setting( if setting[2] == 'bool': try: value_in = eval(str(value_in).capitalize()) - except NameError as e: - log.error(f'Invalid input for `value_in`: {e}') + except NameError as _error: + log.error(f'Invalid input for `value_in`: {_error}') # TODO var msg await interaction.followup.send( - 'Input `value_in` needs to be `True` or `False`' + I18N.t( + 'log_maintenance.commands.setting.' + 'value_in_input_invalid', + error=_error + ) ) return log.debug(f'`value_in` is {value_in} ({type(value_in)})') @@ -168,7 +186,8 @@ async def log_setting( updates=[('value', value_in)] ) await interaction.followup.send( - content='Setting updated', ephemeral=True + I18N.t('log_maintenance.commands.setting.msg_confirm'), + ephemeral=True ) LogMaintenance.log_maintenance.restart() break @@ -177,7 +196,6 @@ async def log_setting( # Tasks @tasks.loop(hours=4) async def log_maintenance(): - # maintenance logs folder log_files = os.listdir(envs.LOG_DIR) settings_type = await db_helper.get_output( template_info=envs.log_db_schema, @@ -199,20 +217,29 @@ async def log_maintenance(): log.verbose( f'`settings_type` is {settings_limit} {type(settings_limit)}' ) - log_msg = 'I\'m not deleting logs, but have notified the bot '\ - 'channel about the situation' + log.log('I\'m not deleting logs, but have notified the bot ' + 'channel about the situation') if settings_type == 'size': folder_size = file_io.folder_size( str(envs.LOG_DIR), human=True ) - discord_msg = '`settings_type` is set to `0`. The log '\ - f'folder\'s size as of now is {folder_size}. '\ - 'To disable these messages, run `/log maintenance stop`' + discord_msg = I18N.t( + 'log_maintenance.tasks.log_maintenance.msg.size_and_none', + folder_size=folder_size + ) + discord_msg += '\n' + discord_msg += I18N.t( + 'log_maintenance.tasks.log_maintenance.msg.disable_posting' + ) elif settings_type in ['day', 'days']: num_log_files = len(os.listdir(str(envs.LOG_DIR))) - discord_msg = '`settings_type` is set to `0`. The log folder '\ - f'has logs from {num_log_files} days back.'\ - 'To disable these messages, run `/log maintenance stop`' + discord_msg = I18N.t( + 'log_maintenance.tasks.log_maintenance.msg.days_and_none', + num_files=num_log_files + ) + discord_msg += I18N.t( + 'log_maintenance.tasks.log_maintenance.msg.disable_posting' + ) else: log_msg = 'Wrong input in `settings_type`: '\ f'{settings_type}' @@ -221,7 +248,6 @@ async def log_maintenance(): await discord_commands.log_to_bot_channel(discord_msg) elif settings_limit > 0: deleted_files = [] - status_msg = 'Log maintenance done. Deleted the following files:' discord_msg_out = '' log_msg_out = '' folder_size = file_io.folder_size(str(envs.LOG_DIR)) @@ -243,11 +269,11 @@ async def log_maintenance(): print(f'`size_diff` reduced to {size_diff}') if size_diff <= 0: break - print(f'Got enough files to delete: {deleted_files}') + log.debug(f'Got enough files to delete: {deleted_files}') for _file in deleted_files: os.remove(envs.LOG_DIR / _file) new_folder_size = file_io.folder_size(str(envs.LOG_DIR)) - print(f'Folder went from {folder_size} to {new_folder_size}') + log.debug(f'Folder went from {folder_size} to {new_folder_size}') elif settings_type in ['day', 'days']: if len(log_files) > 10: for _file in log_files[0:-10]: @@ -260,7 +286,10 @@ async def log_maintenance(): ) return if len(deleted_files) > 0: - discord_msg_out += status_msg + status_msg = 'Log maintenance done. Deleted the following files:' + discord_msg_out += I18N.t( + 'log_maintenance.tasks.' + 'log_maintenance.msg.maintenance_done') for _file in deleted_files: discord_msg_out += f'\n- {_file}' await discord_commands.log_to_bot_channel(discord_msg_out) diff --git a/sausage_bot/cogs/poll.py b/sausage_bot/cogs/poll.py index 7922375..57aa2cc 100755 --- a/sausage_bot/cogs/poll.py +++ b/sausage_bot/cogs/poll.py @@ -1,15 +1,17 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- +import discord from discord.ext import commands +from discord.app_commands import locale_str, describe import random import asyncio import re import pendulum import uuid -import discord from sausage_bot.util import db_helper, envs from sausage_bot.util import datetime_handling +from sausage_bot.util.i18n import I18N from sausage_bot.util.log import log _tz = 'local' @@ -27,7 +29,14 @@ def __init__(self, bot): ) @discord.app_commands.command( name="poll", - description='Make a poll for voting on something' + description=locale_str(I18N.t('poll.commands.poll.cmd')) + ) + @describe( + channel=I18N.t('poll.commands.poll.desc.channel'), + post_time=I18N.t('poll.commands.poll.desc.post_time'), + lock_time=I18N.t('poll.commands.poll.desc.lock_time'), + poll_text=I18N.t('poll.commands.poll.desc.poll_text'), + alternatives=I18N.t('poll.commands.poll.desc.alternatives') ) async def poll( self, interaction: discord.Interaction, channel: discord.TextChannel, @@ -35,19 +44,6 @@ async def poll( ): ''' Make a poll for voting on something. - - Parameters - ------------ - channel: discord.TextChannel - Channel to post poll in - post_time: str - What to post the poll. Accepts time in 0000 - lock_time: str - Lock poll after x m(inutes) or h(ours) - poll_text: str - Input for the poll - alternatives: str - Alternatives for the poll, separated by semicolon ''' await interaction.response.defer(ephemeral=True) if post_time in [None, 'no', 'now']: @@ -67,24 +63,32 @@ async def poll( dt_post_secs = await datetime_handling.get_dt(dt=dt_post) if dt_post_secs < dt_now: await interaction.followup.send( - 'Posting time is in the past' + I18N.t('poll.commands.poll.msg.post_in_past') ) return else: await interaction.followup.send( - f'`post_time` "{post_time}" gives error' + I18N.t( + 'poll.commands.poll.msg.post_gives_error', + post_time=post_time + ) ) return # Check lock_time if lock_time in [None, 'no', 'now']: await interaction.followup.send( - 'No lock_time is given', ephemeral=True + I18N.t('poll.commands.poll.msg.no_time_given'), + ephemeral=True ) return lock_time_regex = r'^(\d+)(\s)?(h|m)$' if not re.match(lock_time_regex, lock_time): await interaction.followup.send( - 'Error with given `lock_time`', ephemeral=True + I18N.t( + 'poll.commands.poll.msg.lock_gives_error', + lock_time=lock_time + ), + ephemeral=True ) return _uuid = str(uuid.uuid4()) @@ -129,14 +133,17 @@ async def poll( if dt_post is None: post_wait = 0 coming_post = await interaction.followup.send( - 'Avstemningen postes straks' + I18N.t('poll.commands.poll.msg.posting_now') ) else: dt_now = pendulum.now('local') post_wait = dt_now.diff(dt_post).in_seconds() dt_post_epoch = dt_post.format('x')[0:-3] coming_post = await interaction.followup.send( - f'Avstemningen postes om ', + I18N.t( + 'poll.commands.poll.msg.posting_fixed', + dt_post_epoch=dt_post_epoch + ), ephemeral=False ) log.debug(f'post_wait: {post_wait}') @@ -174,14 +181,14 @@ async def poll( ) embed_json = discord.Embed.from_dict( { - 'title': 'Avstemning', + 'title': I18N.t('poll.commands.poll.msg.embed_title'), 'description': desc_out, } ) except TimeoutError: - # TODO var msg await interaction.followup.send( - 'Timed out', ephemeral=True + I18N.t('poll.commands.poll.msg.timed_out'), + ephemeral=True ) return # Post the poll message @@ -196,12 +203,14 @@ async def poll( post_text = pendulum.now('local').format('DD.MM.YY, HH:mm') await coming_post.delete() await interaction.followup.send( - content=f"Avstemningen ble postet {post_text}", + I18N.t('poll.commands.poll.msg.post_confirm', post_text=post_text), ephemeral=True ) - message_text = f'Avstemning blir stengt om ' poll_msg = await channel.send( - content=message_text, + I18N.t( + 'poll.commands.poll.msg.lock_confirm_future', + dt_lock_epoch=dt_lock_epoch + ), embed=embed_json ) log.debug(f'Got `poll_msg`: {poll_msg}') @@ -265,11 +274,13 @@ async def poll( desc_out += f'\n{reaction[0]}: {reaction[1]}' embed_json = discord.Embed.from_dict( { - 'title': 'Avstemning', + 'title': I18N.t('poll.commands.poll.msg.embed_title'), 'description': desc_out, 'footer': { - # TODO var msg - 'text': f'Avstemning ble stengt etter {dt_lock_text}' + 'text': I18N.t( + 'poll.commands.poll.msg.lock_confirm', + dt_lock_text=dt_lock_text + ) } } ) diff --git a/sausage_bot/cogs/quote.py b/sausage_bot/cogs/quote.py index 800d9dd..ae6ce93 100755 --- a/sausage_bot/cogs/quote.py +++ b/sausage_bot/cogs/quote.py @@ -2,11 +2,15 @@ # -*- coding: UTF-8 -*- import discord from discord.ext import commands +from discord.utils import get +from discord.app_commands import locale_str, describe +import typing import uuid from asyncio import TimeoutError from sausage_bot.util.datetime_handling import get_dt from sausage_bot.util import envs, db_helper, file_io +from sausage_bot.util.i18n import I18N from sausage_bot.util.log import log @@ -142,7 +146,7 @@ def __init__( # Create elements num_label = QuoteTextInput( style_in=discord.TextStyle.short, - label_in='Quote number', + label_in=I18N.t('quote.modals.quote_num'), default_in=self.quote_in[0][0] if self.quote_in else self.available_row_id, required_in=False if self.quote_in else True @@ -150,7 +154,7 @@ def __init__( quote_text = QuoteTextInput( style_in=discord.TextStyle.paragraph, - label_in='Quote text', + label_in=I18N.t('quote.modals.quote_text'), default_in=self.quote_in[0][2] if self.quote_in else '', required_in=True, placeholder_in='Text' @@ -158,10 +162,10 @@ def __init__( quote_date = QuoteTextInput( style_in=discord.TextStyle.short, - label_in='Quote date', + label_in=I18N.t('quote.modals.quote_date'), default_in=self.quote_in[0][3] if self.quote_in else '', required_in=False, - placeholder_in='Date and time (DD.MM.YYYY HH:MM)' + placeholder_in=I18N.t('quote.modals.date_placeholder') ) self.add_item(num_label) @@ -174,13 +178,14 @@ async def on_submit(self, interaction: discord.Interaction): self.quote_out['datetime'] = self.children[2].value await interaction.response.send_message( - # TODO i18n - 'Quote added', ephemeral=True + I18N.t('quote.modals.add.msg_confirm'), + ephemeral=True ) async def on_error(self, interaction: discord.Interaction, error): await interaction.response.send_message( - f'Error: {error}', ephemeral=True + I18N.t('quote.modals.error', error=error), + ephemeral=True ) @@ -204,7 +209,7 @@ def __init__( # Create elements quote_text = QuoteTextInput( style_in=discord.TextStyle.paragraph, - label_in='Quote text', + label_in=I18N.t('quote.modals.quote_text'), default_in=self.quote_in[0][2] if self.quote_in else '', required_in=True, placeholder_in='Text' @@ -212,10 +217,10 @@ def __init__( quote_date = QuoteTextInput( style_in=discord.TextStyle.short, - label_in='Quote date', + label_in=I18N.t('quote.modals.quote_date'), default_in=self.quote_in[0][3] if self.quote_in else '', required_in=False, - placeholder_in='Date and time (DD.MM.YYYY HH:MM)' + placeholder_in=I18N.t('quote.modals.date_placeholder') ) self.add_item(quote_text) @@ -227,13 +232,14 @@ async def on_submit(self, interaction: discord.Interaction): self.quote_out['datetime'] = self.children[1].value await interaction.response.send_message( - # TODO i18n - 'Quote edited', ephemeral=True + I18N.t('quote.modals.edit.msg_confirm'), + ephemeral=True ) async def on_error(self, interaction: discord.Interaction, error): await interaction.response.send_message( - f'Error: {error}', ephemeral=True + I18N.t('quote.modals.error', error=error), + ephemeral=True ) @@ -263,12 +269,17 @@ def __init__(self, bot): super().__init__() group = discord.app_commands.Group( - name="quote", description='Quotes' + name="quote", description=locale_str( + I18N.t('quote.commands.quote.cmd') + ) ) @discord.app_commands.autocomplete(quote_in=quotes_autocomplete) @group.command( - name="post", description="Post a random quote" + name="post", description=locale_str(I18N.t('quote.commands.post.cmd')) + ) + @describe( + number=I18N.t('quote.commands.post.desc.number') ) async def post( self, interaction: discord.Interaction, @@ -365,7 +376,10 @@ async def get_random_quote(): return else: await interaction.followup.send( - envs.QUOTE_DOES_NOT_EXIST.format(quote_in) + I18N.t( + 'quote.commands.post.quote_not_exist', + number=number + ) ) return @@ -374,7 +388,7 @@ async def get_random_quote(): commands.has_permissions(administrator=True) ) @group.command( - name="add", description="Add a quote" + name="add", description=locale_str(I18N.t('quote.commands.add.cmd')) ) async def quote_add( self, interaction: discord.Interaction, @@ -390,7 +404,7 @@ async def quote_add( else: last_row_id = _row_ids[-1]+1 modal_in = QuoteAddModal( - title_in='Add quote', + title_in=I18N.t('quote.modals.add.modal_title'), available_row_id=last_row_id ) await interaction.response.send_modal(modal_in) @@ -412,6 +426,11 @@ async def quote_add( (quote_out['uuid'], quote_out['quote_text'], iso_date) ] ) + await interaction.followup.send( + I18N.t( + 'quote.commands.add.msg_confirm'), + ephemeral=True + ) return @commands.check_any( @@ -420,7 +439,10 @@ async def quote_add( ) @discord.app_commands.autocomplete(quote_in=quotes_autocomplete) @group.command( - name="edit", description="Edit an existing quote" + name="edit", description=locale_str(I18N.t('quote.commands.edit.cmd')) + ) + @describe( + quote_in=I18N.t('quote.commands.edit.desc.quote_in') ) async def quote_edit( self, interaction: discord.Interaction, quote_in: str, @@ -430,7 +452,7 @@ async def quote_edit( quote_from_db = await get_quote_from_db(quote_in) log.verbose(f'quote_from_db: {quote_from_db}') modal_in = QuoteEditModal( - title_in='Edit quote', + title_in=I18N.t('quote.modals.edit.modal_title'), quote_in=quote_from_db ) await interaction.response.send_modal(modal_in) @@ -443,13 +465,17 @@ async def quote_edit( elif str(quote_from_db[0][2]) != str(modal_in.quote_out['datetime']): update_triggered = True else: - _msg = 'No changes discovered in quote' - log.error(_msg) - await interaction.followup.send(_msg, ephemeral=True) + log.error('No changes discovered in quote') + await interaction.followup.send( + I18N.t('quote.modals.edit.no_change'), + ephemeral=True + ) return if update_triggered: - log.verbose('Discovered changes in quote:', - pretty=modal_in.quote_out) + log.verbose( + 'Discovered changes in quote:', + pretty=modal_in.quote_out + ) # Update quote await db_helper.update_fields( template_info=envs.quote_db_schema, @@ -472,7 +498,12 @@ async def quote_edit( ) @discord.app_commands.autocomplete(quote_in=quotes_autocomplete) @group.command( - name="delete", description="Delete a quote" + name="delete", description=locale_str( + I18N.t('quote.commands.delete.cmd') + ) + ) + @describe( + quote_in=I18N.t('quote.commands.delete.desc.quote_in') ) async def quote_delete( self, interaction: discord.Interaction, @@ -486,9 +517,11 @@ async def quote_delete( log.db(f'quote is: {quote}') confirm_buttons = ConfirmButtons() await interaction.followup.send( - envs.QUOTE_CONFIRM_DELETE.format( - quote[0], quote[2], - get_dt( + I18N.t( + 'quote.commands.delete.confirm_delete', + quote_num=quote[0], + quote_text=quote[2], + quote_date=get_dt( format='datetextfull', dt=quote[3] ) @@ -506,12 +539,15 @@ async def quote_delete( ) # Confirm that the quote has been deleted await interaction.followup.send( - envs.QUOTE_DELETE_CONFIRMED.format(quote[0]), + I18N.t( + 'quote.commands.delete.msg_confirm', + quote_num=quote[0] + ), ephemeral=True ) else: await interaction.followup.send( - envs.QUOTE_DENY_DELETE, + I18N.t('quote.commands.delete.msg_fail'), ephemeral=True ) return @@ -521,7 +557,9 @@ async def quote_delete( commands.has_permissions(administrator=True) ) @group.command( - name="count", description="Count the number of quotes" + name="count", description=locale_str( + I18N.t('quote.commands.count.cmd') + ) ) async def quote_count(self, interaction: discord.Interaction): 'Count the number of quotes available' @@ -532,7 +570,10 @@ async def quote_count(self, interaction: discord.Interaction): ) ) await interaction.followup.send( - envs.QUOTE_COUNT.format(quote_count) + I18N.t( + 'quote.commands.count.msg_confirm', + num_quotes=quote_count + ) ) return diff --git a/sausage_bot/cogs/roles.py b/sausage_bot/cogs/roles.py index 75318a6..c5cfc5c 100755 --- a/sausage_bot/cogs/roles.py +++ b/sausage_bot/cogs/roles.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- +import discord from discord.ext import commands from discord.utils import get -import discord +from discord.app_commands import locale_str, describe from tabulate import tabulate import re import typing from sausage_bot.util import config, envs, file_io, discord_commands, db_helper +from sausage_bot.util.i18n import I18N from sausage_bot.util.log import log @@ -93,13 +95,16 @@ def prep_dropdown(perm_name, permissions_in): ) general_dropdown = DropdownPermissions( - 'Select general permissions', self.permissions_out, general_perms + I18N.t('roles.views.perms.drop_general'), + self.permissions_out, general_perms ) text_dropdown = DropdownPermissions( - 'Select text permissions', self.permissions_out, text_perms + I18N.t('roles.views.perms.drop_text'), + self.permissions_out, text_perms ) voice_dropdown = DropdownPermissions( - 'Select voice permissions', self.permissions_out, voice_perms + I18N.t('roles.views.perms.drop_voice'), + self.permissions_out, voice_perms ) self.add_item(general_dropdown) @@ -114,9 +119,9 @@ async def settings_autocomplete( current: str, ) -> list[discord.app_commands.Choice[str]]: settings_db = [setting for setting in await db_helper.get_output( - template_info=envs.roles_db_settings_schema, - get_row_ids=True - ) + template_info=envs.roles_db_settings_schema, + get_row_ids=True + ) ] print(f'SETTINGS_DB: {settings_db}') settings_in_list = [] @@ -139,6 +144,8 @@ async def settings_autocomplete( ) for setting in settings_in_list if current.lower() in setting[3].lower() + for setting in settings_in_list + if current.lower() in setting[3].lower() ] @@ -284,6 +291,7 @@ async def sync_reaction_message_from_settings( } # Edit discord message await msg_obj.edit( + content=db_message[0][3], embed=discord.Embed.from_dict(embed_json) ) return @@ -357,23 +365,23 @@ def tabulate_roles(dict_in): content = { 'emoji': { 'length': 7, - 'header': 'Emoji' + 'header': I18N.t('roles.roles_headers.emoji') }, 'name': { 'length': 0, - 'header': 'Navn' + 'header': I18N.t('roles.roles_headers.name') }, 'id': { 'length': 20, - 'header': 'ID' + 'header': I18N.t('roles.roles_headers.id') }, 'members': { 'length': 8, - 'header': 'Members' + 'header': I18N.t('roles.roles_headers.members') }, 'managed': { 'length': 17, - 'header': 'Auto-håndtert?' + 'header': I18N.t('roles.roles_headers.managed') } } for dict_item in dict_in: @@ -417,6 +425,77 @@ def tabulate_roles(dict_in): return paginated +def tabulate_emojis_and_roles(dict_in): + content = { + 'emoji': { + 'length': 1, + }, + 'emoji_name': { + 'length': len(str( + I18N.t('roles.tabulate_emojis_and_roles.emoji_name') + )), + 'header': I18N.t('roles.tabulate_emojis_and_roles.emoji_name') + }, + 'emoji_id': { + 'length': 19, + 'header': str(I18N.t('roles.tabulate_emojis_and_roles.emoji_id')) + }, + 'role_name': { + 'length': len(str( + I18N.t('roles.tabulate_emojis_and_roles.role_name') + ))+1, + 'header': str(I18N.t('roles.tabulate_emojis_and_roles.role_name')) + }, + 'role_id': { + 'length': 19, + 'header': str(I18N.t('roles.tabulate_emojis_and_roles.role_id')) + } + } + for dict_item in dict_in: + log.debug(f'Processing: {dict_item}') + for item in dict_in[dict_item]: + print('if {} > {}'.format( + len(str(item)), + str(content[dict_item]['length']) + )) + if len(str(item)) > content[dict_item]['length']: + content[dict_item]['length'] = len(str(item)) + 1 + header = '` {:>{}} {:>{}} {:>{}} {:>{}}`'.format( + content['emoji_name']['header'], + content['emoji_name']['length'], + content['emoji_id']['header'], + content['emoji_id']['length'], + content['role_name']['header'], + content['role_name']['length'], + content['role_id']['header'], + content['role_id']['length'] + ) + paginated = [] + temp_out = header + counter = 0 + while counter <= len(dict_in['emoji_id'])-1: + line_out = '{} `{:>{}} {:>{}} {:>{}} {:<{}}`'.format( + '<:{}:{}>'.format( + dict_in['emoji_name'][counter], + dict_in['emoji_id'][counter] + ), + dict_in['emoji_name'][counter], content['emoji_name']['length'], + dict_in['emoji_id'][counter], content['emoji_id']['length'], + dict_in['role_name'][counter], content['role_name']['length'], + dict_in['role_id'][counter], content['role_id']['length'] + ) + if len(temp_out) + len(line_out) > 1900: + log.debug('Hit 1900 mark') + paginated.append(temp_out) + temp_out = header + temp_out += f'\n{line_out}' + else: + temp_out += f'\n{line_out}' + counter += 1 + paginated.append(temp_out) + return paginated + + def paginate_tabulate(tabulated): log.debug(f'Length of `tabulated` is {len(tabulated)}') paginated = [] @@ -520,27 +599,35 @@ def __init__(self, bot): super().__init__() roles_group = discord.app_commands.Group( - name="roles", description='Control roles on the server' + name="roles", description=locale_str( + I18N.t('roles.commands.roles.cmd') + ) ) roles_reaction_group = discord.app_commands.Group( - name="reaction", description='Control reaction messages on the server', + name="reaction", description=locale_str(I18N.t( + 'roles.commands.reaction.cmd' + )), parent=roles_group ) roles_reaction_add_group = discord.app_commands.Group( - name="reaction_add", description='Add reaction message to the server', + name="reaction_add", description=locale_str(I18N.t( + 'roles.commands.reaction_add.cmd' + )), parent=roles_group ) roles_reaction_remove_group = discord.app_commands.Group( name="reaction_remove", - description='Remove reaction message from the server', + description=locale_str(I18N.t('roles.commands.reaction_remove.cmd')), parent=roles_group ) roles_settings_group = discord.app_commands.Group( - name="settings", description='Control settings on the server', + name="settings", description=locale_str(I18N.t( + 'roles.commands.settings_group' + )), parent=roles_group ) @@ -549,11 +636,18 @@ def __init__(self, bot): commands.has_permissions(manage_roles=True) ) @roles_group.command( - name='info', description='Get info about a specific role' + name='info', description=locale_str(I18N.t('roles.commands.info.cmd'),) + ) + @describe( + public=I18N.t('roles.commands.info.desc.public'), + role_in=I18N.t('roles.commands.info.desc.role_in') ) async def role_info( self, interaction: discord.Interaction, - public: typing.Literal['Yes', 'No'], role_in: discord.Role + public: typing.Literal[ + I18N.t('common.literal_yes_no.yes'), + I18N.t('common.literal_yes_no.no') + ], role_in: discord.Role ): ''' Get info about a specific role (`role_in`) @@ -627,11 +721,25 @@ async def role_info( @roles_group.command( name='list', description='List roles or emojis' ) + @describe( + public=I18N.t('roles.commands.list.desc.public'), + type=I18N.t('roles.commands.list.desc.type'), + sort=I18N.t('roles.commands.list.desc.sort') + ) async def roles_list( self, interaction: discord.Interaction, - public: typing.Literal['Yes', 'No'], - type: typing.Literal['Roles', 'Emojis'], - sort: typing.Literal['By name', 'By ID'] + public: typing.Literal[ + I18N.t('common.literal_yes_no.yes'), + I18N.t('common.literal_yes_no.no') + ], + type: typing.Literal[ + I18N.t('roles.commands.list.literal.type.roles'), + I18N.t('roles.commands.list.literal.type.emojis') + ], + sort: typing.Literal[ + I18N.t('roles.commands.list.literal.sort.name'), + I18N.t('roles.commands.list.literal.sort.id') + ] ): async def roles_list_roles(): _guild = discord_commands.get_guild() @@ -642,11 +750,11 @@ async def roles_list_roles(): 'members': [], 'managed': [] } - if sort.lower() == 'by name': + if sort == I18N.t('roles.commands.list.literal.sort.name'): _roles = tuple(sorted( _guild.roles, key=lambda role: role.name.lower() )) - elif sort.lower() == 'by id': + elif sort == I18N.t('roles.commands.list.literal.sort.id'): _roles = tuple(sorted( _guild.roles, key=lambda role: role.id )) @@ -656,11 +764,13 @@ async def roles_list_roles(): tabulate_dict['id'].append(role.id) tabulate_dict['members'].append(len(role.members)) if role.managed: - # TODO i18n? - tabulate_dict['managed'].append('Ja') + tabulate_dict['managed'].append( + I18N.t('common.literal_yes_no.yes') + ) elif not role.managed: - # TODO i18n? - tabulate_dict['managed'].append('Nei') + tabulate_dict['managed'].append( + I18N.t('common.literal_yes_no.no') + ) return tabulate_roles(tabulate_dict) async def roles_list_emojis(): @@ -672,11 +782,11 @@ async def roles_list_emojis(): 'animated': [], 'managed': [] } - if sort.lower() == 'by name': + if sort == I18N.t('roles.commands.list.literal.sort.name'): _emojis = tuple(sorted( _guild.emojis, key=lambda emoji: emoji.name.lower() )) - elif sort.lower() == 'by id': + elif sort == I18N.t('roles.commands.list.literal.sort.id'): _emojis = tuple(sorted( _guild.emojis, key=lambda emoji: emoji.id )) @@ -687,24 +797,35 @@ async def roles_list_emojis(): tabulate_dict['name'].append(emoji.name) tabulate_dict['id'].append(emoji.id) if emoji.animated: - tabulate_dict['animated'].append('Ja') + tabulate_dict['animated'].append( + I18N.t('common.literal_yes_no.yes') + ) else: - tabulate_dict['animated'].append('Nei') + tabulate_dict['animated'].append( + I18N.t('common.literal_yes_no.no') + ) if emoji.managed: - tabulate_dict['managed'].append('Ja') + tabulate_dict['managed'].append( + I18N.t('common.literal_yes_no.yes') + ) else: - tabulate_dict['managed'].append('Nei') + tabulate_dict['managed'].append( + I18N.t('common.literal_yes_no.no') + ) # Returning pagination - return tabulate_emoji(tabulate_dict) + return tabulate_emoji( + type_in='roles_list_emoji', + dict_in=tabulate_dict + ) - if public.lower() == 'yes': + if public == I18N.t('common.literal_yes_no.yes'): _ephemeral = False - elif public.lower() == 'no': + elif public == I18N.t('common.literal_yes_no.no'): _ephemeral = True await interaction.response.defer(ephemeral=_ephemeral) - if type.lower() == 'roles': + if type == I18N.t('roles.commands.list.literal.type.roles'): pages = await roles_list_roles() - elif type.lower() == 'emojis': + elif type == I18N.t('roles.commands.list.literal.type.emojis'): pages = await roles_list_emojis() for page in pages: log.verbose(f'{page}') @@ -719,33 +840,21 @@ async def roles_list_emojis(): commands.has_permissions(manage_roles=True) ) @roles_group.command( - name='add', description='Add a role' + name='add', description=locale_str(I18N.t('roles.commands.add.cmd')) + ) + @describe( + role_name=I18N.t('roles.commands.add.desc.role_name'), + hoist=I18N.t('roles.commands.add.desc.hoist'), + mentionable=I18N.t('roles.commands.add.desc.mentionable'), + color=I18N.t('roles.commands.add.desc.color'), + display_icon=I18N.t('roles.commands.add.desc.display_icon') ) async def add_role( self, interaction: discord.Interaction, role_name: str, hoist: bool, mentionable: bool, color: str = None, display_icon: discord.Attachment = None ): - ''' - Add role to the server - - Parameters - ------------ - role_name: str - The names of the role to add - color: str - Set color for the role (accepts `0x`, `#`, `0x#`, - or `rgb(, , )`) - hoist: str (yes/no) - Set if the role should be mentionable or not - mentionable: str (yes/no) - Set if the role should be mentionable or not - display_icon: discord.Attachment - Set a display icon for the role. Only possible if the guild - has enough boosts - ''' await interaction.response.defer(ephemeral=True) - # TODO i18n if not color: color = discord.Color.random() else: @@ -754,7 +863,7 @@ async def add_role( display_icon = await display_icon.read() perm_view = PermissionsView() await interaction.followup.send( - "Set permissions", view=perm_view + I18N.t('roles.commands.add.set_perms'), view=perm_view ) await perm_view.wait() # Get new permissions and add to role @@ -774,16 +883,16 @@ async def add_role( display_icon=display_icon ) await interaction.followup.send( - 'Role is created' + I18N.t('roles.commands.add.msg_confirm') ) except discord.errors.Forbidden as e: await interaction.followup.send( - f'Error when creating role: {e.text}' + I18N.t('roles.commands.add.msg_error', _error=e.text) ) return except ValueError as e: await interaction.followup.send( - f'Error when creating role: {e}' + I18N.t('roles.commands.add.msg_error', _error=e) ) return return @@ -793,25 +902,22 @@ async def add_role( commands.has_permissions(manage_roles=True) ) @roles_group.command( - name='remove', description='Remove a role from the server' + name='remove', description=locale_str(I18N.t( + 'roles.commands.remove.cmd' + )) + ) + @describe( + role_name=I18N.t('roles.commands.remove.desc.role_name') ) async def remove_role( self, interaction: discord.Interaction, role_name: discord.Role ): - ''' - Remove a role from the server - - Parameters - ------------ - role_name: discord.Role - The name of the role to remove - ''' await interaction.response.defer(ephemeral=True) _guild = discord_commands.get_guild() - _rolename = role_name.name + rolename = role_name.name await _guild.get_role(int(role_name.id)).delete() await interaction.followup.send( - f'Role `{_rolename}` has been deleted' + I18N.t('roles.commands.remove.msg_confirm', rolename=rolename) ) return @@ -820,48 +926,44 @@ async def remove_role( commands.has_permissions(manage_roles=True) ) @roles_group.command( - name='edit', description='Edit a role on the server' + name='edit', description=locale_str(I18N.t('roles.commands.edit.cmd')) + ) + @describe( + role_name=I18N.t('roles.commands.edit.desc.role_name'), + new_name=I18N.t('roles.commands.edit.desc.new_name'), + color=I18N.t('roles.commands.edit.desc.color'), + hoist=I18N.t('roles.commands.edit.desc.hoist'), + permissions=I18N.t('roles.commands.edit.desc.permissions') ) async def edit_role( self, interaction: discord.Interaction, role_name: discord.Role, permissions: bool, new_name: str = None, color: str = None, hoist: bool = None ): - ''' - Edit a role on the server - - Parameters - ------------ - role_name: str - The name of the role to edit - new_name: str - Name for the role (default: None) - color: str - Color for the role. Accepts 0x, #, 0x#, - rgb(, , ) - hoist: bool - Indicates if the role will be displayed separately from other - members. - permissions: bool - Indicate if the permissions also should be edited - ''' await interaction.response.defer(ephemeral=True) changes = [] if new_name: log.debug('Changed name') - changes.append(f'\n- Name: `{role_name}` -> `{new_name}`') + i18n_name = I18N.t('roles.commands.edit.changelist_name') + changes.append(f'\n- {i18n_name}: `{role_name}` -> `{new_name}`') await role_name.edit( name=new_name ) if color: log.debug('Changed color') - changes.append(f'\n- Color: `{role_name.color}` -> `{color}`') + i18n_color = I18N.t('roles.commands.edit.changelist_color') + changes.append( + f'\n- {i18n_color}: `{role_name.color}` -> `{color}`' + ) await role_name.edit( color=discord.Color.from_str(color) ) if hoist: log.debug('Changed hoist setting') - changes.append(f'\n- Hoist: `{role_name.hoist}` -> `{hoist}`') + i18n_hoist = I18N.t('roles.commands.edit.changelist_hoist') + changes.append( + f'\n- {i18n_hoist}: `{role_name.hoist}` -> `{hoist}`' + ) await role_name.edit( hoist=hoist ) @@ -874,7 +976,7 @@ async def edit_role( permissions_in=perms_in ) await interaction.followup.send( - "Change permissions", view=perm_view + I18N.t("roles.commands.edit.change_perms"), view=perm_view ) await perm_view.wait() perms_out = perm_view.permissions_out @@ -884,15 +986,28 @@ async def edit_role( ] if permissions: if len(perms_added) > 0: - changes.append('\n- Permissions added: {}'.format( + i18n_added = I18N.t( + 'roles.commands.edit.changelist_perms_added' + ) + changes.append('\n- {}: {}'.format( + i18n_added, ', '.join(perms_added) )) if len(perms_removed) > 0: - changes.append('\n- Permissions removed: {}'.format( + i18n_removed = I18N.t( + 'roles.commands.edit.changelist_perms_removed' + ) + changes.append('\n- {}: {}'.format( + i18n_removed, ', '.join(perms_removed) )) if len(changes) > 0: - changes_out = f'Did following changes on role `{role_name.name}`:' + changes_out = '{}:'.format( + I18N.t( + 'roles.commands.edit.changes_out', + role_name=role_name.name + ) + ) for change in changes: changes_out += change await interaction.followup.send( @@ -904,22 +1019,19 @@ async def edit_role( commands.is_owner(), commands.has_permissions(manage_roles=True) ) + @describe( + reaction_msg=I18N.t('roles.commands.react_list.desc.reaction_msg') + ) @discord.app_commands.autocomplete(reaction_msg=reaction_msgs_autocomplete) @roles_reaction_group.command( - name='list', description='List all reactions' + name='list', description=locale_str(I18N.t( + 'roles.commands.react_list.cmd' + )) ) async def list_reactions( self, interaction: discord.Interaction, reaction_msg: str = None ): - ''' - List reactions - - Parameters - ------------ - reaction_msg_name: str - The names of the reaction message to list (default: None) - ''' await interaction.response.defer(ephemeral=True) if reaction_msg: db_reactions = await db_helper.get_combined_output( @@ -931,36 +1043,65 @@ async def list_reactions( 'content', 'channel', 'A.msg_id', - 'role_name', + 'role', 'emoji' ], where=[ ('A.msg_id', reaction_msg) ] ) - if len(db_reactions) <= 0: + if len(db_reactions) <= 0 or db_reactions is None: await interaction.followup.send( - f'Did not find `{reaction_msg}`' + I18N.t( + 'roles.commands.react_list.msg_error', + reaction_msg=reaction_msg + ) ) return tabulate_dict = { - 'role': [], - 'emoji': [] + 'emoji_id': [], + 'emoji_name': [], + 'role_id': [], + 'role_name': [] } for reaction in db_reactions: - tabulate_dict['role'].append(reaction[4]) - tabulate_dict['emoji'].append(reaction[5]) - # TODO i18n? + if re.match(r'\d{19,22}', reaction[4]): + role = get( + discord_commands.get_guild().roles, + id=int(reaction[4]) + ) + role_name = role.name + role_id = role.id + else: + role_name = I18N.t('roles.commands.react_list.role_error') + role_id = reaction[4] + if re.match(r'\d{19,22}', reaction[5]): + emoji = get( + discord_commands.get_guild().emojis, + id=int(reaction[5]) + ) + emoji_name = emoji.name + emoji_id = emoji.id + else: + emoji_name = I18N.t( + 'roles.commands.react_list.emoji_error') + emoji_id = reaction[5] + tabulate_dict['emoji_id'].append(emoji_id) + tabulate_dict['emoji_name'].append(emoji_name) + tabulate_dict['role_id'].append(role_id) + tabulate_dict['role_name'].append(role_name) await interaction.followup.send( - 'Navn: `{}`\nKanal: `{}`\nMeldings-ID: `{}`\n' - 'Tekst: `{}`\n\n```{}```'.format( - db_reactions[0][0], - db_reactions[0][1], - db_reactions[0][2], - db_reactions[0][3], - tabulate(tabulate_dict, headers=['Rolle', 'Emoji']) + '{}: `{}`\n{}: `{}`\n{}: `{}`\n' + '{}: `{}`'.format( + I18N.t('common.name'), db_reactions[0][0], + I18N.t('common.channel'), db_reactions[0][1], + I18N.t('common.message_id'), db_reactions[0][2], + I18N.t('common.text'), db_reactions[0][3] ) ) + pages = tabulate_emojis_and_roles(tabulate_dict) + for page in pages: + await interaction.followup.send(page) elif not reaction_msg: tabulate_dict = { 'name': [], @@ -1006,10 +1147,13 @@ async def list_reactions( await interaction.followup.send( '```{}```'.format( tabulate( - # TODO i18n? tabulate_dict, headers=[ - 'Navn', 'Kanal', 'Rekkefølge', 'ID', 'Tekst', - 'Ant. reaksj.' + I18N.t(''), I18N.t('common.name'), + I18N.t('common.channel'), + I18N.t('common.order'), + I18N.t('common.id'), + I18N.t('common.text'), + I18N.t('common.num_reactions') ] ) ) @@ -1017,7 +1161,18 @@ async def list_reactions( return @roles_reaction_add_group.command( - name='message', description='Add reaction message' + name='message', description=locale_str(I18N.t( + 'roles.commands.add_reaction_msg.cmd' + )) + ) + @describe( + msg_name=I18N.t('roles.commands.add_reaction_msg.desc.msg_name'), + message_text=I18N.t('roles.commands.add_reaction_msg.desc.msg_text'), + order=I18N.t('roles.commands.add_reaction_msg.desc.order'), + channel=I18N.t('roles.commands.add_reaction_msg.desc.channel'), + roles=I18N.t('roles.splits.roles'), + emojis=I18N.t('roles.splits.emojis'), + header=I18N.t('roles.commands.add_reaction_msg.desc.header') ) async def add_reaction_message( self, interaction: discord.Interaction, @@ -1027,25 +1182,6 @@ async def add_reaction_message( ): ''' Add a reaction message - - Parameters - ------------ - msg_name: str - Name of the message for the reaction roles - message_text: str - The text for the message - channel: discord.TextChannel - Channel to post reaction message to - order: int - Set order for the message in the channel - roles: str - Tagged roles separated by any of the following characers: - " .,;-_\\/" - emojis: str - Tagged emojis separated by any of the following characers: - " .,;-_\\/" - header: str - Header for the message ''' await interaction.response.defer(ephemeral=True) msg_db_orders = await db_helper.get_output( @@ -1054,10 +1190,12 @@ async def add_reaction_message( select=('msg_order', 'name') ) if order in [msg_order[0] for msg_order in msg_db_orders]: - # TODO var msg await interaction.followup.send( - f'That order number already exist for {channel}, ' - f'try {len(msg_db_orders)+1}' + I18N.t( + 'roles.commands.add_reaction_msg.msg_order_exist', + channel=channel, + num=len(msg_db_orders)+1 + ) ) return msg_db = await db_helper.get_output( @@ -1068,9 +1206,12 @@ async def add_reaction_message( select=('name') ) if len(msg_db) == 1: - # TODO var msg await interaction.followup.send( - f'Reaction message `{msg_name}` is already registered...' + I18N.t( + 'roles.commands.add_reaction_msg.' + 'msg_reaction_already_exist', + msg_name=msg_name + ) ) return desc_out = '' @@ -1082,7 +1223,9 @@ async def add_reaction_message( ) if merged_roles_emojis is None: await interaction.followup.send( - 'Could not find any roles or emojis' + I18N.t( + 'roles.commands.add_reaction_msg.msg_roles_emojis_error' + ) ) return for combo in merged_roles_emojis: @@ -1155,7 +1298,8 @@ async def add_reaction_message( ] ) await interaction.followup.send( - 'Message added', ephemeral=True + I18N.t('roles.commands.add_reaction_msg.msg_confirm'), + ephemeral=True ) return @@ -1164,7 +1308,15 @@ async def add_reaction_message( commands.has_permissions(manage_roles=True) ) @roles_reaction_add_group.command( - name='role', description='Add roles to a reaction message') + name='role', description=locale_str(I18N.t( + 'roles.commands.add_reaction_role.cmd' + )) + ) + @describe( + msg_name=I18N.t('roles.commands.add_reaction_role.desc.msg_name'), + roles=I18N.t('roles.splits.roles'), + emojis=I18N.t('roles.splits.emojis') + ) @discord.app_commands.autocomplete( msg_name=reaction_msgs_autocomplete ) @@ -1174,15 +1326,6 @@ async def add_reaction_role( ): ''' Add reaction roles to an existing message - - Parameters - ------------ - msg_name: int/str - Name of the saved message - roles: str - The roles to add - emojis: str - The emojis to add ''' await interaction.response.defer(ephemeral=True) msg_info = await get_msg_id_and_name(msg_name) @@ -1208,7 +1351,8 @@ async def add_reaction_role( ) await sync_reaction_message_from_settings(msg_name) await interaction.followup.send( - 'Roles added', ephemeral=True + I18N.t('roles.commands.add_reaction_role.msg_confirm'), + ephemeral=True ) return @@ -1217,20 +1361,19 @@ async def add_reaction_role( commands.has_permissions(manage_roles=True) ) @discord.app_commands.autocomplete(reaction_msg=reaction_msgs_autocomplete) + @describe( + reaction_msg=I18N.t('roles.commands.sync.desc.reaction_msg') + ) @roles_reaction_group.command( - name='sync', description='Synchronize reactions messages with database' + name='sync', description=locale_str(I18N.t( + 'roles.commands.sync.cmd' + )) ) async def sync_reaction_items( self, interaction: discord.Interaction, reaction_msg: str ): ''' Synchronize a reaction message with the database - - Parameters - ------------ - reaction_msg: int/str - The message ID to look for, or name of the saved message in - database ''' await interaction.response.defer(ephemeral=True) sync_errors = await sync_reaction_message_from_settings(reaction_msg) @@ -1240,7 +1383,8 @@ async def sync_reaction_items( ) else: await interaction.followup.send( - 'Reaction message synced', ephemeral=True + I18N.t('roles.commands.sync.confirm_sync'), + ephemeral=True ) return @@ -1249,17 +1393,15 @@ async def sync_reaction_items( commands.has_permissions(manage_roles=True) ) @discord.app_commands.autocomplete(reaction_msg=reaction_msgs_autocomplete) + @describe( + reaction_msg=I18N.t('roles.commands.sort.desc.reaction_msg') + ) @roles_reaction_group.command(name='sort') async def sort_reaction_items( self, interaction: discord.Interaction, reaction_msg: str ): ''' Sort items in a reaction message alphabetically - - Parameters - ------------ - reaction_msg: str - The message ID from database ''' await interaction.response.defer(ephemeral=True) # Get message object @@ -1268,9 +1410,11 @@ async def sort_reaction_items( msg_info['id'], msg_info['channel'] ) if _msg is None: - # TODO var msg await interaction.followup.send( - 'Could not find reaction message' + I18N.t( + 'roles.commands.sort.msg_error', + reaction_msg=reaction_msg + ) ) return sync_errors = await sync_reaction_message_from_settings( @@ -1283,7 +1427,8 @@ async def sort_reaction_items( ) else: await interaction.followup.send( - 'Roles sorted', ephemeral=True + I18N.t('roles.commands.sort.msg_confirm'), + ephemeral=True ) return @@ -1313,9 +1458,11 @@ async def remove_reaction_message( msg_info['id'], msg_info['channel'] ) if _msg is None: - # TODO var msg await interaction.followup.send( - 'Could not find reaction message' + I18N.t( + 'roles.commands.remove_msg.msg_error', + reaction_msg=reaction_msg + ) ) return # Remove reaction message from database @@ -1327,9 +1474,8 @@ async def remove_reaction_message( ) # Remove message from guild await _msg.delete() - # TODO var msg await interaction.followup.send( - 'Reaction message removed' + I18N.t('roles.commands.remove_msg.msg_confirm') ) return @@ -1338,7 +1484,13 @@ async def remove_reaction_message( commands.has_permissions(manage_roles=True) ) @roles_reaction_remove_group.command( - name='role', description='Remove a reaction from reaction message' + name='role', description=locale_str(I18N.t( + 'roles.commands.remove_role.cmd' + )) + ) + @describe( + reaction_msg=I18N.t('roles.commands.remove_role.desc.reaction_msg'), + role_name=I18N.t('roles.commands.remove_role.desc.role_name') ) @discord.app_commands.autocomplete(reaction_msg=reaction_msgs_autocomplete) async def remove_reaction_role( @@ -1347,32 +1499,27 @@ async def remove_reaction_role( ): ''' Remove a reaction from reaction message - - Parameters - ------------ - msg_id_or_name: int/str - The message ID to look for, or name of the saved message in - settings file - role_name: str - Name of a role that is connected to a reaction - in the message ''' await interaction.response.defer(ephemeral=True) # Get message object msg_info = await get_msg_id_and_name(reaction_msg) # Delete reaction from db - role_name = f'<@&{role_name.id}>' + role_name_out = f'<@&{role_name.id}>' await db_helper.del_row_by_AND_filter( template_info=envs.roles_db_roles_schema, where=[ ('msg_id', msg_info['id']), - ('role_name', role_name) + ('role_name', role_name_out) ] ) # Sync settings await sync_reaction_message_from_settings(reaction_msg) await interaction.followup.send( - 'Role removed', ephemeral=True + I18N.t( + 'roles.commands.remove_role.msg_confirm', + rolename=role_name.name + ), + ephemeral=True ) return @@ -1381,8 +1528,12 @@ async def remove_reaction_role( commands.has_permissions(manage_roles=True) ) @roles_reaction_group.command( - name='reorder', description='Check reaction messages order in a ' - 'discord channel and recreate them based on settings' + name='reorder', description=locale_str(I18N.t( + 'roles.commands.reorder.cmd' + )) + ) + @describe( + channel=I18N.t('roles.commands.reorder.desc.channel') ) async def reorder_reaction_messages( self, interaction: discord.Interaction, channel: discord.TextChannel @@ -1390,11 +1541,6 @@ async def reorder_reaction_messages( ''' Check reaction messages order in a discord channel and recreate them based on settings - - Parameters - ------------ - channel: str - What channel to check ''' async def update_msg_id(old_msg, new_msg): # Update msg id in both dbs @@ -1430,10 +1576,9 @@ async def update_msg_id(old_msg, new_msg): # Check order of messages if len(discord_msgs) != len(react_msgs): - # TODO var msg log.log( - 'Antall reaksjonsmeldinger i {} og database stemmer ' - 'ikke overens ({} vs {})'.format( + 'Number of reaction messages in {} and database are not' + 'the same nubmer ({} vs {})'.format( channel, len(discord_msgs), len(react_msgs) ) ) @@ -1454,6 +1599,7 @@ async def update_msg_id(old_msg, new_msg): # Delete the old message and recreate messages for react_msg in react_msgs: log.verbose(f'Getting object for react_msg: {react_msg}') + # TODO convert to `from discord.utils import get` old_react_msg = await discord_commands.get_message_obj( react_msg[0], react_msg[1] @@ -1475,14 +1621,12 @@ async def update_msg_id(old_msg, new_msg): await sync_reaction_message_from_settings( str(new_reaction_msg.id) ) - # TODO i18n await interaction.followup.send( - 'Reaction messages reordered' + I18N.t('roles.commands.reorder.msg_confirm') ) else: - # TODO i18n await interaction.followup.send( - 'Messages are already in correct order' + I18N.t('roles.commands.reorder.msg_already_sorted') ) return @@ -1491,25 +1635,27 @@ async def update_msg_id(old_msg, new_msg): commands.has_permissions(manage_roles=True) ) @roles_settings_group.command( - name='add', description='Add a setting for roles on the server' + name='add', description=locale_str(I18N.t( + 'roles.commands.settings_add.cmd' + )) + ) + @describe( + setting=I18N.t('roles.commands.settings_add.desc.setting'), + role_in=I18N.t('roles.commands.settings_add.desc.role_in') ) async def add_settings( self, interaction: discord.Interaction, - setting: typing.Literal['Unique role', 'Not include in total'], + setting: typing.Literal[ + I18N.t('roles.commands.settings_add.literal.setting.unique'), + I18N.t('roles.commands.settings_add.literal.setting.not_include_in_total') + ], role_in: discord.Role ): ''' Add a setting for roles on the server - - Parameters - ------------ - setting: typing.Literal - The setting to add - role_in: discord.Role - The role to add to the setting ''' await interaction.response.defer(ephemeral=True) - if setting == 'Unique role': + if setting == I18N.t('roles.commands.settings_add.literal.setting.unique'): _setting = 'unique' # TODO Add a check, unique should only appear once unique = await db_helper.get_output( @@ -1520,11 +1666,10 @@ async def add_settings( ) if unique: await interaction.followup.send( - # TODO i18n - 'Unique role already set, remove or edit it' + I18N.t('roles.commands.settings_add.role_already_set') ) return - elif setting == 'Not include in total': + elif setting == I18N.t('roles.commands.settings_add.literal.setting.not_include_in_total'): _setting = 'not_include_in_total' await db_helper.insert_many_all( template_info=envs.roles_db_settings_schema, @@ -1532,8 +1677,9 @@ async def add_settings( (_setting, str(role_in.id)) ] ) - # TODO i18n - await interaction.followup.send('Added setting') + await interaction.followup.send( + I18N.t('roles.commands.settings_add.msg_confirm') + ) return @commands.check_any( @@ -1542,7 +1688,12 @@ async def add_settings( ) @discord.app_commands.autocomplete(setting=settings_autocomplete) @roles_settings_group.command( - name='remove', description='Remove a setting for roles on the server' + name='remove', description=locale_str(I18N.t( + 'roles.commands.settings_remove.cmd' + )) + ) + @describe( + setting=I18N.t('roles.commands.settings_remove.desc.setting') ) async def remove_settings( self, interaction: discord.Interaction, @@ -1550,11 +1701,6 @@ async def remove_settings( ): ''' Remove a setting for roles on the server - - Parameters - ------------ - setting: str - The setting to remove ''' await interaction.response.defer(ephemeral=True) log.debug(f'Got row_id `{setting}`') @@ -1562,8 +1708,9 @@ async def remove_settings( template_info=envs.roles_db_settings_schema, numbers=setting ) - # TODO i18n - await interaction.followup.send('Removed setting') + await interaction.followup.send( + I18N.t('roles.commands.settings_remove.msg_confirm'), + ) return @commands.check_any( @@ -1571,7 +1718,9 @@ async def remove_settings( commands.has_permissions(manage_roles=True) ) @roles_settings_group.command( - name='list', description='List settings for roles on the server' + name='list', description=locale_str(I18N.t( + 'roles.commands.settings_list.cmd' + )) ) async def list_settings( self, interaction: discord.Interaction @@ -1593,10 +1742,11 @@ async def list_settings( _settings_db_expanded.append( (setting[0], _role, setting[1]) ) - _settings = tabulate( - _settings_db_expanded, headers=['Setting', 'Role', 'Value'] - ) - # TODO i18n + _settings = tabulate(_settings_db_expanded, headers=[ + I18N.t('roles.commands.settings_list.headers.setting'), + I18N.t('roles.commands.settings_list.headers.role'), + I18N.t('roles.commands.settings_list.headers.value') + ]) await interaction.followup.send(f'```{_settings}```') return @@ -1606,7 +1756,13 @@ async def list_settings( ) @discord.app_commands.autocomplete(setting=settings_autocomplete) @roles_settings_group.command( - name='edit', description='Edit a setting for roles on the server' + name='edit', description=locale_str(I18N.t( + 'roles.commands.settings_edit.cmd' + )) + ) + @describe( + setting=I18N.t('roles.commands.settings_edit.desc.setting'), + role_in=I18N.t('roles.commands.settings_edit.desc.role_in') ) async def edit_settings( self, interaction: discord.Interaction, @@ -1614,13 +1770,6 @@ async def edit_settings( ): ''' Edit a setting for roles on the server - - Parameters - ------------ - setting: str - The setting to edit - role_in: discord.Role - The role to add to the setting ''' await interaction.response.defer(ephemeral=True) await db_helper.update_fields( @@ -1628,9 +1777,11 @@ async def edit_settings( where=('setting', setting), updates=('value', str(role_in.id)) ) - # TODO i18n await interaction.followup.send( - f'Edited setting ({setting} = {role_in.id})' + I18N.t( + 'roles.commands.settings_edit.confirm_msg', + setting=setting, role_in=role_in.id + ) ) return @@ -1696,7 +1847,6 @@ async def setup(bot): # Add roles to users from reactions @config.bot.event async def on_raw_reaction_add(payload): - # TODO var msg log.debug('Checking added reaction role') if str(payload.user_id) == str(config.BOT_ID): log.debug('Change made by bot, skip') @@ -1710,7 +1860,6 @@ async def on_raw_reaction_add(payload): _guild = discord_commands.get_guild() for reaction_message in reaction_messages: if str(payload.message_id) == str(reaction_message[0]): - # TODO var msg log.debug('Found message, checking add reactions...') reactions = await db_helper.get_combined_output( envs.roles_db_roles_schema, @@ -1746,8 +1895,9 @@ async def on_raw_reaction_add(payload): discord_commands.get_guild().roles, id=int(reaction[1]) ), - reason='Added in accordance with ' - 'reaction messages' + reason=I18N.t( + 'roles.cogs.on_raw_reaction_add.channel_log_confirm' + ) ) break else: @@ -1758,7 +1908,6 @@ async def on_raw_reaction_add(payload): # Remove roles from users from reactions @config.bot.event async def on_raw_reaction_remove(payload): - # TODO var msg log.debug('Checking removed reaction role') if str(payload.user_id) == str(config.BOT_ID): log.debug('Change made by bot, skip') @@ -1772,7 +1921,6 @@ async def on_raw_reaction_remove(payload): _guild = discord_commands.get_guild() for reaction_message in reaction_messages: if str(payload.message_id) == str(reaction_message[0]): - # TODO var msg log.debug('Found message, checking remove reactions...') reactions = await db_helper.get_combined_output( envs.roles_db_roles_schema, @@ -1814,9 +1962,9 @@ async def on_raw_reaction_remove(payload): payload.user_id ).remove_roles( _role, - reason='Removed in accordance with ' - 'reaction message ' - f'{reaction_message}' + reason=I18N.t( + 'roles.cogs.on_raw_reaction_remove.channel_log_confirm' + ) ) break return @@ -1839,11 +1987,9 @@ async def on_member_update(before, after): ) log.debug(f'Got `unique_role`: {unique_role}') if not unique_role or unique_role == '': - # TODO var msg log.log('No unique role provided or setting is not string') return if unique_role: - # TODO var msg log.debug('Check for unique role') if str(before.id) == str(config.BOT_ID): log.debug('Change made by bot, skip') @@ -1872,7 +2018,6 @@ async def on_member_update(before, after): where=('setting', 'not_include_in_total') ) if len(not_include_in_total) > 0: - # TODO var msg log.debug('Found roles not to include in total') _before -= len(not_include_in_total) _after -= len(not_include_in_total) @@ -1884,13 +2029,11 @@ async def on_member_update(before, after): log.verbose(f'_before: {_before}') log.verbose(f'_after: {_after}') if int(_after) <= 0: - # TODO var msg log.debug('Length of _after is 0, adding unique role') await after.add_roles( get(_guild.roles, id=int(unique_role)) ) elif int(_after) > 1: - # TODO var msg log.debug( 'Length of after.roles is more than 1, removing unique role' ) diff --git a/sausage_bot/cogs/rss.py b/sausage_bot/cogs/rss.py index 562d17b..e015e9c 100755 --- a/sausage_bot/cogs/rss.py +++ b/sausage_bot/cogs/rss.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -from discord.ext import commands, tasks import discord +from discord.ext import commands, tasks +from discord.app_commands import locale_str, describe import typing from time import sleep import re from sausage_bot.util import config, envs, feeds_core, file_io, net_io from sausage_bot.util import db_helper, discord_commands +from sausage_bot.util.i18n import I18N from sausage_bot.util.log import log from sausage_bot.util.args import args @@ -80,19 +82,19 @@ def __init__(self, bot): super().__init__() rss_group = discord.app_commands.Group( - name="rss", description='Administer RSS-feeds' + name="rss", description=locale_str(I18N.t('rss.groups.rss')) ) rss_filter_group = discord.app_commands.Group( - name="filter", description='Manage RSS filters', + name="filter", description=locale_str(I18N.t('rss.groups.filter')), parent=rss_group ) rss_posting_group = discord.app_commands.Group( - name="posting", description='Posting from RSS feeds', + name="posting", description=locale_str(I18N.t('rss.groups.posting')), parent=rss_group ) @rss_posting_group.command( - name='start', description='Start posting' + name='start', description=locale_str(I18N.t('rss.commands.start.cmd')) ) async def rss_posting_start( self, interaction: discord.Interaction @@ -109,11 +111,11 @@ async def rss_posting_start( updates=('status', 'started') ) await interaction.followup.send( - 'RSS posting started' + I18N.t('rss.commands.start.msg_confirm') ) @rss_posting_group.command( - name='stop', description='Stop posting' + name='stop', description=locale_str(I18N.t('rss.commands.stop.cmd')) ) async def rss_posting_stop( self, interaction: discord.Interaction @@ -130,7 +132,7 @@ async def rss_posting_stop( updates=('status', 'stopped') ) await interaction.followup.send( - 'RSS posting stopped' + I18N.t('rss.commands.stop.msg_confirm') ) @commands.check_any( @@ -139,56 +141,46 @@ async def rss_posting_stop( ) @discord.app_commands.autocomplete(feed_name=feed_name_autocomplete) @rss_group.command( - name='add', description='Add a RSS feed' + name='add', description=locale_str(I18N.t('rss.commands.add.cmd')) + ) + @describe( + feed_name=I18N.t('rss.commands.add.desc.feed_name'), + feed_link=I18N.t('rss.commands.add.desc.feed_link'), + channel=I18N.t('rss.commands.add.desc.channel') ) async def rss_add( self, interaction: discord.Interaction, feed_name: str, feed_link: str, channel: discord.TextChannel ): - '''Add a RSS feed - - Parameters - ------------ - feed_name: str - The name of the feed to add - feed_link: str - Link to the RSS-/XML-feed (default: None) - channel: discord.TextChannel - The channel to post from the feed - ''' + '''Add a RSS feed''' await interaction.response.defer(ephemeral=True) AUTHOR = interaction.user.name # Verify that the url is a proper feed if "open.spotify.com/show/" not in feed_link: valid_feed = await feeds_core.check_feed_validity(feed_link) if not valid_feed: - if not args.rss_skip_url_validation: - # TODO var msg - await interaction.followup.send( - 'Urlen er ikke en RSS/XML feed', ephemeral=True - ) - return - else: - pass - elif isinstance(valid_feed, int): - if not args.rss_skip_url_validation: - await interaction.followup.send( - f'Urlen gir feilkode {valid_feed}', ephemeral=True - ) - return - else: - pass + # TODO var msg + await interaction.followup.send( + I18N.t('rss.commands.add.msg_feed_failed'), + ephemeral=True + ) + return log.verbose('Adding feed to db') await feeds_core.add_to_feed_db( 'spotify', str(feed_name), str(feed_link), channel.name, AUTHOR ) await discord_commands.log_to_bot_channel( - envs.RSS_ADDED_BOT.format( - AUTHOR, feed_name, feed_link, channel.name + I18N.t( + 'rss.commands.add.log_feed_confirm', + user_name=AUTHOR, feed_name=feed_name, + channel_name=channel.name ) ) await interaction.followup.send( - envs.RSS_ADDED.format(feed_name, channel.name), + I18N.t( + 'rss.commands.add.msg_feed_confirm', + feed_name=feed_name, channel_name=channel.name + ), ephemeral=True ) return @@ -199,18 +191,17 @@ async def rss_add( ) @discord.app_commands.autocomplete(feed_name=feed_name_autocomplete) @rss_group.command( - name='remove', description='Remove a RSS feed' + name='remove', description=locale_str(I18N.t( + 'rss.commands.remove.cmd' + )) + ) + @describe( + feed_name=I18N.t('rss.commands.remove.desc.feed_name') ) async def rss_remove( self, interaction: discord.Interaction, feed_name: str ): - '''Remove a RSS feed - - Parameters - ------------ - feed_name: str - The name of the feed to remove - ''' + '''Remove a RSS feed''' await interaction.response.defer() AUTHOR = interaction.user.name removal = await feeds_core.remove_feed_from_db( @@ -218,19 +209,31 @@ async def rss_remove( ) if removal: await discord_commands.log_to_bot_channel( - envs.RSS_REMOVED_BOT.format(feed_name, AUTHOR) + I18N.t( + 'rss.commands.remove.log_feed_removed', + feed_name=feed_name, user_name=AUTHOR + ) ) await interaction.followup.send( - envs.RSS_REMOVED.format(feed_name) + I18N.t( + 'rss.commands.remove.msg_feed_removed', + feed_name=feed_name + ) ) elif removal is False: # Couldn't remove the feed await interaction.followup.send( - envs.RSS_COULD_NOT_REMOVE.format(feed_name) + I18N.t( + 'rss.commands.remove.msg_feed_remove_failed', + feed_name=feed_name + ) ) # Also log and send error to bot-channel await discord_commands.log_to_bot_channel( - envs.RSS_TRIED_REMOVED_BOT.format(AUTHOR, feed_name) + I18N.t( + 'rss.commands.remove.log_feed_remove_failed', + user_name=AUTHOR, feed_name=feed_name + ) ) return @@ -240,7 +243,15 @@ async def rss_remove( ) @discord.app_commands.autocomplete(feed_name=feed_name_autocomplete) @rss_group.command( - name='edit', description='Edit a RSS feed' + name='edit', description=locale_str(I18N.t( + 'rss.commands.edit.cmd' + )) + ) + @describe( + feed_name=I18N.t('rss.commands.edit.desc.feed_name'), + new_feed_name=I18N.t('rss.commands.edit.desc.new_feed_name'), + channel=I18N.t('rss.commands.edit.desc.channel'), + url=I18N.t('rss.commands.edit.desc.url') ) async def rss_edit( self, interaction: discord.Interaction, @@ -254,18 +265,32 @@ async def rss_edit( where=(('feed_name', feed_name)) ) log.debug(f'`feed_info` is {feed_info}') - changes_out = f'Did following changes on feed `{feed_name}`:' + changes_out = I18N.t( + 'rss.commands.edit.changes_out.msg', + feed_name=feed_name + ) updates_in = [] if new_feed_name: updates_in.append(('feed_name', new_feed_name)) - changes_out += f'\n- Feed name: `{feed_info[0][0]}` -> '\ - '`{new_feed_name}`' + changes_out += '\n- {}: `{}` -> `{}`'.format( + I18N.t('rss.commands.edit.changes_out.feed_name'), + feed_info[0][0], + new_feed_name + ) if channel: updates_in.append(('channel', channel)) - changes_out += f'\n- Channel: `{feed_info[0][1]}` -> `{channel}`' + changes_out += '\n- {}: `{}` -> `{}`'.format( + I18N.t('rss.commands.edit.changes_out.channel'), + feed_info[0][1], + channel + ) if url: updates_in.append(('url', url)) - changes_out += f'\n- Channel: `{feed_info[0][2]}` -> `{url}`' + changes_out += '\n- {}: `{}` -> `{}`'.format( + I18N.t('rss.commands.edit.changes_out.url'), + feed_info[0][2], + url + ) await db_helper.update_fields( template_info=envs.rss_db_schema, where=('feed_name', feed_name), @@ -282,26 +307,22 @@ async def rss_edit( ) @discord.app_commands.autocomplete(feed_name=feed_name_autocomplete) @rss_filter_group.command( - name='add', description='Add filters on a RSS feed' + name='add', description=locale_str(I18N.t('rss.commands.filter_add.cmd')) + ) + @describe( + feed_name=I18N.t('rss.commands.filter_add.desc.feed_name'), + allow_deny=I18N.t('rss.commands.filter_add.desc.allow_deny'), + filters_in=I18N.t('rss.commands.filter_add.desc.filters_in') ) async def rss_filter_add( self, interaction: discord.Interaction, feed_name: str, - allow_deny: typing.Literal['Allow', 'Deny'], filters_in: str + allow_deny: typing.Literal[ + I18N.t('rss.commands.filter_add.desc.allow_deny.allow'), + I18N.t('rss.commands.filter_add.desc.allow_deny.deny') + ], filters_in: str ): ''' Add filter for feed (deny/allow) - - Parameters - ------------ - feed_name: str - Name of feed - allow_deny: str - Specify if the filter should `allow` or `deny`. Separate multiples - with any of the following characers: " .,;-_\\/" - filters_in: str - What to filter a post by. Separate multiple with any of the - following characers: " .,;-_\\/" - ''' await interaction.response.defer(ephemeral=True) # Make sure that the filter input can be split @@ -322,13 +343,16 @@ async def rss_filter_add( inserts=temp_inserts ) if adding_filter: - msg_out = f'Added filters as {allow_deny}:' + msg_out = I18N.t( + 'rss.commands.filter_add.msg_confirm', + allow_deny=allow_deny + ) for filter in _filters_in: msg_out += f'\n- {filter}' await interaction.followup.send(msg_out, ephemeral=True) else: await interaction.followup.send( - 'Error when adding filter, check logs', + I18N.t('rss.commands.filter_add.msg_error'), ephemeral=True ) return @@ -340,20 +364,19 @@ async def rss_filter_add( @discord.app_commands.autocomplete(feed_name=feed_name_autocomplete) @discord.app_commands.autocomplete(filter_in=rss_filter_autocomplete) @rss_filter_group.command( - name='remove', description='Remove filters on a RSS feed' + name='remove', description=locale_str(I18N.t( + 'rss.commands.filter_remove.cmd' + )) + ) + @describe( + feed_name=I18N.t('rss.commands.filter_remove.desc.feed_name'), + filter_in=I18N.t('rss.commands.filter_remove.desc.filter') ) async def rss_filter_remove( self, interaction: discord.Interaction, feed_name: str, filter_in: str ): ''' Remove filter for feed - - Parameters - ------------ - feed_name: str - Name of feed - filter_in: str - What filter to look for ''' await interaction.response.defer(ephemeral=True) _uuid = await db_helper.get_output( @@ -362,13 +385,6 @@ async def rss_filter_remove( where=(('feed_name', feed_name)), single=True ) - if _uuid is None: - _error_msg = f'The feed `{feed_name}` does not exist' - log.debug(_error_msg) - await interaction.followup.send( - _error_msg, ephemeral=True - ) - return removing_filter = await db_helper.del_row_by_AND_filter( template_info=envs.rss_db_filter_schema, where=( @@ -378,12 +394,18 @@ async def rss_filter_remove( ) if removing_filter: await interaction.followup.send( - f'Removed filter `{filter_in}`', + I18N.t( + 'rss.commands.filter_remove.msg_confirm', + filter=filter_in + ), ephemeral=True ) else: await interaction.followup.send( - f'Error when removing filter `{filter_in}`, check logs', + I18N.t( + 'rss.commands.filter_remove.msg_error', + filter=filter_in + ), ephemeral=True ) return @@ -393,11 +415,18 @@ async def rss_filter_remove( commands.has_permissions(administrator=True) ) @rss_group.command( - name='list', description='List all active rss feeds' + name='list', description=locale_str(I18N.t('rss.commands.list.cmd')) + ) + @describe( + list_type=I18N.t('rss.commands.list.desc.list_type') ) async def list_rss( self, interaction: discord.Interaction, - list_type: typing.Literal['Normal', 'Added', 'Filter'] + list_type: typing.Literal[ + I18N.t('rss.commands.list.literal_type.normal'), + I18N.t('rss.commands.list.literal_type.added'), + I18N.t('rss.commands.list.literal_type.filter') + ] ): ''' List all active rss feeds @@ -430,7 +459,8 @@ async def list_rss( sleep(1) else: await interaction.followup.send( - 'No feeds added', ephemeral=True + I18N.t('rss.commands.list.msg_error'), + ephemeral=True ) return diff --git a/sausage_bot/cogs/stats.py b/sausage_bot/cogs/stats.py index 89aa5f9..3176d31 100755 --- a/sausage_bot/cogs/stats.py +++ b/sausage_bot/cogs/stats.py @@ -2,6 +2,7 @@ # -*- coding: UTF-8 -*- import os from discord.ext import commands, tasks +from discord.app_commands import locale_str import discord from discord.utils import get from tabulate import tabulate @@ -9,6 +10,7 @@ from sausage_bot.util import envs, datetime_handling, file_io, config from sausage_bot.util import discord_commands, db_helper +from sausage_bot.util.i18n import I18N from sausage_bot.util.log import log @@ -37,9 +39,9 @@ async def hidden_roles_autocomplete( current: str, ) -> list[discord.app_commands.Choice[str]]: hidden_roles_in_db = await db_helper.get_output( - template_info=envs.stats_db_hide_roles_schema, - get_row_ids=True - ) + template_info=envs.stats_db_hide_roles_schema, + get_row_ids=True + ) hidden_roles_in_list = [] for role in hidden_roles_in_db: hidden_roles_in_list.append( @@ -103,21 +105,24 @@ def __init__(self, bot): super().__init__() stats_group = discord.app_commands.Group( - name="stats", description='Administer stats on the server' + name="stats", + description=locale_str(I18N.t('stats.commands.groups.stats')) ) stats_posting_group = discord.app_commands.Group( - name="posting", description='Posting stats', + name="posting", + description=locale_str(I18N.t('stats.commands.groups.posting')), parent=stats_group ) @stats_posting_group.command( - name='start', description='Start posting' + name='start', + description=locale_str(I18N.t('stats.commands.start.command')) ) async def stats_posting_start( self, interaction: discord.Interaction ): await interaction.response.defer(ephemeral=True) - log.log('Task started') + log.log(I18N.t('stats.commands.start.log_started')) Stats.update_stats.start() await db_helper.update_fields( template_info=envs.tasks_db_schema, @@ -128,18 +133,19 @@ async def stats_posting_start( updates=('status', 'started') ) await interaction.followup.send( - 'Stats posting started' + I18N.t('stats.commands.start.confirm_started') ) @stats_posting_group.command( - name='stop', description='Stop posting' + name='stop', + description=locale_str(I18N.t('stats.commands.stop.command')) ) async def stats_posting_stop( self, interaction: discord.Interaction, remove_post: typing.Literal['Yes', 'No'] ): await interaction.response.defer(ephemeral=True) - log.log('Task stopped') + log.log(I18N.t('stats.commands.stop.log_stopped')) Stats.update_stats.cancel() await db_helper.update_fields( template_info=envs.tasks_db_schema, @@ -162,20 +168,21 @@ async def stats_posting_stop( stats_channel = 'stats' await discord_commands.remove_stats_post(stats_channel) await interaction.followup.send( - 'Stats posting stopped' + I18N.t('commands.stop.confirm_stopped') ) @stats_posting_group.command( - name='restart', description='Restart posting' + name='restart', + description=locale_str(I18N.t('stats.commands.restart.command')) ) async def stats_posting_restart( self, interaction: discord.Interaction ): await interaction.response.defer(ephemeral=True) - log.log('Task restarted') + log.log('Stats posting restarted') Stats.update_stats.restart() await interaction.followup.send( - 'Stats posting restarted' + I18N.t('commands.restart.log_restarted') ) @commands.check_any( @@ -183,7 +190,8 @@ async def stats_posting_restart( commands.has_permissions(administrator=True) ) @stats_group.command( - name='list', description='List the available settings for this cog' + name='list', + description=locale_str(I18N.t('stats.commands.list.command')) ) async def list_settings( self, interaction: discord.Interaction @@ -196,8 +204,13 @@ async def list_settings( template_info=envs.stats_db_settings_schema, select=('setting', 'value', 'value_help') ) - headers_settings = ['Setting', 'Value', 'Value type'] - out = '## Settings\n```{}```'.format( + headers_settings = [ + I18N.t('stats.commands.list.headers.settings.setting'), + I18N.t('stats.commands.list.headers.settings.value'), + I18N.t('stats.commands.list.headers.settings.value_type') + ] + out = '## {}\n```{}```'.format( + I18N.t('stats.commands.list.stats_msg_out.sub_settings'), tabulate(settings_in_db, headers=headers_settings) ) hidden_roles_in_db = await db_helper.get_output( @@ -208,7 +221,10 @@ async def list_settings( hidden_roles_in_list.append(role[0]) log.debug(f'`hidden_roles_in_list` is {hidden_roles_in_list}') if len(hidden_roles_in_list) > 0: - headers_hidden_roles = ['Name', 'ID'] + headers_hidden_roles = [ + I18N.t('stats.commands.list.headers.hidden_roles.hidden_name'), + I18N.t('stats.commands.list.headers.hidden_roles.hidden_id') + ] populated_roles = [] for role in hidden_roles_in_list: populated_roles.append( @@ -219,7 +235,8 @@ async def list_settings( ), role ) ) - out += '\n## Hidden roles\n```{}```'.format( + out += '\n## {}\n```{}```'.format( + I18N.t('stats.commands.list.stats_msg_out.sub_hidden'), tabulate(populated_roles, headers=headers_hidden_roles) ) await interaction.followup.send(content=out, ephemeral=True) @@ -232,7 +249,8 @@ async def list_settings( name_of_setting=name_of_settings_autocomplete ) @stats_group.command( - name='setting', description='Change a setting for this cog' + name='setting', + description=locale_str(I18N.t('stats.commands.setting.command')) ) async def stats_setting( self, interaction: discord.Interaction, name_of_setting: str, @@ -258,12 +276,11 @@ async def stats_setting( if setting[2] == 'bool': try: value_in = eval(str(value_in).capitalize()) - except NameError as e: - log.error(f'Invalid input for `value_in`: {e}') - # TODO var msg - await interaction.followup.send( - 'Input `value_in` needs to be `True` or `False`' - ) + except NameError as _error: + log.error(f'Invalid input for `value_in`: {_error}') + await interaction.followup.send(I18N.t( + 'stats.setting_input_reply' + )) return log.debug(f'`value_in` is {value_in} ({type(value_in)})') log.debug(f'`setting[2]` is {setting[2]} ({type(setting[2])})') @@ -274,7 +291,8 @@ async def stats_setting( updates=[('value', value_in)] ) await interaction.followup.send( - content='Setting updated', ephemeral=True + content=I18N.t('stats.commands.setting.update_confirmed'), + ephemeral=True ) Stats.update_stats.restart() break @@ -285,7 +303,9 @@ async def stats_setting( commands.has_permissions(administrator=True) ) @stats_group.command( - name='hide_roles_add', description='Add roles to hide' + name='hide_roles_add', + description=locale_str( + I18N.t('stats.commands.hide_roles_add.command')), ) async def stats_add_hidden_roles( self, interaction: discord.Interaction, @@ -309,7 +329,7 @@ async def stats_add_hidden_roles( if str(role_in.id) in hidden_roles_in_list: # TODO var msg await interaction.followup.send( - 'Role is already hidden' + I18N.t('stats.commands.hide_roles_add.msg.already_hidden') ) return else: @@ -321,7 +341,9 @@ async def stats_add_hidden_roles( ) # TODO var msg await interaction.followup.send( - content='Role added as hidden', ephemeral=True + content=I18N.t( + 'stats.commands.hide_roles_add.msg.confirm_added'), + ephemeral=True ) Stats.update_stats.restart() return @@ -335,7 +357,8 @@ async def stats_add_hidden_roles( ) @stats_group.command( name='hide_roles_remove', - description='Remove roles from hiding in stats' + description=locale_str( + I18N.t('stats.commands.hide_roles_remove.command')) ) async def stats_remove_hidden_roles( self, interaction: discord.Interaction, @@ -350,14 +373,14 @@ async def stats_remove_hidden_roles( The role to remove ''' await interaction.response.defer(ephemeral=True) - # TODO Remove by ROW_ID from autocomplette await db_helper.del_row_id( template_info=envs.stats_db_hide_roles_schema, numbers=hidden_roles ) - # TODO var msg await interaction.followup.send( - content='Role removed from hidden', ephemeral=True + content=I18N.t( + 'stats.commands.hide_roles_remove.msg.confirm_removed'), + ephemeral=True ) Stats.update_stats.restart() return @@ -367,16 +390,18 @@ async def stats_remove_hidden_roles( commands.has_permissions(administrator=True) ) @stats_group.command( - name='reload', description='Reload the stats task' + name='restart', + description=locale_str(I18N.t('stats.commands.restart.command')) ) - async def stats_reload( + async def stats_restart( self, interaction: discord.Interaction ): - '''Reload the stats task''' + '''Restart the stats task''' await interaction.response.defer(ephemeral=True) Stats.update_stats.restart() await interaction.followup.send( - content='Stats reloaded', ephemeral=True + content=I18N.t('stats.commands.restart.msg.confirm_restarted'), + ephemeral=True ) return @@ -400,8 +425,9 @@ async def tabify( where=[('setting', 'hide_roles')] ) hide_roles_lower = [x[0].lower() for x in hide_roles] - # TODO var msg - log.debug(f'Using this for filter:\n{hide_roles_lower}') + log.debug( + f'Using this for filter:\n{hide_roles_lower}' + ) text_out = '' if isinstance(dict_in, dict): log.debug( @@ -444,12 +470,10 @@ async def tabify( if role != '@everyone': # Check for `sort_min_role_members` if stats_settings['sort_min_role_members']: - min_members = stats_settings[ - 'sort_min_role_members' - ] - if dict_in[role]['members'] >= int( - min_members - ): + min_members = \ + stats_settings['sort_min_role_members'] + if dict_in[role]['members'] >= \ + int(min_members): dict_out['name'].append( dict_in[role]['name']) dict_out['members'].append( @@ -546,17 +570,28 @@ async def tabify( stats_settings['show_role_stats'] )) if eval(stats_settings['show_role_stats']): - stats_msg += f'### Medlemmer\n```'\ - f'Antall medlemmer: {total_members}\n\n'\ + members_sub = I18N.t( + 'stats.tasks.update_stats.stats_msg.members_sub') + members_num = I18N.t( + 'stats.tasks.update_stats.stats_msg.members_num') + stats_msg += f'### {members_sub}\n```'\ + f'{members_num}: {total_members}\n\n'\ f'{roles_members}```\n' log.debug('`show_code_stats` is {}'.format( stats_settings['show_code_stats'] )) if eval(stats_settings['show_code_stats']): - stats_msg += f'### Kodebase\n```'\ - f'Antall filer med kode: {files_in_codebase}\n'\ - f'Antall linjer med kode: {lines_in_codebase}```\n' - stats_msg += f'```(Serverstats sist oppdatert: {dt_log})```\n' + code_sub = I18N.t('stats.tasks.update_stats.stats_msg.code_sub') + code_files = I18N.t( + 'stats.tasks.update_stats.stats_msg.code_files') + code_lines = I18N.t( + 'stats.tasks.update_stats.stats_msg.code_lines') + stats_msg += f'### {code_sub}\n```'\ + f'{code_files}: {files_in_codebase}\n'\ + f'{code_lines}: {lines_in_codebase}```\n' + code_last_updated = I18N.t( + 'stats.tasks.update_stats.stats_msg.code_last_updated') + stats_msg += f'```{code_last_updated} {dt_log}```\n' log.verbose( f'Trying to post stats to `{stats_channel}`:\n' f'{stats_msg[0:100]}...' diff --git a/sausage_bot/cogs/youtube.py b/sausage_bot/cogs/youtube.py index a0699ea..fc3c555 100755 --- a/sausage_bot/cogs/youtube.py +++ b/sausage_bot/cogs/youtube.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -from discord.ext import commands, tasks import discord +from discord.ext import commands, tasks +from discord.app_commands import locale_str, describe import typing import re from time import sleep @@ -9,6 +10,7 @@ from sausage_bot.util import config, envs, feeds_core, file_io from sausage_bot.util import db_helper, discord_commands +from sausage_bot.util.i18n import I18N from sausage_bot.util.log import log @@ -62,21 +64,29 @@ def __init__(self, bot): super().__init__() youtube_group = discord.app_commands.Group( - name="youtube", description='Administer YouTube feeds' + name="youtube", description=locale_str( + I18N.t('youtube.groups.youtube') + ) ) youtube_filter_group = discord.app_commands.Group( - name="filter", description='Filter YouTube feeds', + name="filter", description=locale_str( + I18N.t('youtube.groups.filter') + ), parent=youtube_group ) youtube_posting_group = discord.app_commands.Group( - name="posting", description='Posting from YouTube feeds', + name="posting", description=locale_str( + I18N.t('youtube.groups.posting') + ), parent=youtube_group ) @youtube_posting_group.command( - name='start', description='Start posting' + name='start', description=locale_str( + I18N.t('youtube.commands.start.cmd') + ) ) async def youtube_posting_start( self, interaction: discord.Interaction @@ -93,11 +103,13 @@ async def youtube_posting_start( updates=('status', 'started') ) await interaction.followup.send( - 'Youtube posting started' + I18N.t('youtube.commands.start.msg_confirm') ) @youtube_posting_group.command( - name='stop', description='Stop posting' + name='stop', description=locale_str( + I18N.t('youtube.commands.stop.cmd') + ) ) async def youtube_posting_stop( self, interaction: discord.Interaction @@ -114,7 +126,7 @@ async def youtube_posting_stop( updates=('status', 'stopped') ) await interaction.followup.send( - 'Youtube posting stopped' + I18N.t('youtube.commands.stop.msg_confirm') ) @commands.check_any( @@ -123,7 +135,14 @@ async def youtube_posting_stop( ) @discord.app_commands.autocomplete(feed_name=feed_name_autocomplete) @youtube_group.command( - name='add', description='Add a YouTube-feed' + name='add', description=locale_str( + I18N.t('youtube.commands.add.cmd') + ) + ) + @describe( + feed_name=I18N.t('youtube.commands.add.desc.feed_name'), + youtube_link=I18N.t('youtube.commands.add.desc.youtube_link'), + channel=I18N.t('youtube.commands.add.desc.channel') ) async def youtube_add( self, interaction: discord.Interaction, feed_name: str, @@ -131,15 +150,6 @@ async def youtube_add( ): ''' Add a Youtube feed - - Parameters - ------------ - feed_name: str - Name of feed to manage - youtube_link: str - The link for the YouTube-channel - channel: str - The Discord channel to post from the feed ''' await interaction.response.defer() AUTHOR = interaction.user.name @@ -147,7 +157,10 @@ async def youtube_add( youtube_info = await Youtube.get_youtube_info(youtube_link) if youtube_info is None: await interaction.followup.send( - envs.YOUTUBE_EMPTY_LINK.format(youtube_link) + I18N.t( + 'youtube.commands.add.msg_empty_link', + link=youtube_link + ), ) return await feeds_core.add_to_feed_db( @@ -155,12 +168,17 @@ async def youtube_add( AUTHOR, youtube_info['channel_id'] ) await discord_commands.log_to_bot_channel( - envs.YOUTUBE_ADDED_BOT.format( - AUTHOR, feed_name, youtube_link, channel.name + I18N.t( + 'youtube.commands.add.log_feed_confirm', + user=AUTHOR, feed_name=feed_name, + yt_link=youtube_link, channel=channel.name ) ) await interaction.followup.send( - envs.YOUTUBE_ADDED.format(feed_name, channel.name) + I18N.t( + 'youtube.commands.add.msg_added', + feed_name=feed_name, channel_name=channel.name + ) ) return @@ -170,18 +188,18 @@ async def youtube_add( ) @discord.app_commands.autocomplete(feed_name=feed_name_autocomplete) @youtube_group.command( - name='remove', description='Remove a YouTube-feed' + name='remove', description=locale_str( + I18N.t('youtube.commands.remove.cmd') + ) + ) + @describe( + feed_name=I18N.t('youtube.commands.remove.desc.feed_name') ) async def youtube_remove( self, interaction: discord.Interaction, feed_name: str ): ''' Remove a Youtube feed - - Parameters - ------------ - feed_name: str - Name of feed to manage ''' await interaction.response.defer() AUTHOR = interaction.user.name @@ -192,10 +210,12 @@ async def youtube_remove( single=True ) if _uuid is None: - _error_msg = f'The feed `{feed_name}` does not exist' - log.debug(_error_msg) + log.debug(f'The feed `{feed_name}` does not exist') await interaction.followup.send( - _error_msg + I18N.t( + 'youtube.commands.remove.msg_remove_non_existing_feed', + feed_name=feed_name + ) ) return removal = await feeds_core.remove_feed_from_db( @@ -203,19 +223,31 @@ async def youtube_remove( ) if removal: await discord_commands.log_to_bot_channel( - envs.YOUTUBE_REMOVED_BOT.format(feed_name, AUTHOR) + I18N.t( + 'youtube.commands.remove.log_feed_removed', + feed_name=feed_name, user_name=AUTHOR + ) ) await interaction.followup.send( - envs.YOUTUBE_REMOVED.format(feed_name) + I18N.t( + 'youtube.commands.remove.msg_feed_removed', + feed_name=feed_name + ) ) elif removal is False: # Couldn't remove the feed await interaction.followup.send( - envs.YOUTUBE_COULD_NOT_REMOVE.format(feed_name) + I18N.t( + 'youtube.commands.remove.msg_feed_remove_failed', + feed_name=feed_name + ) ) # Also log and send error to either a bot-channel or admin await discord_commands.log_to_bot_channel( - envs.YOUTUBE_TRIED_REMOVED_BOT.format(AUTHOR, feed_name) + I18N.t( + 'youtube.commands.remove.log_feed_remove_failed', + user_name=AUTHOR, feed_name=feed_name + ) ) return @@ -225,26 +257,24 @@ async def youtube_remove( ) @discord.app_commands.autocomplete(feed_name=feed_name_autocomplete) @youtube_filter_group.command( - name='add', description='Add filters on an Youtube feed' + name='add', description=locale_str( + I18N.t('youtube.commands.filter_add.cmd') + ) + ) + @describe( + feed_name=I18N.t('youtube.commands.filter_add.desc.feed_name'), + allow_deny=I18N.t('youtube.commands.filter_add.desc.allow_deny'), + filters_in=I18N.t('youtube.commands.filter_add.desc.filters_in') ) async def youtube_filter_add( self, interaction: discord.Interaction, feed_name: str, - allow_deny: typing.Literal['Allow', 'Deny'], filters_in: str + allow_deny: typing.Literal[ + I18N.t('youtube.commands.filter_add.literal.allow'), + I18N.t('youtube.commands.filter_add.literal.deny') + ], filters_in: str ): ''' Add filter for feed (deny/allow) - - Parameters - ------------ - feed_name: str - Name of feed - allow_deny: str - Specify if the filter should `allow` or `deny`. Separate multiples - with any of the following characers: " .,;-_\\/" - filters_in: str - What to filter a post by. Separate multiple with any of the - following characers: " .,;-_\\/" - ''' await interaction.response.defer(ephemeral=True) # Make sure that the filter input can be split @@ -257,21 +287,23 @@ async def youtube_filter_add( where=(('feed_name', feed_name)), single=True ) - temp_inserts = [] - for _index, filter in enumerate(_filters_in): - temp_inserts.append((_uuid, allow_deny, filter)) + _inserts = [(_uuid, allow_deny, filter)] adding_filter = await db_helper.insert_many_all( template_info=envs.youtube_db_filter_schema, - inserts=temp_inserts + inserts=_inserts ) if adding_filter: - msg_out = f'Added filters as {allow_deny}:' - for filter in _filters_in: - msg_out += f'\n- {filter}' - await interaction.followup.send(msg_out, ephemeral=True) + await interaction.followup.send( + I18N.t( + 'youtube.commands.filter_add.msg_filter_added', + allow_deny=allow_deny, filter_in=filter + ), + ephemeral=True) else: await interaction.followup.send( - 'Error when adding filter, check logs', + I18N.t( + 'youtube.commands.filter_add.msg_filter_failed' + ), ephemeral=True ) return @@ -283,20 +315,19 @@ async def youtube_filter_add( @discord.app_commands.autocomplete(feed_name=feed_name_autocomplete) @discord.app_commands.autocomplete(filter_in=youtube_filter_autocomplete) @youtube_filter_group.command( - name='remove', description='Remove filters on an Youtube feed' + name='remove', description=locale_str( + I18N.t('youtube.commands.filter_remove.cmd') + ) + ) + @describe( + feed_name=I18N.t('youtube.commands.filter_remove.desc.feed_name'), + filter_in=I18N.t('youtube.commands.filter_remove.desc.filter_in') ) async def youtube_filter_remove( self, interaction: discord.Interaction, feed_name: str, filter_in: str ): ''' Remove filter for feed - - Parameters - ------------ - feed_name: str - Name of feed - filter_in: str - What filter to look for ''' await interaction.response.defer(ephemeral=True) _uuid = await db_helper.get_output( @@ -314,33 +345,48 @@ async def youtube_filter_remove( ) if removing_filter: await interaction.followup.send( - f'Removed filter `{filter_in}`', + I18N.t( + 'youtube.commands.filter_remove.msg_confirm', + filter_in=filter_in + ), ephemeral=True ) else: await interaction.followup.send( - f'Error when removing filter `{filter_in}`, check logs', + I18N.t( + 'youtube.commands.filter_remove.msg_error', + filter_in=filter_in + ), ephemeral=True ) return @youtube_group.command( - name='list', description='List all active YouTube feeds' + name='list', description=locale_str( + I18N.t('youtube.commands.list.cmd') + ) + ) + @describe( + list_type=I18N.t('youtube.commands.list.desc.list_type') ) async def youtube_list( self, interaction: discord.Interaction, - list_type: typing.Literal['Normal', 'Added', 'Filter'] + list_type: typing.Literal[ + I18N.t('youtube.commands.list.literal_type.normal'), + I18N.t('youtube.commands.list.literal_type.added'), + I18N.t('youtube.commands.list.literal_type.filter') + ] ): ''' List all active Youtube feeds ''' await interaction.response.defer() - if list_type.lower() == 'added': + if list_type == I18N.t('youtube.commands.list.literal_type.added'): formatted_list = await feeds_core.get_feed_list( db_in=envs.youtube_db_schema, list_type=list_type.lower() ) - elif list_type.lower() == 'filter': + elif list_type == I18N.t('youtube.commands.list.literal_type.filter'): formatted_list = await feeds_core.get_feed_list( db_in=envs.youtube_db_schema, db_filter_in=envs.youtube_db_filter_schema, @@ -362,7 +408,7 @@ async def youtube_list( sleep(1) else: await interaction.followup.send( - 'No feeds added' + I18N.t('youtube.commands.list.msg_error') ) return @@ -407,7 +453,7 @@ async def post_videos(): ] ) if len(feeds) == 0: - log.log(envs.YOUTUBE_NO_FEEDS_FOUND) + log.log('Couldn\'t find any Youtube feeds') return log.verbose('Got these feeds:') for feed in feeds: @@ -426,9 +472,9 @@ async def post_videos(): ) log.debug(f'Got this for `FEED_POSTS`: {FEED_POSTS}') if FEED_POSTS is None: - log.log(envs.YOUTUBE_FEED_POSTS_IS_NONE.format(feed)) + log.log(f'{feed}: this feed returned NoneType.') await discord_commands.log_to_bot_channel( - envs.YOUTUBE_FEED_POSTS_IS_NONE.format(FEED_NAME) + I18N.t('youtube.tasks.log_error', feed_name=FEED_NAME) ) else: await feeds_core.process_links_for_posting_or_editing( diff --git a/sausage_bot/locale/autoevent.en.yml b/sausage_bot/locale/autoevent.en.yml new file mode 100755 index 0000000..e706a06 --- /dev/null +++ b/sausage_bot/locale/autoevent.en.yml @@ -0,0 +1,46 @@ +en: + commands: + autoevent: + cmd: Administer match events on the discord server based on a url from a supported website + add: + cmd: Add an event + desc: + url: URL to match page from nifs, vglive or tv2.no/livesport + channel: Channel to post to + text: Additional text to the event's description + event_image: Image for event (800 x 320) + description: + tournament: Tournament + when: When + where: Where + reminder: Remember that the event opens 30 minutes before matchstart + log_confirm: Autogenerated event added + msg_confirm: 'Added event for %{home} - %{away} (ID: %{id})' + msg_failed: 'Got an error when posting event: %{error_in}' + remove: + cmd: Remove a scheduled event + desc: + event: The event to remove + remove_all: Do you want to remove all events? + msg_all_confirm: All events removed + msg_one_confirm: Event removed + msg_no_event: No event given + list: + cmd: List all the planned events + msg_no_events: No events are currently planned + sync: + cmd: Create a timer for an + desc: + sync_time: The time you want to sync to + countdown: How many seconds should it count down before hitting the `sync_time` + msg_confirm: 'Sync til %{time1}:%{time2} %{rel_start}' + not_correct_format: '`sync_time` is not in the correct format' + announce: + cmd: Announce an event + desc: + event: The event to announce + channel: The channel to announce in + annouce_text: Reminding everybody that the event starts %{rel_start}, 30 minutes before matchstart + msg_confirm: 'Message sent to `#%{channel}`' + msg_forbidden: 'I am not able to send messages in `#%{channel}`' + msg_error: 'An error occurred when announcing event: %{error}' diff --git a/sausage_bot/locale/autoevent.no.yml b/sausage_bot/locale/autoevent.no.yml new file mode 100755 index 0000000..1c650d1 --- /dev/null +++ b/sausage_bot/locale/autoevent.no.yml @@ -0,0 +1,46 @@ +no: + commands: + autoevent: + cmd: Administrer kamp-hendelser på serveren basert på en link fra en støttet nettside + add: + cmd: Legg til en hendelse + desc: + url: URL til kampsiden fra nifs, vglive eller tv2.no/livesport + channel: Kanal å poste til + text: Ekstra tekst til hendelsens beskrivelse + event_image: Bilde for hendelse (800 x 320) + description: + tournament: Turnering + when: Når + where: Hvor + reminder: Husk at eventet er åpent en halvtime før kampstart + log_confirm: Opprettet autogenerert hendelse + msg_confirm: 'Opprettet hendelse for %{home} - %{away} (ID: %{id})' + msg_failed: 'Fikk en feil ved publisering av hendelse: %{error_in}' + remove: + cmd: Fjern en planlagt hendelse + desc: + event: Hendelsen som skal fjernes + remove_all: Vil du slette alle hendelser? + msg_all_confirm: Alle hendelser fjernet + msg_one_confirm: Hendelse fjernet + msg_no_event: Ingen hendelse valgt + list: + cmd: Vis alle planlagte hendelser + msg_no_events: Ingen hendelser er planlagt + sync: + cmd: Opprett en tidtaker for en hendelse + desc: + sync_time: Tiden du vil synkronisere til + countdown: Hvor mange sekunder skal brukes til nedtelling til `sync_time`? + msg_confirm: 'Sync til %{time1}:%{time2} %{rel_start}' + not_correct_format: '`sync_time` is not in the correct format' + announce: + cmd: Annonser en hendelse + desc: + event: Hendelsen som skal annonses + channel: Kanal som hendelsen skal annonses i + annouce_text: Minner om eventen som begynner %{rel_start}, 30 min før kampstart + msg_confirm: 'Melding sendt til `#%{channel}`' + msg_forbidden: 'Jeg kan ikke sende meldinger i `#%{channel}`' + msg_error: 'En feil oppstod ved annonsering av hendelset: %{error}' diff --git a/sausage_bot/locale/common.en.yml b/sausage_bot/locale/common.en.yml new file mode 100755 index 0000000..5c3515c --- /dev/null +++ b/sausage_bot/locale/common.en.yml @@ -0,0 +1,21 @@ +en: + too_few_arguments: Too few arguments given to command + literal_yes_no: + yes: Yes + no: No + literal_allow_deny: + allow: Allow + deny: Deny + name: Name + channel: Channel + message_id: Message ID + text: Text + order: Order + id: ID + num_reactions: No. reactions + msg_name: Message name + msg_text: Message text + order: Order + roles: Roles + emojis: Emojis + header: Header \ No newline at end of file diff --git a/sausage_bot/locale/common.no.yml b/sausage_bot/locale/common.no.yml new file mode 100755 index 0000000..448e44a --- /dev/null +++ b/sausage_bot/locale/common.no.yml @@ -0,0 +1,21 @@ +no: + too_few_arguments: Du har gitt for få argumenter til kommandoen + literal_yes_no: + yes: Ja + no: Nei + literal_allow_deny: + allow: Tillat + deny: Nekt + name: Navn + channel: Kanal + message_id: Meldings-ID + text: Tekst + order: Rekkefølge + id: ID + num_reactions: Ant. reaksjoner + msg_name: Meldingsnavn + msg_text: Meldingstekst + order: Rekkefølge + roles: Roller + emojis: Emojier + header: Overskrift \ No newline at end of file diff --git a/sausage_bot/locale/dilemmas.en.yml b/sausage_bot/locale/dilemmas.en.yml new file mode 100755 index 0000000..bad3d48 --- /dev/null +++ b/sausage_bot/locale/dilemmas.en.yml @@ -0,0 +1,17 @@ +en: + commands: + post: + cmd: Post a random dilemma + no_dilemmas_in_db: No dilemmas in database + add: + cmd: Add a dilemma + desc: + dilemmas_in: Dilemma to add + msg_confirm: 'Added the following dilemma: %{dilemmas_in}' + count: + cmd: Count the numbers of dilemmas + msg_confirm: + zero: There are no dilemmas + one: There is one dilemma + few: There are %{count} dilemmas + many: There are %{count} dilemmas diff --git a/sausage_bot/locale/dilemmas.no.yml b/sausage_bot/locale/dilemmas.no.yml new file mode 100755 index 0000000..f3fe591 --- /dev/null +++ b/sausage_bot/locale/dilemmas.no.yml @@ -0,0 +1,17 @@ +no: + commands: + post: + cmd: Post et tilfeldig dilemma + no_dilemmas_in_db: Fant ingen dilemmas i databasen + add: + cmd: Legg til en dilemma + desc: + dilemmas_in: Dilemma som skal legges til + msg_comfirm: 'La til følgende dilemma: %{dilemmas_in}' + count: + cmd: Tell antall dilemmaer + msg_confirm: + zero: There are no dilemmas + one: There is one dilemma + few: There are %{count} dilemmas + many: There are %{count} dilemmas diff --git a/sausage_bot/locale/discord_commands.en.yml b/sausage_bot/locale/discord_commands.en.yml new file mode 100755 index 0000000..eb5ddfb --- /dev/null +++ b/sausage_bot/locale/discord_commands.en.yml @@ -0,0 +1,7 @@ +en: + get_sorted_scheduled_events: + headers: + match: Match + date: Date + interest: No. interested + id: ID \ No newline at end of file diff --git a/sausage_bot/locale/discord_commands.no.yml b/sausage_bot/locale/discord_commands.no.yml new file mode 100755 index 0000000..f120ab1 --- /dev/null +++ b/sausage_bot/locale/discord_commands.no.yml @@ -0,0 +1,7 @@ +no: + get_sorted_scheduled_events: + headers: + match: Kamp + date: Dato + interest: Ant. interesserte + id: ID \ No newline at end of file diff --git a/sausage_bot/locale/log_maintenance.en.yml b/sausage_bot/locale/log_maintenance.en.yml new file mode 100755 index 0000000..e114a23 --- /dev/null +++ b/sausage_bot/locale/log_maintenance.en.yml @@ -0,0 +1,34 @@ +en: + commands: + log: + cmd: Administer log + maintenance: + cmd: Administer log task + settings: + cmd: Administer log settings + start: + cmd: Start log maintenance + msg_confirm: Log maintenance started + stop: + cmd: Stop log maintenance + msg_confirm: Log maintenance stopped + list: + cmd: List the available settings for this cog + headers: + setting: Setting + value: Value + value_type: Value type + setting: + cmd: Change a setting for this cog + desc: + name_of_setting: Name of the setting to change + value_in: Value to change the setting to + value_in_input_invalid: 'Invalid input for `value_in`: %{error}' + msg_confirm: Setting updated + tasks: + log_maintenance: + msg: + size_and_none: 'Max folder size is not set, or set to 0. The log folder size as of now is %{folder_size}.' + days_and_none: 'Max number of log days is not set, or set to 0. The log folder as of now has %{num_files} files.' + disable_posting: 'To disable these messages, run `/log maintenance stop`' + maintenance_done: 'Log maintenance done. Deleted the following files:' \ No newline at end of file diff --git a/sausage_bot/locale/log_maintenance.no.yml b/sausage_bot/locale/log_maintenance.no.yml new file mode 100755 index 0000000..38692f9 --- /dev/null +++ b/sausage_bot/locale/log_maintenance.no.yml @@ -0,0 +1,34 @@ +no: + commands: + log: + cmd: Administrer logging + maintenance: + cmd: Administrer logging-oppgave + settings: + cmd: Administrer innstillinger for logging + start: + cmd: Start vedlikehold av logg + msg_confirm: Startet vedlikehold av logg + stop: + cmd: Stopp vedlikehold av logg + msg_confirm: Stoppet vedlikehold av logg + list: + cmd: Hent tilgjengelige innstillinger for denne cogen + headers: + setting: Innstilling + value: Verdi + value_type: Verdi type + setting: + cmd: Endre en innstilling for denne cogen + desc: + name_of_setting: Navn på innstilling som skal endres + value_in: Verdi som skal settes + value_in_input_invalid: 'Ugyldig inndata for `value_in`: %{error}' + msg_confirm: Innstilling oppdatert + tasks: + log_maintenance: + msg: + size_and_none: 'Maks mappestørrelse er ikke definert, eller er 0. Logg-mappens størrelse er %{folder_size}' + days_and_none: 'Max number of log days is not set, or set to 0. Logg-mappen har %{num_files} filer' + disable_posting: 'Kjør kommandoen `/log maintenance stop` for å stoppe disse meldingene' + maintenance_done: 'Logg-vedlikehold er ferdig. Slettet følgende filer:' \ No newline at end of file diff --git a/sausage_bot/locale/main.en.yml b/sausage_bot/locale/main.en.yml new file mode 100755 index 0000000..0ab42f2 --- /dev/null +++ b/sausage_bot/locale/main.en.yml @@ -0,0 +1,37 @@ +en: + owner_only: Owner only + msg: + failed_bot_start: Could not start bot (%{error}) + bot_connected: "%{bot} has connected to %{server}" + bot_watching: some random youtube videos + create_log_channel_logging: "Incoming log messages from %{botname}" + commands: + language: + confirm_language_set: Language set to `%{language}` + test: + confirm: English test message + delete: + command: "Delete `amount` number of messages in the chat" + less_than_0: The number must be bigger than 0 + log_confirm: Mass delete via bot + msg_confirm: Deleted %{amount} messages + say: + command: Post a message to a channel. Enter `message_id` if replying to a specific post + say_again: + command: 'Change a previous message sent with `say`' + tasks: + command: List tasks and their status + ping: + command: Check latency + kick: + command: Kick a user (with reason) + msg_confirm: '%{member} has been kicked' + msg_failed: 'Failed to kick: %{error}' + ban: + command: Ban a user (with reason) + msg_confirm: '%{member} has been banned' + msg_failed: 'Failed to ban: %{error}' + + + + diff --git a/sausage_bot/locale/main.no.yml b/sausage_bot/locale/main.no.yml new file mode 100755 index 0000000..c63a758 --- /dev/null +++ b/sausage_bot/locale/main.no.yml @@ -0,0 +1,39 @@ +no: + owner_only: Kun for eier + msg: + failed_bot_start: Kunne ikke starte boten (%{error}) + bot_connected: "%{bot} har koblet til %{server}" + bot_watching: tilfeldige youtube videoer + create_log_channel_logging: "Innkommende logg-meldinger fra %{botname}" + commands: + language: + confirm_language_set: Språk satt til `%{language}` + test: + confirm: Norsk testmelding + delete: + command: 'Slett `amount` antall meldinger i chaten' + less_than_0: Tallet må være mer enn 0 + log_confirm: Massesletting via bot + msg_confirm: Slettet %{amount} meldinger + say: + command: Sender melding til en kanal. Fyll inn `message_id` hvis det skal svares på en melding + msg_confirm: 'Melding sendt til `#%{channel}`' + msg_forbidden: 'Jeg kan ikke sende meldinger i `#%{channel}`' + msg_error: 'En feil oppstod ved sending av melding: %{error}' + say_again: + command: Endre en tidligere melding sendt med /say + tasks: + command: List opp oppgaver og deres status + ping: + command: Sjekk latency + kick: + command: Spark ut en bruker (med grunn) + msg_confirm: '%{member} har blitt parket ut' + msg_failed: 'Klarte ikke å sparke ut: %{error}' + ban: + command: Utesteng en bruker (med grunn) + msg_confirm: '%{member} har blitt utestengt' + msg_failed: 'Klarte ikke å utestenge: %{error}' + + + diff --git a/sausage_bot/locale/poll.en.yml b/sausage_bot/locale/poll.en.yml new file mode 100755 index 0000000..bdff8d6 --- /dev/null +++ b/sausage_bot/locale/poll.en.yml @@ -0,0 +1,22 @@ +en: + commands: + poll: + cmd: Make a poll for voting on something + desc: + channel: Channel to post poll in + post_time: What time to post the poll. Accepts time in 0000 + lock_time: Lock poll after x m(inutes) or h(ours) + poll_text: Input for the poll + alternatives: Alternatives for the poll, separated by semicolon + msg: + post_in_past: Posting time is in the past + post_gives_error: '`post_time` "%{post_time}" gives error' + no_time_given: No lock_time is given + lock_gives_error: '`lock_time` "%{lock_time}" gives error' + posting_now: Poll will be posted soon + posting_fixed: 'Poll will be posted in ' + embed_title: Poll + timed_out: Timed out + post_confirm: 'Poll was posted %{post_text}' + lock_confirm_future: 'Poll is closed in ' + lock_confirm: 'Poll was closed after %{dt_lock_text}' diff --git a/sausage_bot/locale/poll.no.yml b/sausage_bot/locale/poll.no.yml new file mode 100755 index 0000000..8790e7c --- /dev/null +++ b/sausage_bot/locale/poll.no.yml @@ -0,0 +1,22 @@ +no: + commands: + poll: + cmd: Lag en avstemming + desc: + channel: Kanal som avstemmingen skal postes i + post_time: Tid for å poste avstemmingen. Godtar tid i formatet 0000 + lock_time: Lås avstemmingen etter x m (minutter) eller h (timer) + poll_text: Inputt for avstemmingen + alternatives: Alternativer for avstemmingen, separert med semikolon + msg: + post_in_past: Publiseringstiden er i fortiden + post_gives_error: '`post_time` "%{post_time}" gir feilmelding' + no_time_given: Ingen låsetid er gitt + lock_gives_error: '`lock_time` "%{lock_time}" gir feilmelding' + posting_now: Avstemningen postes straks + posting_fixed: 'Avstemningen postes om ' + embed_title: Avstemning + timed_out: Timed out + post_confirm: 'Avstemningen ble postet %{post_text}' + lock_confirm_future: 'Avstemningen blir stengt om ' + lock_confirm: 'Avstemning ble stengt etter %{dt_lock_text}' diff --git a/sausage_bot/locale/quote.en.yml b/sausage_bot/locale/quote.en.yml new file mode 100755 index 0000000..ba1cf16 --- /dev/null +++ b/sausage_bot/locale/quote.en.yml @@ -0,0 +1,40 @@ +en: + commands: + quote: + cmd: Quotes + post: + cmd: Post a quote + desc: + number: Choose a number if you want a specific quote + quote_not_exist: 'Quote # %{number} does not exist' + add: + cmd: Add a quote + modal_title: Add quote + msg_confirm: 'Added this quote:\n> %{quote_num}\n> %{quote_text}\n> %{quote_date}' + edit: + cmd: Edit an existing quote + desc: + quote_in: Quote to edit + modal_title: Edit quote + no_change: No changes discovered + delete: + cmd: Delete a quote + desc: + quote_in: Quote to delete + confirm_delete: 'Are you sure you want to delete the following quote?\n```# %{quote_num}\n%{quote_text}\n(%{quote_date})```\n' + msg_confirm: 'Deleted quote # %{quote_num}' + msg_fail: Will not delete quote, check logs + count: + cmd: Count the number of quotes + msg_confirm: 'Jeg har %{num_quotes} sitater på lager' + modals: + quote_num: Quote number + quote_text: Quote text + quote_date: Quote date + date_placeholder: Date and time (DD.MM.YYYY HH:MM) + add: + msg_confirm: Quote added + msg_error: 'Error: %{error}' + edit: + msg_confirm: Quote edited + msg_error: 'Error: %{error}' \ No newline at end of file diff --git a/sausage_bot/locale/quote.no.yml b/sausage_bot/locale/quote.no.yml new file mode 100755 index 0000000..2884524 --- /dev/null +++ b/sausage_bot/locale/quote.no.yml @@ -0,0 +1,38 @@ +no: + commands: + quote: + cmd: Sitater + post: + cmd: Post et sitat + desc: + number: Velg et nummer hvis du vil ha et spesifikt sitat + quote_not_exist: 'Sitat nummer %{number} finnes ikke' + add: + cmd: Legg til sitat + modal_title: Legg til sitat + msg_confirm: 'Added this quote:\n> %{quote_num}\n> %{quote_text}\n> %{quote_date}' + edit: + cmd: Rediger eksisterende sitat + desc: + quote_in: Sitat som skal endres + modal_title: Rediger sitat + no_change: Ingen endringer funnet + delete: + cmd: Slett sitat + desc: + quote_in: Sitat som skal slettes + confirm_delete: 'Er du sikker på at du vil slette følgende sitat?\n```# {quote_num}\n{quote_text}\n({quote_date})```\n' + msg_confirm: 'Slettet sitat nummer %{quote_num}' + msg_fail: Sitatet slettes ikke + count: + cmd: Tell antall sitater + modals: + quote_num: Sitat nummer + quote_text: Sitat tekst + quote_date: Sitat dato + date_placeholder: Dato og tid (DD.MM.YYYY HH:MM) + error: 'Feil: %{error}' + add: + msg_confirm: Sitat lagt til + edit: + msg_confirm: Sitat redigert diff --git a/sausage_bot/locale/roles.en.yml b/sausage_bot/locale/roles.en.yml new file mode 100755 index 0000000..dd9a36a --- /dev/null +++ b/sausage_bot/locale/roles.en.yml @@ -0,0 +1,174 @@ +en: + views: + perms: + drop_general: Select general permissions + drop_text: Select text permissions + drop_voice: Select voice permissions + emoji_headers: + emoji: Emoji + name: Name + id: ID + animated: Animated? + managed: Auto-managed? + roles_headers: + emoji: Emoji + name: Name + id: ID + members: Members + managed: Auto-managed? + tabulate_emojis_and_roles: + emoji_name: Emoji + emoji_id: Emoji ID + role_name: Role + role_id: Role ID + splits: + roles: 'Roles will be split on one or more of the following characers: " .,;-_\\/"' + emojis: 'Emojis will be split on one or more of the following characers: " .,;-_\\/"' + commands: + roles: + cmd: Control roles on the server + reaction: + cmd: Control reaction messages on the server + reaction_add: + cmd: Add reaction message to the server + reaction_remove: + cmd: Remove reaction message from the server + settings_group: + cmd: Control settings on the server + info: + cmd: Get info about a specific role + desc: + public: Make the reply public + role_in: Role to get info about + list: + cms: + desc: + public: Make the reply public + type: Type of list + sort: Sorting method + literal: + type: + roles: Roles + emojis: Emojis + sort: + name: By name + id: By ID + add: + cmd: Add a role + desc: + role_name: Name of the role to add + hoist: Set if the role should be distinguishable or not + mentionable: Set if the role should be mentionable or not + color: 'Color for the role. Accepts 0x, #, 0x#, rgb(, , )' + display_icon: Set a display icon for the role. Only possible if the guild has enough boosts + set_perms: Set permissions + msg_confirm: Role is created + msg_error: 'Error when creating role: %{_error}' + remove: + cmd: Remove a role from the server + desc: + role_name: Name of the role to remove + msg_confirm: 'Role `%{rolename}` has been removed' + edit: + cmd: Edit a role on the server + desc: + role_name: Name of the role to edit + new_name: New name for the role + color: 'Color for the role. Accepts 0x, #, 0x#, rgb(, , )' + hoist: Indicates if the role will be displayed separately from other members. + permissions: Indicate if the permissions also should be edited + changelist_name: Name + changelist_color: Color + changelist_hoist: Hoist + changelist_perms_added: Permissions added + changelist_perms_removed: Permissions removed + changes_out: 'Did following changes on role `%{role_name}`' + change_perms: Change permissions + msg_confirm: 'Role `%{role_name}` has been edited' + msg_error: 'Error when editing role: %{_error}' + react_list: + cmd: List reaction messages or info about a reaction message + desc: + reaction_msg: Reaction message to list + msg_error: 'Did not find %{reaction_msg}' + role_error: Role not found + emoji_error: Emoji not found + msg_no_msgs: No messages in database + add_reaction_msg: + cmd: Add reaction message + desc: + msg_name: Name of the message for the reaction roles + msg_text: The text for the message + channel: Channel to post reaction message to + order: Set order for the message in the channel + header: Header for the message + msg_order_exist: That order number already exist for %{channel}, try %{num} + msg_reaction_already_exist: Reaction message %{msg_name} already exists + msg_roles_emojis_error: Could not find any roles or emojis + msg_confirm: Message added + add_reaction_role: + cmd: Add roles to a reaction message + desc: + msg_name: Name of the message for the reaction roles + msg_confirm: Roles added + sync: + cmd: Synchronize reactions messages with database + desc: + reaction_msg: The message to sync + confirm_sync: Reaction message synced + sort: + cmd: Sort reaction messages alphabetically + desc: + reaction_msg: The message ID from database + msg_error: 'Did not find %{reaction_msg}' + msg_confirm: Roles sorted + remove_msg: + cmd: Remove a reaction message + desc: + reaction_msg: The message ID from Discord or name in the database + msg_confirm: Reaction message removed + msg_error: 'Could not find reaction message' + remove_role: + cmd: Remove a reaction from reaction message + desc: + reaction_msg: The message ID from database + role_name: Name of a role that is connected to a reaction in the message + msg_confirm: 'Role `%{rolename}` has been removed' + reorder: + cmd: Check and fix order of reaction messages in a channel + desc: + channel: What channel to check + msg_confirm: Reaction messages reordered + msg_already_sorted: Messages are already in correct order + settings_add: + cmd: Add a setting for roles on the server + desc: + setting: The setting to add + role_in: The role to add to the setting + literal: + setting: + unique: Unique role + not_include_in_total: Not include in total + role_already_set: Role is already set + msg_confirm: Added setting + settings_remove: + cmd: Remove a setting for roles on the server + desc: + setting: The setting to remove + msg_confirm: 'Setting `%{setting}` has been removed' + settings_list: + cmd: List settings for roles on the server + headers: + setting: Setting + role: Role + value: Value + settings_edit: + cmd: Edit a setting for roles on the server + desc: + setting: The setting to edit + role_in: The role to add to the setting + confirm_msg: 'Edited setting (%{setting} = %{role_in})' + on_raw_reaction_add: + channel_log_confirm: Added in accordance with reaction messages + on_raw_reaction_remove: + channel_log_confirm: Removed in accordance with reaction message diff --git a/sausage_bot/locale/roles.no.yml b/sausage_bot/locale/roles.no.yml new file mode 100755 index 0000000..4a5e407 --- /dev/null +++ b/sausage_bot/locale/roles.no.yml @@ -0,0 +1,174 @@ +no: + views: + perms: + drop_general: Velg generelle tillatelser + drop_text: Velg tekstillatelser + drop_voice: Velg stemmetillatelser + emoji_headers: + emoji: Emoji + name: Navn + id: ID + animated: Animert? + managed: Auto-håndtert? + roles_headers: + emoji: Emoji + name: Navn + id: ID + members: Medlemmer + managed: Auto-håndtert? + tabulate_emojis_and_roles: + emoji_name: Emoji + emoji_id: Emoji ID + role_name: Rolle + role_id: Rolle ID + splits: + roles: 'Roller blir splittet på en eller fler av følgende tegn: " .,;-_\\/"' + emojis: 'Emojier blir splittet på en eller fler av følgende tegn: " .,;-_\\/"' + commands: + roles: + cmd: Kontroller roller på serveren + reaction: + cmd: Kontroller reaksjonsmeldinger på serveren + reaction_add: + cmd: Legg til reaksjonsmelding på serveren + reaction_remove: + cmd: Fjern reaksjonsmelding på serveren + settings: + cmd: Kontroller innstillinger på serveren + info: + cmd: Hent informasjon om en spesifikk rolle + desc: + public: Gjør svaret offentlig + role_in: Rolle å hente informasjon om + list: + cms: + desc: + public: Gjør svaret offentlig + type: Listetype + sort: Sorteringsmetode + literal: + type: + roles: Roller + emojis: Emojier + sort: + name: Etter navn + id: Etter ID + add: + cmd: Legg til en rolle + desc: + role_name: Navn på rollen som skal legges til + hoist: Angi om rollen skal skilles ut i brukerliste eller ikke + mentionable: Angi om rollen skal kunne nevnes for alle eller ikke + color: 'Farge for rollen. Aksepterer 0x, #, 0x#, rgb(, , )' + display_icon: Angi et ikon for rollen. Kun mulig hvis guilden har nok booster + set_perms: Angi tillatelser + msg_confirm: Rolle er opprettet + msg_error: 'Feil ved opprettelse av rolle: %{_error}' + remove: + cmd: Fjern en rolle fra serveren + desc: + role_name: Navn på rollen som skal fjernes + msg_confirm: 'Rollen `%{rolename}` har blitt fjernet' + edit: + cmd: Rediger en rolle på serveren + desc: + role_name: Navn på rollen som skal redigeres + new_name: Nytt navn for rollen + color: 'Farge for rollen. Aksepterer 0x, #, 0x#, rgb(, , )' + hoist: Angir om rollen skal skilles ut i brukerliste eller ikke + permissions: Angir om tillatelser skal redigeres også + changelist_name: Navn + changelist_color: Farge + changelist_hoist: Spesielt synlig + changelist_perms_added: Tillatelser lagt til + changelist_perms_removed: Tillatelser fjernet + changes_out: 'Utførte følgende endringer på rollen `%{role_name}`' + change_perms: Endre tillatelser + msg_confirm: 'Rollen `%{role_name}` har blitt redigert' + msg_error: 'Feil ved redigering av rolle: %{_error}' + react_list: + cmd: Vis reaksjonsmeldinger eller info om en reaksjonsmelding + desc: + reaction_msg: Reaksjonsmelding som skal vises + msg_error: 'Fant ikke %{reaction_msg}' + role_error: 'Rolle ikke funnet' + emoji_error: 'Emoji ikke funnet' + msg_no_msgs: Ingen meldinger i databasen + add_reaction_msg: + cmd: Legg til reaksjonsmelding + desc: + msg_name: Navn på reaksjonsmelding + msg_text: Reaksjonsmeldingens tekst + channel: Kanalen å poste reaksjonsmelding på + order: Gi reaksjonsmeldingen en rekkefølge + header: Tittel for melingen + msg_order_exist: 'En annen melding har allerede det nummeret i kanalen %{channel}, prøv istedet %{num}' + msg_reaction_already_exist: Reaksjonsmeldingen %{msg_name} finnes allerede + msg_roles_emojis_error: Klarte ikke å finne verken roller eller emojier + msg_confirm: Melding lagt til + add_reaction_role: + cmd: Legg til roller på en reaksjonsmelding + desc: + msg_name: Navn på melding for reaksjonsroller + msg_confirm: Roles added + sync: + cmd: Synkroniser reaksjonmeldinger med database + desc: + reaction_msg: Reaksjonsmeldingen som skal synkroniseres + confirm_sync: Reaksjonsmelding synkronisert + sort: + cmd: Sorter reaksjonsmeldinger alfabetisk + desc: + reaction_msg: Meldingen fra databasen + msg_error: 'Fant ikke %{reaction_msg}' + msg_confirm: Roller sortert + remove_msg: + cmd: Fjern en reaksjonsmelding + desc: + reaction_msg: Meldingen fra databasen + msg_confirm: Reaksjonsmelding fjernet + msg_error: Klarte ikke finne reaksjonsmeldingen + remove_role: + cmd: Fjern en reaksjon fra en reaksjonsmelding + desc: + reaction_msg: Meldingen fra databasen + role_name: Navn på en rolle som er knyttet mot en reaksjon + msg_confirm: 'Rollen `%{rolename}` er fjernet' + reorder: + cmd: Sjekk og fiks rekkefølgen på reaksjonsmeldinger i en kanal + desc: + channel: Kanal å sjekke + msg_confirm: Reaksjonsmeldinger sortert + msg_already_sorted: Reaksjonsmeldinger er allerede i riktig rekkefølge + settings_add: + cmd: Legg til en innstilling for roller + desc: + setting: Innstillingen som skal legges til + role_in: Rollen som skal legges til innstillingen + literal: + setting: + unique: Unik rolle + not_include_in_total: Ikke inkluder i totalen + role_already_set: Rollen er allerede satt + msg_confirm: Innstilling lagt til + settings_remove: + cmd: Fjern ern innstilling for roller + desc: + setting: Innstillingen som skal fjernes + msg_confirm: Innstilling `%{setting}` er fjernet + settings_list: + cmd: List opp innstillinger for roller + headers: + setting: Innstilling + role: Rolle + value: Verdi + settings_edit: + cmd: Rediger en innstilling for roller + desc: + setting: Innstillingen som skal redigeres + role_in: Rollen som skal legges til innstillingen + confirm_msg: 'Redigerte innstillingen (%{setting} = %{role_in})' + on_raw_reaction_add: + channel_log_confirm: Lagt til i henhold til reaksjonsmeldinger + on_raw_reaction_remove: + channel_log_confirm: Fjernet i henhold til reaksjonsmelding diff --git a/sausage_bot/locale/rss.en.yml b/sausage_bot/locale/rss.en.yml new file mode 100755 index 0000000..2a7e4fb --- /dev/null +++ b/sausage_bot/locale/rss.en.yml @@ -0,0 +1,66 @@ +en: + groups: + rss: Administer RSS-feeds + filter: Manage RSS filters + posting: Posting from RSS feeds + commands: + start: + cmd: Start posting + msg_confirm: RSS posting started + stop: + cmd: Stop posting + msg_confirm: RSS posting stopped + add: + cmd: Add a RSS feed + desc: + feed_name: The name of the feed to add + feed_link: Link to the RSS-/XML-feed + channel: The channel to post from the feed + msg_feed_failed: The url is not a RSS/XML feed + msg_feed_confirm: '%{feed_name} was added to #%{channel_name}' + log_feed_confirm: '%{user_name} added the feed %{feed_name} til #%{channel_name}' + remove: + cmd: Remove a RSS feed + desc: + feed_name: The name of the feed to remove + msg_feed_not_found: The feed was not found + msg_feed_removed: '%{feed_name} was removed' + log_feed_removed: 'Feeden %{feed_name} ble fjernet av %{user_name}' + msg_feed_remove_failed: 'Failed when trying to remove the feed %{feed_name}' + log_feed_remove_failed: '%{user_name} tried to remove the feed %{feed_name}, but it failed' + edit: + cmd: Edit a RSS feed + desc: + feed_name: The name of the feed to edit + new_feed_name: The new name of the feed + channel: The channel to edit the feed from + url: The new url of the feed + changes_out: + msg: 'Did following changes on feed %{feed_name}:' + feed_name: Feed name + channel: Channel + url: URL + filter_add: + cmd: Add a filter to a RSS feed + desc: + feed_name: The name of the feed to add + allow_deny: Allow or deny + filter: 'What to filter a post by. Separate multiple with any of the following characers: " .,;-_\\/"' + msg_confirm: 'Added filters as `%{allow_deny}`:' + msg_error: 'Error when adding filter, check logs' + filter_remove: + cmd: Remove a filter from a RSS feed + desc: + feed_name: The name of the feed to remove + filter: 'What filter to look for' + msg_confirm: 'Removed filter `%{filter}`' + msg_error: 'Error when removing filter `%{filter}`, check logs' + list: + cmd: List all active rss feeds + desc: + list_type: 'Specify type of list' + literal_type: + normal: Normal + added: Added + filter: Filter + msg_error: No feeds added \ No newline at end of file diff --git a/sausage_bot/locale/rss.no.yml b/sausage_bot/locale/rss.no.yml new file mode 100755 index 0000000..3988047 --- /dev/null +++ b/sausage_bot/locale/rss.no.yml @@ -0,0 +1,66 @@ +no: + groups: + rss: Administer RSS-feeds + filter: Manage RSS filters + posting: Posting from RSS feeds + commands: + start: + cmd: Start posting + msg_confirm: RSS posting started + stop: + cmd: Stop posting + msg_confirm: RSS posting stopped + add: + cmd: Add a RSS feed + desc: + feed_name: The name of the feed to add + feed_link: Link to the RSS-/XML-feed + channel: The channel to post from the feed + msg_feed_failed: Urlen er ikke en RSS/XML feed + msg_feed_confirm: '%{feed_name} ble lagt til i #%{channel_name}' + log_feed_confirm: '%{user_name} la til feeden %{feed_name} i #%{channel_name}' + remove: + cmd: Remove a RSS feed + desc: + feed_name: The name of the feed to remove + msg_feed_not_found: The feed was not found + msg_feed_removed: '%{feed_name} was removed from #%{channel_name}' + log_feed_removed: 'Feeden %{feed_name} ble fjernet av %{user_name}' + msg_feed_remove_failed: 'Klarte ikke å fjerne RSS-feeden %{feed_name}' + log_feed_remove_failed: '%{user_name} prøvde å fjerne feeden %{feed_name}, men det oppsto en feil' + edit: + cmd: Rediger en RSS-feed + desc: + feed_name: Navnet på feeden som skal redigeres + new_feed_name: Nytt navn på feeden som skal endres til + channel: Kanalen feeden skal endres til + url: Ny URL for feeden + changes_out: + msg: 'Gjorde følgende endringer på feeden %{feed_name}:' + feed_name: Feednavn + channel: Kanal + url: URL + filter_add: + cmd: Legg til et filter på en RSS-feed + desc: + feed_name: Navnet på feeden som skal legges til + allow_deny: Tillat eller nekt + filter: 'Hva feeden skal filtreres etter. Separer flere med noen av følgende tegn: " .,;-_\\/"' + msg_confirm: 'Lagt til filtre som `%{allow_deny}`:' + msg_error: 'Feil ved legging av filter, sjekk loggfilene' + filter_remove: + cmd: Fjern et filter fra en RSS-feed + desc: + feed_name: Navnet på feeden som skal fjernes + filter: 'Hvilket filter som skal søkes etter' + msg_confirm: 'Fjernet filter `%{filter}`' + msg_error: 'Feil ved fjerning av filter `%{filter}`, sjekk loggfilene' + list: + cmd: List alle aktive RSS-feeder + desc: + list_type: 'Spesifiser type liste' + literal_type: + normal: Normal + added: Lagt til + filter: Filter + msg_error: Ingen feeds lagt til \ No newline at end of file diff --git a/sausage_bot/locale/stats.en.yml b/sausage_bot/locale/stats.en.yml new file mode 100755 index 0000000..d6634fa --- /dev/null +++ b/sausage_bot/locale/stats.en.yml @@ -0,0 +1,56 @@ +en: + setting_input_reply: Input `value_in` needs to be `True` or `False` + commands: + groups: + stats: Administer stats on the server + posting: Posting stats + start: + command: Start posting + log_started: Task started + confirm_started: Stats posting started + stop: + command: Stop posting + log_stopped: Task stopped + confirm_stopped: Stats posting stopped + restart: + command: Restart posting + log_restarted: Task restarted + confirm_restarted: Stats posting restarted + list: + command: Get available settings + headers: + settings: + setting: Setting + value: Value + value_type: Valute type + hidden_roles: + hidden_name: Name + hidden_id: ID + stats_msg_out: + sub_settings: Innstillinger + sub_hidden: Hidden roles + setting: + command: Change settings + update_confirmed: Settings updated + hide_roles_add: + command: Add roles to hidden roles + msg: + already_hidden: Role is already hidden + confirm_added: Role added as hidden role + hide_roles_remove: + command: Remove roles from hidden roles + msg: + confirm_removed: Role removed as hidden role + restart: + command: Restart the stats task + msg: + confirm_restarted: Stats task restarted + tasks: + update_stats: + stats_msg: + members_sub: Members + members_num: Number of members + code_sub: Code base + code_files: Files of code + code_lines: Lines of code + code_last_updated: Serverstats was last updated diff --git a/sausage_bot/locale/stats.no.yml b/sausage_bot/locale/stats.no.yml new file mode 100755 index 0000000..6dc1c72 --- /dev/null +++ b/sausage_bot/locale/stats.no.yml @@ -0,0 +1,57 @@ +no: + setting_log: Feil input for `value_in` ({e}) + setting_input_reply: Input `value_in` må være `True` eller `False` + commands: + groups: + stats: Administrer stats på serveren + posting: Poster stats + start: + command: Start posting + log_started: Oppgave startet + confirm_started: Posting av stats startet + stop: + command: Stop posting + log_stopped: Oppgave stoppet + confirm_stopped: Posting av stats stoppet + restart: + command: Restart posting + log_restarted: Restartet oppgave + confirm_restarted: Restartet posting av stats + list: + command: Hent tilgjengelige innstillinger + headers: + settings: + setting: Innstilling + value: Verdi + value_type: Type + hidden_roles: + hidden_name: Navn + hidden_id: ID + stats_msg_out: + sub_settings: Innstillinger + sub_hidden: Skjulte roller + setting: + command: Endre innstillinger + update_confirmed: Innstillinger oppdatert + hide_roles_add: + command: Legg rolle til skjulte roller + msg: + already_hidden: Rollen er allerede skjult + confirm_added: Rolle lagt til som skjult rolle + hide_roles_remove: + command: Fjern rolle fra skjulte roller + msg: + confirm_removed: Rolle fjernet som skjult rolle + restart: + command: Restart oppgaven + msg: + confirm_restarted: Restartet stats oppgaven + tasks: + update_stats: + stats_msg: + members_sub: Medlemmer + members_num: Antall medlemmer + code_sub: Kodebase + code_files: Antall filer med kode + code_lines: Antall linjer med kode + code_last_updated: Serverstats ble sist oppdatert diff --git a/sausage_bot/locale/youtube.en.yml b/sausage_bot/locale/youtube.en.yml new file mode 100755 index 0000000..2ef457f --- /dev/null +++ b/sausage_bot/locale/youtube.en.yml @@ -0,0 +1,56 @@ +en: + groups: + youtube: Administer YouTube feeds + filter: Filter YouTube feeds + posting: Posting from YouTube feeds + commands: + start: + cmd: Start posting + msg_confirm: Youtube posting started + stop: + cmd: Stop posting + msg_confirm: Youtube posting stopped + add: + cmd: Add a YouTube-feed + desc: + feed_name: Name of feed to manage + youtube_link: The link to the YouTube channel to get videos from + channel: The channel to post to + msg_empty_link: 'Klarer ikke å hente linken: `%{link}`' + log_feed_confirm: '%{user} added %{feed_name} (`%{yt_link}`) to #%{channel}' + msg_added: 'Added %{feed_name} to channel %{channel_name}' + remove: + cmd: Remove a YouTube feed + desc: + feed_name: Name of Youtube feed to manage + msg_remove_non_existing_feed: The youtube feed `%{feed_name}` does not exist + msg_feed_removed: 'Removed Youtube feed `{feed_name}` from channel `{channel_name}`' + log_feed_removed: 'Youtube feed `%{feed_name}` removed by %{user_name}' + msg_feed_remove_failed: 'The Youtube feed `{feed_name}` could not be removed' + log_feed_remove_failed: 'User %{user_name} tried to remove the Youtube feed `%{feed_name}`, but it failed' + filter_add: + cmd: Add filters on an Youtube feed + desc: + feed_name: Name of Youtube feed to manage + allow_deny: 'Should the filter `allow` or `deny`.' + filters_in: 'What keyword to filter a post by.' + msg_filter_added: 'Added filter `%{filter_in} as `%{allow_deny}`' + msg_filter_failed: 'Error when adding filter, check logs' + filter_remove: + cmd: Remove filters on an Youtube feed + desc: + feed_name: Name of feed + filter_in: What filter to look for + msg_confirm: 'Removed filter `%{filter_in}`' + msg_error: Error when removing filter `{filter_in}`, check logs + list: + cmd: List all active YouTube feeds + desc: + list_type: 'Specify type of list' + msg_error: No feeds added + literal_type: + normal: Normal + added: Added + filter: Filter + tasks: + log_error: '%{feed_name} returned NoneType' \ No newline at end of file diff --git a/sausage_bot/locale/youtube.no.yml b/sausage_bot/locale/youtube.no.yml new file mode 100755 index 0000000..574c1aa --- /dev/null +++ b/sausage_bot/locale/youtube.no.yml @@ -0,0 +1,56 @@ +no: + groups: + youtube: Administrer YouTube-feeder + filter: Filtrer YouTube-feeder + posting: Posting fra YouTube-feeder + commands: + start: + cmd: Start posting + msg_confirm: Youtube-posting startet + stop: + cmd: Stop posting + msg_confirm: Youtube posting stoppet + add: + cmd: Legg til en YouTube-feed + desc: + feed_name: Navn på feeden som skal håndteres + youtube_link: Link til YouTube-kanalen + channel: Kanalen å poste til + msg_empty_link: 'Klarer ikke å hente linken: `%{link}`' + log_feed_confirm: '%{user} la til %{feed_name} (`%{yt_link}`) til #%{channel}' + msg_added: 'La til %{feed_name} til #%{channel_name}' + remove: + cmd: Fjern en YouTube-feed + desc: + feed_name: Navn på YouTube-feeden som skal håndteres + msg_remove_non_existing_feed: Youtube-feeden `%{feed_name}` finnes ikke + msg_feed_removed: 'Fjernet Youtube-feed `%{feed_name}` fra #%{channel_name}' + log_feed_removed: 'Youtube-feed `%{feed_name}` fjernet av %{user_name}' + msg_feed_remove_failed: 'Youtube-feed `%{feed_name}` kunne ikke fjernes' + log_feed_remove_failed: 'Bruker %{user_name} prøvde å fjerne Youtube-feed `%{feed_name}`, men det feilet' + filter_add: + cmd: Legg til filter på en Youtube-feed + desc: + feed_name: Navn på Youtube-feeden som skal håndteres + allow_deny: 'Om filteret skal `tillate` eller `nekte`' + filters_in: Hvilket nøkkelord en post skal filtreres mot + msg_filter_added: 'La til filter `%{filter_in}` som `%{allow_deny}`' + msg_filter_failed: Feilet ved forsøk på å legge til filter, sjekk logger + filter_remove: + cmd: Fjern filter på en Youtube-feed + desc: + feed_name: Navn på Youtube-feeden som skal håndteres + filter_in: Hvilket nøkkelord en post skal filtreres mot + msg_confirm: 'Fjernet filter `%{filter_in}`' + msg_error: Feilet ved forsøk på fjerning av filter, sjekk logger + list: + cmd: List alle active YouTube-feeder + desc: + list_type: Velg type liste + msg_error: Ingen feeder lagt til + literal_type: + normal: Normal + added: Lagt til + filter: Filter + tasks: + log_error: '%{feed_name} returnerte NoneType' \ No newline at end of file diff --git a/sausage_bot/util/args.py b/sausage_bot/util/args.py index 18b5c10..8272353 100755 --- a/sausage_bot/util/args.py +++ b/sausage_bot/util/args.py @@ -29,6 +29,11 @@ action='store_true', default=False, dest='log_error') +logging_args.add_argument('--i18n', '-i', + help='Log i18n errors', + action='store_true', + default=False, + dest='log_i18n') logging_args.add_argument('--log-print', '-lp', help='Print logging to output', action='store_true', diff --git a/sausage_bot/util/datetime_handling.py b/sausage_bot/util/datetime_handling.py index f900439..13a3ed2 100755 --- a/sausage_bot/util/datetime_handling.py +++ b/sausage_bot/util/datetime_handling.py @@ -242,7 +242,7 @@ def change_dt( `unit`: Unit to change. Accepted units are `years`, `months`, `days`, `hours`, `minutes` and `seconds`''' if change is None or unit is None or count is None: - log.log(envs.TOO_FEW_ARGUMENTS) + log.log(I18N.t('common.too_few_arguments')) return None accepted_units = [ 'years', 'months', 'days', 'hours', 'minutes', 'seconds' diff --git a/sausage_bot/util/discord_commands.py b/sausage_bot/util/discord_commands.py index 601e8d9..2ef85d9 100755 --- a/sausage_bot/util/discord_commands.py +++ b/sausage_bot/util/discord_commands.py @@ -5,6 +5,7 @@ from sausage_bot.util import config, envs from sausage_bot.util.datetime_handling import get_dt +from sausage_bot.util.i18n import I18N from .log import log @@ -154,7 +155,20 @@ async def get_sorted_scheduled_events(): sched_dict['id'].append(event_dict[event]['id']) out = tabulate( sched_dict, - headers=['Kamp', 'Dato', 'Intr.', 'ID'], + headers=[ + I18N.t( + 'discord_commands.get_sorted_scheduled_events.headers.match' + ), + I18N.t( + 'discord_commands.get_sorted_scheduled_events.headers.date' + ), + I18N.t( + 'discord_commands.get_sorted_scheduled_events.headers.interest' + ), + I18N.t( + 'discord_commands.get_sorted_scheduled_events.headers.id' + ) + ], numalign='center' ) out = '```{}```'.format(out) @@ -271,17 +285,14 @@ async def update_stats_post(stats_info, stats_channel): channel_out = config.bot.get_channel(server_channels[stats_channel]) found_stats_msg = False async for msg in channel_out.history(limit=10): - # TODO var msg log.debug(f'Got msg: ({msg.author.id}) {msg.content[0:50]}...') if str(msg.author.id) == config.BOT_ID: if 'Serverstats sist' in str(msg.content): - # TODO var msg log.debug('Found post with `Serverstats sist`, editing...') await msg.edit(content=stats_info) found_stats_msg = True return if found_stats_msg is False: - # TODO var msg log.debug('Creating stats message') await channel_out.send(stats_info) @@ -297,7 +308,6 @@ async def remove_stats_post(stats_channel): channel_out = config.bot.get_channel(server_channels[stats_channel]) found_stats_msg = False async for msg in channel_out.history(limit=10): - # TODO var msg log.debug(f'Got msg: ({msg.author.id}) {msg.content[0:50]}...') if str(msg.author.id) == config.BOT_ID: if 'Serverstats sist' in str(msg.content): @@ -309,7 +319,6 @@ async def remove_stats_post(stats_channel): found_stats_msg = True return if found_stats_msg is False: - # TODO var msg log.debug('No stats post found') diff --git a/sausage_bot/util/envs.py b/sausage_bot/util/envs.py index 4d5e245..9a95dbe 100755 --- a/sausage_bot/util/envs.py +++ b/sausage_bot/util/envs.py @@ -19,6 +19,7 @@ STATIC_DIR = DATA_DIR / 'static' TEMP_DIR = ROOT_DIR / 'tempfiles' MERMAID_DIR = ROOT_DIR / 'docs' / 'mermaid_charts' +LOCALE_DIR = ROOT_DIR / 'locale' # Relative paths COGS_REL_DIR = 'sausage_bot.cogs' @@ -324,7 +325,6 @@ 'autoincrement': False } - youtube_db_log_schema = { 'db_file': str(DB_DIR / 'youtube_log.sqlite'), 'name': 'log', @@ -337,6 +337,16 @@ 'autoincrement': False } +locale_db_schema = { + 'db_file': str(DB_DIR / 'locale.sqlite'), + 'name': 'locale', + 'items': [ + 'locale TEXT' + ], + 'primary': None, + 'autoincrement': False +} + def log_extra_info(type): infos = { @@ -345,7 +355,8 @@ def log_extra_info(type): 'verbose': 'VERBOSE', 'database': 'DATABASES', 'debug': 'DEBUG', - 'error': 'ERROR' + 'error': 'ERROR', + 'i18n': 'I18N' }, 'length': 9 } @@ -358,54 +369,18 @@ def log_extra_info(type): ### Botlines ### # Generiske GUILD_NOT_FOUND = 'Fant ikke serveren {}, dobbeltsjekk navnet i .env' -NOT_AUTHORIZED = 'Du har ikke tilgang til den kommandoen' -TOO_MANY_ARGUMENTS = 'Du har gitt for mange argumenter til kommandoen' -TOO_FEW_ARGUMENTS = 'Du har gitt for få argumenter til kommandoen' -CHANNEL_NOT_FOUND = 'Finner ikke kanalen `{}` på denne discord-serveren' POST_TO_NON_EXISTING_CHANNEL = 'Prøver å poste til {}, men kanalen '\ 'finnes ikke' ERROR_WITH_ERROR_MSG = 'Feil: {}' -COMPARING_IDS = 'Sammenligner med `{}` ({}) med `{}` ({})' -CREATING_DB_FILES = 'Sjekker databasefil' -BOT_NOT_SET_UP = 'The bot is not properly set up' - -# MAIN -PURGE_DESC = 'Successfully purged {} messages.\nCommand executed by {}.' - -# COG - COG ADMIN IN MAIN -COGS_TOO_FEW_ARGUMENTS = 'Du har gitt for få argumenter til kommandoen' -COGS_CHANGE_STATUS_FAIL = 'Klarte ikke å endre status. Feilmelding: {}' -COGS_WRONG_STATUS = 'Kjente ikke igjen status `{}`' -COGS_ENABLED = 'Aktiverte `{}`' -COGS_DISABLED = 'Deaktiverte `{}`' -ALL_COGS_ENABLED = 'Aktiverte alle cogs' -ALL_COGS_DISABLED = 'Deaktiverte alle cogs' # COG - GENERIC MESSAGES COG_STARTING = 'Starting cog: `{}`' # COG # RSS -RSS_URL_NOT_OK = 'Linken du ga ser ikke ut til å være en ordenlig URL' -RSS_URL_AND_CHANNEL_NOT_OK = 'Du må oppgi både link og hvilken kanal '\ - 'du ønsker den skal publiseres til.' -RSS_ADDED = '{} ble lag til i kanalen {}' -RSS_ADDED_BOT = '{} la til feeden {} ({}) til kanalen {}' -RSS_REMOVED = 'RSS-feeden `{}` ble fjernet' -RSS_REMOVED_BOT = 'RSS-feeden `{}` ble fjernet av {}' -RSS_TRIED_REMOVED_BOT = '{} forsøkte å fjerne RSS-feeden `{}`, '\ - 'men det oppsto en feil' -RSS_COULD_NOT_REMOVE = 'Klarte ikke å fjerne RSS-feeden {}' -RSS_FEED_CHANNEL_CHANGE = 'rss: {} endret kanalen til feeden `{}` til `{}`' -RSS_LIST_ARG_WRONG = 'Kjenner ikke til kommandoen {}' RSS_INVALID_URL = 'Inputen `{}` er ikke en ordentlig URL. Dobbelsjekk staving.' -RSS_MISSING_SCHEME = 'URLen `{}` hadde ikke (http/https). Legger til og '\ - 'prøver igjen...' -RSS_CONNECTION_ERROR = 'Feil ved oppkobling til URLen: {}' -RSS_NOT_ABLE_TO_SCRAPE = 'Klarte ikke å scrape {}: {}' RSS_NO_FEEDS_FOUND = 'Fant ingen RSS-feeds' RSS_FEED_POSTS_IS_NONE = '{}: this feed returned NoneType.' -RSS_CHANGED_CHANNEL = 'Endret kanal for feeden `{}` til `{}`' RSS_VARS = { 'feed_name': { 'title': 'Name', 'db_col': 'feed_name', 'max_len': 0, 'list_type': [] @@ -432,40 +407,15 @@ def log_extra_info(type): # CORE FEEDS_SOUP_ERROR = 'Feil ved lesing av `soup` fra {}: {}' -FEEDS_LINK_INDEX_ERROR = 'Fikk IndexError ved henting av link til {} i {}' -FEEDS_NONE_VALUE_AS_TEXT = 'Ingen' FEEDS_URL_ERROR = 'Failed' FEEDS_URL_STALE = 'Stale' FEEDS_URL_ERROR_LIMIT = 3 FEEDS_URL_SUCCESS = 'OK' CHANNEL_STATUS_ERROR = 'Failed' CHANNEL_STATUS_SUCCESS = 'OK' -NET_IO_CONNECTION_ERROR = 'Feil ved oppkobling til `{}`: {}' -NET_IO_TIMEOUT = 'Oppkobling til `{}` gikk ut på tid: {}' -NET_IO_ERROR_RESPONSE = 'Got a {} response (HTTP {}) when fetching {}. '\ - 'If this causes problems, you need to check the link.' - -# COG - QUOTE -QUOTE_EDIT_CONFIRMED = 'Endret sitat' -QUOTE_NO_EDIT_CONFIRMED = 'Endret *ikke* sitat' -QUOTE_CONFIRM_DELETE = 'Er du sikker på at du vil slette følgende sitat?'\ - '\n```# {}\n{}\n({})```\n' -QUOTE_DENY_DELETE = 'Sitatet slettes ikke' -QUOTE_DELETE_CONFIRMED = 'Slettet sitat # {}' -QUOTE_COUNT = 'Jeg har {} sitater på lager' -QUOTE_DOES_NOT_EXIST = 'Sitat nummer {} finnes ikke' # COG - YOUTUBE -YOUTUBE_NO_FEEDS_FOUND = 'Fant ingen Youtube-feeds' YOUTUBE_RSS_LINK = 'https://www.youtube.com/feeds/videos.xml?channel_id={}' -YOUTUBE_ADDED = '{} ble lag til i kanalen {}' -YOUTUBE_ADDED_BOT = '{} la til feeden {} (`{}`) til kanalen {}' -YOUTUBE_REMOVED = 'Youtube-feeden {} ble fjernet' -YOUTUBE_REMOVED_BOT = 'Youtube-feeden {} ble fjernet av {}' -YOUTUBE_TRIED_REMOVED_BOT = '{} forsøkte å fjerne Youtube-feeden {}' -YOUTUBE_COULD_NOT_REMOVE = 'Klarte ikke å fjerne Youtube-feeden {}' -YOUTUBE_FEED_POSTS_IS_NONE = '{}: this feed returned NoneType.' -YOUTUBE_EMPTY_LINK = 'Klarer ikke å hente linken: `{}`' YOUTUBE_VARS = { 'url': { 'title': 'Feed', 'max_len': 0, 'list_type': [] @@ -489,25 +439,6 @@ def log_extra_info(type): # COG - AUTOEVENT AUTOEVENT_PARSE_ERROR = 'Klarte ikke parsing av {} - fikk følgende feil:\n{}' -AUTOEVENT_NO_EVENTS_LISTED = 'Ingen events ligger inne for øyeblikket' -AUTOEVENT_EVENT_FOUND = 'Fant event: {}' -AUTOEVENT_EVENT_NOT_FOUND = 'Fant ingen eventer med den IDen. Sjekk '\ - 'liste på nytt med `!autoevent list`' -AUTOEVENT_START_TIME_NOT_CORRECT_FORMAT = '`start_time` ser ikke ut til å '\ - 'være i riktig format' -AUTOEVENT_EVENT_START_IN_PAST = 'Kan ikke lage en event med starttid i fortida' -AUTOEVENT_HTTP_EXCEPTION_ERROR = 'Got an error when posting event: {}' - -# COG - ROLES -ROLES_KEY_PHRASES = [ - 'Svar på denne meldingen med navnet på en rolle', - 'Timed out' -] - -# COG - DILEMMAS -DILEMMAS_NO_DILEMMAS_IN_DB = 'Fant ingen lagrede dilemmas' -DILEMMAS_COUNT = 'Fant {} dilemma' - # VARIABLES input_split_regex = r'[\s\.\-_,;\\\/]+' diff --git a/sausage_bot/util/i18n.py b/sausage_bot/util/i18n.py new file mode 100755 index 0000000..6b82f7c --- /dev/null +++ b/sausage_bot/util/i18n.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +import os +import re +import discord +from discord import app_commands + +from .log import log +from . import envs, db_helper + +import i18n as _i18n +_i18n.load_path.append(envs.LOCALE_DIR) +I18N = _i18n +_i18n.set('fallback', 'en') + +if log.i18n: + # Clean i18n log file before starting + _logfilename = envs.LOG_DIR / 'i18n.log' + write_log = open(_logfilename, 'w', encoding="utf-8") + write_log.write('') + write_log.close() + + +class MyTranslator(app_commands.Translator): + async def translate( + self, + string: app_commands.locale_str, + locale: discord.Locale, + context: app_commands.TranslationContext, + ) -> str | None: + return I18N.t(str(string)) + + +def handler_placeholder(key, locale, text, name): + _error = f'Missing placeholder {name!r} while translating {key!r} to '\ + f'{locale!r} (in {text!r})' + log.i18n(_error) + return 'undefined' + + +def handler_translation(key, locale, **kwargs): + _error = f'Missing translation for {key!r} in {locale!r}' + log.i18n(_error) + return 'undefined' + + +def handler_plural(key, locale, **kwargs): + _error = f'Missing plural for {key!r} in {locale!r}' + log.i18n(_error) + return 'undefined' + + +_i18n.set('on_missing_placeholder', handler_placeholder) +#_i18n.set('on_missing_translation', handler_translation) +_i18n.set('on_missing_plural', handler_plural) + + +def reload_i18n(): + I18N.reload_everything() + + +def available_languages(): + lang_list = [] + for filename in os.listdir(envs.LOCALE_DIR): + file_check = re.search(r'.*\.(.*)\.yml$', filename).group(1) + if file_check and file_check not in lang_list: + lang_list.append(file_check) + return lang_list + + +async def set_language(lang: str): + if lang in available_languages(): + I18N.set('locale', lang) + await db_helper.update_fields( + template_info=envs.locale_db_schema, + updates=[('locale', lang)] + ) + I18N.reload_everything() diff --git a/sausage_bot/util/log/log.py b/sausage_bot/util/log/log.py index 590eda8..89dc675 100755 --- a/sausage_bot/util/log/log.py +++ b/sausage_bot/util/log/log.py @@ -24,6 +24,7 @@ args.log_db = True args.debug = True args.log_error = True + args.log_i18n = True def log_function( @@ -93,8 +94,8 @@ def log_function( log_out_print += ' ' * rem_len if pretty and isinstance(pretty, (dict, list, tuple)): pretty_log = json.dumps( - pretty, indent=4, ensure_ascii=False - ) + pretty, indent=4, ensure_ascii=False + ) else: pretty_log = None if args.log_print: @@ -160,14 +161,15 @@ def verbose( log_in, color=color, sameline=sameline, extra_info=envs.log_extra_info('verbose'), pretty=pretty if pretty else None - ) if args.log_slow: sleep(3) def debug( - log_in: str, color: str = None, pretty: dict | list | tuple = None, + log_in: str, color: str = None, + extra_info: str = envs.log_extra_info('database'), + extra_color: str = None, pretty: dict | list | tuple = None, sameline: bool = False ): ''' @@ -176,12 +178,15 @@ def debug( color Specify the color for highlighting the function name: black, red, green, yellow, blue, magenta, cyan, white. If `color` is not specified, it will highlight in green + extra_info Used to specify extra information in the logging + extra_color Color for the `extra_info` pretty Prettify specific output. Works on dict, list and tuple sameline When printing log, reuse the same line ''' if args.debug: log_function( - log_in, color=color, extra_info=envs.log_extra_info('debug'), + log_in, color=color, extra_color=extra_color, + extra_info=extra_info, sameline=sameline, pretty=pretty if pretty else None ) if args.log_slow: @@ -189,8 +194,10 @@ def debug( def db( - log_in: str, color: str = 'magenta', extra_color: str = None, - pretty: dict | list | tuple = None, sameline: bool = False + log_in: str, color: str = 'magenta', + extra_info: str = envs.log_extra_info('database'), + extra_color: str = None, pretty: dict | list | tuple = None, + sameline: bool = False ): ''' Log database input specifically @@ -206,7 +213,7 @@ def db( if args.log_db: log_function( log_in, color=color, extra_color=extra_color, - extra_info=envs.log_extra_info('database'), + extra_info=extra_info, sameline=sameline, pretty=pretty if pretty else None ) if args.log_slow: @@ -214,8 +221,10 @@ def db( def error( - log_in: str, color: str = 'red', pretty: dict | list | tuple = None, - sameline: bool = False + log_in: str, color: str = 'red', + extra_info: str = envs.log_extra_info('error'), + extra_color: str = None, pretty: dict | list | tuple = None, + sameline: bool = False ): ''' Log the input `log_in`. Used as more verbose than `log` @@ -224,18 +233,36 @@ def error( color Specify the color for highlighting the function name: black, red, green, yellow, blue, magenta, cyan, white. If `color` is not specified, it will highlight in red. + extra_info Used to specify extra information in the logging + extra_color Color for the `extra_info` pretty Prettify specific output. Works on dict, list and tuple sameline When printing log, reuse the same line ''' if args.log_error: log_function( - log_in, color=color, extra_info=envs.log_extra_info('error'), - sameline=sameline, pretty=pretty if pretty else None + log_in, color=color, extra_color=extra_color, + extra_info=extra_info, sameline=sameline, + pretty=pretty if pretty else None ) if args.log_slow: sleep(3) +def i18n(log_in: str): + ''' + Log the input from errors in i18n with `log_in`. + ''' + if args.log_i18n: + dt = pendulum.now(config.TIMEZONE) + dt_full = dt.format('DD.MM.YYYY HH.mm.ss') + log_out = '[ {dt} ] {log_in}\n'.format(dt=dt_full, log_in=str(log_in)) + dt = pendulum.now(config.TIMEZONE) + _logfilename = envs.LOG_DIR / 'i18n.log' + write_log = open(_logfilename, 'a+', encoding="utf-8") + write_log.write(log_out) + write_log.close() + + def log_func_name() -> dict: 'Get the function name that the `log`-function is used within' frame_line = sys._getframe(3).f_lineno