diff --git a/Dockerfile b/Dockerfile index cd2a1993..e64d1547 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ WORKDIR /app COPY README.md . COPY setup.cfg . COPY setup.py . +COPY MANIFEST.in . COPY .streamlit .streamlit COPY defaults defaults COPY src src diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..f21a33b4 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include src/penn_chime/locales/*.yml \ No newline at end of file diff --git a/Pipfile b/Pipfile index 17610092..eae107a2 100644 --- a/Pipfile +++ b/Pipfile @@ -16,6 +16,9 @@ dash = "*" dash-bootstrap-components = "*" gunicorn = "*" PyYAML = "*" +python-i18n = "*" +gspread="*" +oauth2client="*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index a5cd0cac..c5426171 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4c43987fc1ab7a4ca8d36ace9c17a1c36076c3fa2898d913e35dc2b20cac0eab" + "sha256": "06f856f537b7782cf3dcb13159da803c8761892eee3c54ad1649b8c32abce33c" }, "pipfile-spec": 6, "requires": { @@ -29,7 +29,7 @@ "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" ], - "markers": "sys_platform == 'darwin'", + "markers": "platform_system == 'Darwin'", "version": "==0.1.0" }, "astor": { @@ -68,31 +68,31 @@ }, "boto3": { "hashes": [ - "sha256:5246caf509baa4716065e6bb78bdc516fdd6b0dfbd9098cc2a0f779fad789c6c", - "sha256:52b8de35f6647e3b8ce81f6a745a67812623b5c4acc2d6bd5e814fddfa488321" + "sha256:970bd7b332e73d7b51077ed36772c634811b38c81b0cc6ed0f910e50d7ebadf8", + "sha256:cdd79a3a7bbe1f33a365f0acfcc75c4405b482b3eb9ce3f4e6b16c418e201ac3" ], - "version": "==1.12.34" + "version": "==1.12.39" }, "botocore": { "hashes": [ - "sha256:c799623598d04c66b0be4cb990c01a24bd3c06023f0c7221adead38a7431c994", - "sha256:db0fba3f4adfb9bf3976aae10efa9acb59e3efe52ce8feac71ecac0b7be2fc2e" + "sha256:94232b44e1540b7e043e220bd43f855400d0a243e926b26b3fb72994e971d518", + "sha256:e20ba56476b1031ce5ac8e22b59dabc75bd0e03231f124ed6b9ff99fe0b0c96b" ], - "version": "==1.15.34" + "version": "==1.15.39" }, "cachetools": { "hashes": [ - "sha256:9a52dd97a85f257f4e4127f15818e71a0c7899f121b34591fcc1173ea79a0198", - "sha256:b304586d357c43221856be51d73387f93e2a961598a9b6b6670664746f3b6c6c" + "sha256:1d057645db16ca7fe1f3bd953558897603d6f0b9c51ed9d11eb4d071ec4e2aab", + "sha256:de5d88f87781602201cde465d3afe837546663b168e8b39df67411b0bf10cefc" ], - "version": "==4.0.0" + "version": "==4.1.0" }, "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" ], - "version": "==2019.11.28" + "version": "==2020.4.5.1" }, "chardet": { "hashes": [ @@ -110,41 +110,41 @@ }, "dash": { "hashes": [ - "sha256:6a488d380f02ba7c8e624a7c4d89f9e39368b8d9ed4a6f463364894a237f1fd8" + "sha256:339d8066ccf464bcb4a215688e3fbc1136f7c2f02481052c3f83805311e6659d" ], "index": "pypi", - "version": "==1.9.1" + "version": "==1.10.0" }, "dash-bootstrap-components": { "hashes": [ - "sha256:a958ec54ad61b9b2d9842f1afba8999d6b276a7eaa346c2e5645c21580f0c20d" + "sha256:ab6ff87baf3b1bab0877694b1a8eb8ad561ef7b06b55941098b740e5016edce0" ], "index": "pypi", - "version": "==0.9.1" + "version": "==0.9.2" }, "dash-core-components": { "hashes": [ - "sha256:9a8901b67d3a1038fb79153c17234d5713c2d7cca0382718c6376c786b280e63" + "sha256:e5d45237d7645a3b6a54d7c14b147339cd17d15db7611ca0df091f0c134a26f3" ], - "version": "==1.8.1" + "version": "==1.9.0" }, "dash-html-components": { "hashes": [ - "sha256:8992097a044f5add21f86ccbe0caef8c499394a04d5f2b6cc4458a42f37cca98" + "sha256:dafb54ae8ab601fffe50c74d72b32783dec2beea65fd1c7e7dd6a66e20e545ba" ], - "version": "==1.0.2" + "version": "==1.0.3" }, "dash-renderer": { "hashes": [ - "sha256:74aab93b6332290d9bff5f81060a5c4cbf0c393f8ccff85b2edb1ae976bdd5f0" + "sha256:2f656e12d3cfbadb0a4696270dc5e254db55a01e64ee6585820988c1a454de1e" ], - "version": "==1.2.4" + "version": "==1.3.0" }, "dash-table": { "hashes": [ - "sha256:64905af3214b509a95769ba9dfc884677696c270a572ed06b52c57fdcf9d9c77" + "sha256:5ceabd434bc63a9d1aa5bf4f00b55d2e5342072e274f0ee6fc53bc58bef4c1c2" ], - "version": "==4.6.1" + "version": "==4.6.2" }, "decorator": { "hashes": [ @@ -177,10 +177,10 @@ }, "flask": { "hashes": [ - "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", - "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" + "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", + "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" ], - "version": "==1.1.1" + "version": "==1.1.2" }, "flask-compress": { "hashes": [ @@ -194,6 +194,21 @@ ], "version": "==0.18.2" }, + "google-auth": { + "hashes": [ + "sha256:a5ee4c40fef77ea756cf2f1c0adcf475ecb53af6700cf9c133354cdc9b267148", + "sha256:cab6c707e6ee20e567e348168a5c69dc6480384f777a9e5159f4299ad177dcc0" + ], + "version": "==1.13.1" + }, + "gspread": { + "hashes": [ + "sha256:19de5bc716c673fbccc6bb82c27d042fd293d58217bd9efc40ba8cff038d83b6", + "sha256:59c6525720b69fda713fc9eed597ea11f419959ebc3ed7427eecb2daaf51de2b" + ], + "index": "pypi", + "version": "==3.4.2" + }, "gunicorn": { "hashes": [ "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", @@ -202,6 +217,13 @@ "index": "pypi", "version": "==20.0.4" }, + "httplib2": { + "hashes": [ + "sha256:4e26d60a6b37ff0e8401e976f13667cb3746dfa1b50833003b7f138042b54247", + "sha256:b81b2cd2248285168a4359f2acf24521a4099b5853e790309c45dcb90ee4b3c6" + ], + "version": "==0.17.1" + }, "idna": { "hashes": [ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", @@ -367,6 +389,14 @@ "index": "pypi", "version": "==1.18.2" }, + "oauth2client": { + "hashes": [ + "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac", + "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6" + ], + "index": "pypi", + "version": "==4.1.3" + }, "packaging": { "hashes": [ "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", @@ -426,30 +456,30 @@ }, "pillow": { "hashes": [ - "sha256:0011ec16bcab9f2f07afa95081ee025b3f0fe428611a000df0fbcb51dd873ca0", - "sha256:0e5aedc62a78525fcfbec417d8db0073dff5de9d8544047f619d208e2cd3d323", - "sha256:196d9ff5d0b070b50cd3a3c59ffa3b41232ca34dbd09e262bfa32047229d4b16", - "sha256:1e1ed97d93676ba6e19124f8054ed439825292cecb490370729f90a27585f180", - "sha256:2f44dc402c643a88b9453c6fce84e20b16352df42a98ab013aaebe07e1a1c478", - "sha256:32facad1f01111505c0fa3f15b2acac13af231f6dc0e68927c8d2d23df8c25c5", - "sha256:3d4e8b208346374e820bc5f88e753a4a2d35b6ead0aa1f45dcc7b9206780b983", - "sha256:65422a17c9d08bf56d8eb0ce5c83863e050cec5f15e8b2042c955aa724ce515d", - "sha256:75513b87a441c093a26a75974cd8373d8d40476cdb178309e8c2f2a534033ea3", - "sha256:7ccf6e86bd9f7301b883c6ba180309ca6bd566b0324eb66566b0b841c974558e", - "sha256:7dacf81a6b6b724a263c27a364030f81eef97398acda67d1ac50e722ad3cec59", - "sha256:81655ad8b10acf81a644ad88faa9cd19ab2930f1b83ff6d32217f00de602b6e5", - "sha256:83ab411759b9e1f6a695bea321e835bed448f767711da2630d597dabc69d0350", - "sha256:87562ac8e35b873eabed6b02259dcec05d7c776fa6c478865fcb147f17ec7530", - "sha256:c85cce00769e162cf50fc21451be9e627f60a9be22172d684c05dc7d0141c55b", - "sha256:d071c33320c6f66730d1adea0bfd468ab5453627ff8974a6e56f43a48c60bce5", - "sha256:dd4c3cfa93626ed08005abf3aa52b50dae28bf0bc4cb5a4626e9ce6878e7f700", - "sha256:e0fbed3f29291c0e4b83c7f21809a709f5a46eefdad236ec2a11096ebb76c6ae", - "sha256:ecabe2323e4ed3ce24eaea2120aca395723dd301e9fb03d7e0912a61343fc7f3", - "sha256:f8e26f68b5f7b15ad9d1cc2c28ea57fe8c2f9dfee77f4e0519e833fdbe2feb8d", - "sha256:fb13e40bd17615a03192f03cab35df0fbfb61ab58ef08ece42884a6bd314faf4", - "sha256:fb1f9faa2984918cd7a3b18db1192463e0030500cb060c974b5fd98c13276a8e" - ], - "version": "==7.1.0" + "sha256:04a10558320eba9137d6a78ca6fc8f4a5801f1b971152938851dc4629d903579", + "sha256:0f89ddc77cf421b8cd34ae852309501458942bf370831b4a9b406156b599a14e", + "sha256:251e5618125ec12ac800265d7048f5857a8f8f1979db9ea3e11382e159d17f68", + "sha256:291bad7097b06d648222b769bbfcd61e40d0abdfe10df686d20ede36eb8162b6", + "sha256:2f0b52a08d175f10c8ea36685115681a484c55d24d0933f9fd911e4111c04144", + "sha256:3713386d1e9e79cea1c5e6aaac042841d7eef838cc577a3ca153c8bedf570287", + "sha256:433bbc2469a2351bea53666d97bb1eb30f0d56461735be02ea6b27654569f80f", + "sha256:4510c6b33277970b1af83c987277f9a08ec2b02cc20ac0f9234e4026136bb137", + "sha256:50a10b048f4dd81c092adad99fa5f7ba941edaf2f9590510109ac2a15e706695", + "sha256:670e58d3643971f4afd79191abd21623761c2ebe61db1c2cb4797d817c4ba1a7", + "sha256:6c1924ed7dbc6ad0636907693bbbdd3fdae1d73072963e71f5644b864bb10b4d", + "sha256:721c04d3c77c38086f1f95d1cd8df87f2f9a505a780acf8575912b3206479da1", + "sha256:8d5799243050c2833c2662b824dfb16aa98e408d2092805edea4300a408490e7", + "sha256:90cd441a1638ae176eab4d8b6b94ab4ec24b212ed4c3fbee2a6e74672481d4f8", + "sha256:a5dc9f28c0239ec2742d4273bd85b2aa84655be2564db7ad1eb8f64b1efcdc4c", + "sha256:b2f3e8cc52ecd259b94ca880fea0d15f4ebc6da2cd3db515389bb878d800270f", + "sha256:b7453750cf911785009423789d2e4e5393aae9cbb8b3f471dab854b85a26cb89", + "sha256:b99b2607b6cd58396f363b448cbe71d3c35e28f03e442ab00806463439629c2c", + "sha256:cd47793f7bc9285a88c2b5551d3f16a2ddd005789614a34c5f4a598c2a162383", + "sha256:d6bf085f6f9ec6a1724c187083b37b58a8048f86036d42d21802ed5d1fae4853", + "sha256:da737ab273f4d60ae552f82ad83f7cbd0e173ca30ca20b160f708c92742ee212", + "sha256:eb84e7e5b07ff3725ab05977ac56d5eeb0c510795aeb48e8b691491be3c5745b" + ], + "version": "==7.1.1" }, "plotly": { "hashes": [ @@ -495,12 +525,26 @@ ], "version": "==0.6.0" }, + "pyasn1": { + "hashes": [ + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" + ], + "version": "==0.4.8" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", + "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74" + ], + "version": "==0.2.8" + }, "pydeck": { "hashes": [ - "sha256:9d69a0fdc0534e07aef8d59e54d3bfc51f98d3fcb2378bfe610a60c027854c32", - "sha256:b7dd2acfdfb72f1c1cbf9b04319b93fac5b7f5c3551e9a9331d5bc1457926765" + "sha256:495dcfa48b01196b2827e9e0fec5b82e1b77f952a8e6e1253dfc5538e5df5f00", + "sha256:9b6da8ea25acfd678cf3c533276d1490035bc45f23aebcf45a9ee096a5ba5ab7" ], - "version": "==0.3.0b3" + "version": "==0.3.0b4" }, "pygments": { "hashes": [ @@ -511,10 +555,10 @@ }, "pyparsing": { "hashes": [ - "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", - "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492", + "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a" ], - "version": "==2.4.6" + "version": "==3.0.0a1" }, "pyrsistent": { "hashes": [ @@ -529,6 +573,14 @@ ], "version": "==2.8.1" }, + "python-i18n": { + "hashes": [ + "sha256:095cb67a449f9caaddb4ca034255dcb442b1d1aca874294eda537bb5ea919892", + "sha256:85954be9c16b53221b42ebdf5164d6e6fef80f2dd0e388c7a468687330bba1d3" + ], + "index": "pypi", + "version": "==0.3.7" + }, "pytz": { "hashes": [ "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", @@ -599,6 +651,13 @@ ], "version": "==1.3.3" }, + "rsa": { + "hashes": [ + "sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66", + "sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487" + ], + "version": "==4.0" + }, "s3transfer": { "hashes": [ "sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13", @@ -615,10 +674,10 @@ }, "streamlit": { "hashes": [ - "sha256:8543ec85f43d3a33b96606ca8b9295874ad9117cdf4bf550633b35e13cf3f6a8" + "sha256:e2bcfa2bd984c73b998f109aa6a38f3afcd962627be8af90a5b6cc6deed26c81" ], "index": "pypi", - "version": "==0.57.2" + "version": "==0.57.3" }, "toml": { "hashes": [ @@ -669,9 +728,9 @@ }, "validators": { "hashes": [ - "sha256:b192e6bde7d617811d59f50584ed240b580375648cd032d106edeb3164099508" + "sha256:6a0d9502219aee486f1ee12d8a9635e4a56f3dbcfa204b4e0de3a038ae35f34f" ], - "version": "==0.14.2" + "version": "==0.14.3" }, "watchdog": { "hashes": [ @@ -762,10 +821,10 @@ }, "pathspec": { "hashes": [ - "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", - "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" ], - "version": "==0.7.0" + "version": "==0.8.0" }, "pluggy": { "hashes": [ @@ -783,10 +842,10 @@ }, "pyparsing": { "hashes": [ - "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", - "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492", + "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a" ], - "version": "==2.4.6" + "version": "==3.0.0a1" }, "pytest": { "hashes": [ @@ -798,29 +857,29 @@ }, "regex": { "hashes": [ - "sha256:01b2d70cbaed11f72e57c1cfbaca71b02e3b98f739ce33f5f26f71859ad90431", - "sha256:046e83a8b160aff37e7034139a336b660b01dbfe58706f9d73f5cdc6b3460242", - "sha256:113309e819634f499d0006f6200700c8209a2a8bf6bd1bdc863a4d9d6776a5d1", - "sha256:200539b5124bc4721247a823a47d116a7a23e62cc6695744e3eb5454a8888e6d", - "sha256:25f4ce26b68425b80a233ce7b6218743c71cf7297dbe02feab1d711a2bf90045", - "sha256:269f0c5ff23639316b29f31df199f401e4cb87529eafff0c76828071635d417b", - "sha256:5de40649d4f88a15c9489ed37f88f053c15400257eeb18425ac7ed0a4e119400", - "sha256:7f78f963e62a61e294adb6ff5db901b629ef78cb2a1cfce3cf4eeba80c1c67aa", - "sha256:82469a0c1330a4beb3d42568f82dffa32226ced006e0b063719468dcd40ffdf0", - "sha256:8c2b7fa4d72781577ac45ab658da44c7518e6d96e2a50d04ecb0fd8f28b21d69", - "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74", - "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb", - "sha256:9b64a4cc825ec4df262050c17e18f60252cdd94742b4ba1286bcfe481f1c0f26", - "sha256:9e9624440d754733eddbcd4614378c18713d2d9d0dc647cf9c72f64e39671be5", - "sha256:9ff16d994309b26a1cdf666a6309c1ef51ad4f72f99d3392bcd7b7139577a1f2", - "sha256:b33ebcd0222c1d77e61dbcd04a9fd139359bded86803063d3d2d197b796c63ce", - "sha256:bba52d72e16a554d1894a0cc74041da50eea99a8483e591a9edf1025a66843ab", - "sha256:bed7986547ce54d230fd8721aba6fd19459cdc6d315497b98686d0416efaff4e", - "sha256:c7f58a0e0e13fb44623b65b01052dae8e820ed9b8b654bb6296bc9c41f571b70", - "sha256:d58a4fa7910102500722defbde6e2816b0372a4fcc85c7e239323767c74f5cbc", - "sha256:f1ac2dc65105a53c1c2d72b1d3e98c2464a133b4067a51a3d2477b28449709a0" - ], - "version": "==2020.2.20" + "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b", + "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8", + "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3", + "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e", + "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683", + "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1", + "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142", + "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3", + "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468", + "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e", + "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3", + "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a", + "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f", + "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6", + "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156", + "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b", + "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db", + "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd", + "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a", + "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948", + "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89" + ], + "version": "==2020.4.4" }, "six": { "hashes": [ diff --git a/setup.py b/setup.py index a1e0a88a..0394d775 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,8 @@ "pyyaml", "streamlit", "gspread", - "oauth2client" + "oauth2client", + "python-i18n" ], classifiers=[ "Programming Language :: Python :: 3", diff --git a/src/penn_chime/locales/en.yml b/src/penn_chime/locales/en.yml new file mode 100644 index 00000000..d130607d --- /dev/null +++ b/src/penn_chime/locales/en.yml @@ -0,0 +1,94 @@ +en: + app-new-admissions-title: "New Admissions" + app-new-admissions-text: "Projected number of **daily** COVID-19 admissions." + app-admitted-patients-title: "Admitted Patients (Census)" + app-admitted-patients-text: "Projected **census** of COVID-19 patients, accounting for arrivals and discharges." + app-SIR-title: "Susceptible, Infected, and Recovered" + app-SIR-text: "The number of susceptible, infected, and recovered individuals in the hospital catchment region at any given moment" + charts-date: "Date" + charts-daily-admissions: "Daily admissions" + charts-census: "Census" + charts-count: "Count" + presentation-infected-population-warning: "(Warning: The number of estimated infections is greater than the total regional population. Please verify the values entered in the sidebar.)" + presentation-header: |+ + +
+ presentation-notice: |+ + **Notice**: *There is a high + degree of uncertainty about the details of COVID-19 infection, transmission, and the effectiveness of social distancing + measures. Long-term projections made using this simplified model of outbreak progression should be treated with extreme caution.* + presentation-developed-by: |+ + This tool was developed by [Predictive Healthcare](http://predictivehealthcare.pennmedicine.org/) at + Penn Medicine to assist hospitals and public health officials with hospital capacity planning. + Please read [How to Use CHIME]({docs_url}) to customize inputs for your region. + presentation-estimated-number-of-infection: |+ + The estimated number of currently infected individuals is **{total_infections:.0f}**. This is based on current inputs for + Hospitalizations (**{current_hosp}**), Hospitalization rate (**{hosp_rate:.0%}**), Regional population (**{S}**), + and Hospital market share (**{market_share:.0%}**). + + {infected_population_warning_str} + + An initial doubling time of **{doubling_time}** days and a recovery time of **{recovery_days}** days imply an $R_0$ of + **{r_naught:.2f}** and daily growth rate of **{daily_growth:.2f}%**. + + {mitigation_str} + presentation-mitigation-rt-less-then-1: |+ + **Mitigation**: A **{relative_contact_rate:.0%}** reduction in social contact after the onset of the + outbreak **halves the infections every {doubling_time_t:.1f}** days, implying an effective $R_t$ of **${r_t:.2f}$** + and daily growth rate of **{daily_growth_t:.2f}%**. + presentation-mitigation-rt-more-then-equal-1: |+ + **Mitigation**: A **{relative_contact_rate:.0%}** reduction in social contact after the onset of the + outbreak **reduces the doubling time to {doubling_time_t:.1f}** days, implying an effective $R_t$ of **${r_t:.2f}$** + and daily growth rate of **{daily_growth_t:.2f}%**. + presentation-current-hospitalized: "Currently hospitalized COVID-19 patients" + presentation-n-days: "Number of days to project" + presentation-doubling-time: "Doubling time in days (up to today)" + presentation-current-date: "Current date (default is today)" + presentation-date-first-hospitalized: "Date of first hospitalized case (enter this date to have CHIME estimate the initial doubling time)" + presentation-mitigation-date: "Date of social distancing measures effect (may be delayed from implementation)" + presentation-relative-contact-rate: "Social distancing (% reduction in social contact going forward)" + presentation-hospitalized-rate: "Hospitalization %(total infections)" + presentation-icu-rate: "ICU %(total infections)" + presentation-ventilated-rate: "Ventilated %(total infections)" + presentation-hospitalized-days: "Average hospital length of stay (in days)" + presentation-icu-days: "Average days in ICU" + presentation-ventilated-days: "Average days on ventilator" + presentation-market-share: "Hospital market share (%)" + presentation-population: "Regional population" + presentation-infectious-days: "Infectious days" + presentation-max-y-axis-set: "Set the Y-axis on graphs to a static value" + presentation-max-y-axis: "Y-axis static value" + presentation-hospital-parameters: "Hospital Parameters" + presentation-spread-and-contact-parameters: "Spread and Contact Parameters" + presentation-first-hospitalized-check: "I know the date of the first hospitalized case." + presentation-social-distancing-implemented: "Social distancing measures have been implemented" + presentation-severity-parameters: "Severity Parameters" + presentation-display-parameters: "Display Parameters" + presentation-subscribe: "Subscribe" + presentation-enter-email: "Enter Email" + presentation-enter-name: "Enter Name" + presentation-enter-affiliation: "Enter Affiliation" + presentation-submit: "Submit" + presentation-references-acknowledgements: "References & Acknowledgements" + presentation-references-acknowledgements-text: |+ + * AHA Webinar, Feb 26, James Lawler, MD, an associate professor University of Nebraska Medical Center, What Healthcare Leaders Need To Know: Preparing for the COVID-19 + * We would like to recognize the valuable assistance in consultation and review of model assumptions by Michael Z. Levy, PhD, Associate Professor of Epidemiology, Department of Biostatistics, Epidemiology and Informatics at the Perelman School of Medicine + * Finally we'd like to thank [Code for Philly](https://codeforphilly.org/) and the many members of the open-source community that [contributed](https://github.com/CodeForPhilly/chime/graphs/contributors) to this project. + presentation-copyright: "© 2020, The Trustees of the University of Pennsylvania" + presentation-download: |+ + Download {filename} + admits_hospitalized: "Hospitalized Admissions" + admits_icu: "ICU Admissions" + admits_ventilated: "Ventilated Admissions" + census_hospitalized: "Hospitalized Census" + census_icu: "ICU Census" + census_ventilated: "Ventilated Census" + susceptible: "Susceptible" + infected: "Infected" + recovered: "Recovered" + day: "Day" + date: "Date" \ No newline at end of file diff --git a/src/penn_chime/locales/ja.yml b/src/penn_chime/locales/ja.yml new file mode 100644 index 00000000..1e656695 --- /dev/null +++ b/src/penn_chime/locales/ja.yml @@ -0,0 +1,92 @@ +ja: + app-new-admissions-title: "新規入院患者数" + app-new-admissions-text: "**日毎の**COVID-19関連新規入院患者数予測" + app-admitted-patients-title: "累計入院患者数" + app-admitted-patients-text: "入院と退院を考慮したCOVID-19患者の予測**累計患者数**" + app-SIR-title: "感受性保持者、感染者と回復者" + app-SIR-text: "与えられた時間範囲での、病院の担当範囲領域における感受性保持者、感染者と回復者の人数" + charts-date: "日付" + charts-daily-admissions: "毎日の新規入院患者数" + charts-census: "累計患者数" + charts-count: "人数" + presentation-infected-population-warning: "(警告:推定感染者数が地域の人口を上回っています。サイドバーに入力された値を確認してください。)" + presentation-header: |+ + + + + presentation-notice: |+ + **重要**: COVID-19の感染、伝播、 + 社会的遠距離戦略の厳密な有効性については、まだ多くの不確実な点があります。 + この簡略化されたアウトブレイク進行モデルを用いた長期予測は、細心の注意を払って取り扱ってください。 + presentation-developed-by: |+ + このツールはペンシルバニア州立大学の医療部門であるPenn Medicineの、[予測医療チーム](http://predictivehealthcare.pennmedicine.org/)によって、 + 病院や公衆衛生担当者の収容能力把握を支援するために開発されました。 + 地域の状況に応じたデータにカスタマイズするには、「[CHIMEの使い方]({docs_url})」をご覧ください。 + presentation-estimated-number-of-infection: |+ + 現在の予測感染者人数は**{total_infections:.0f}**人です。 + この結果は入院患者数(**{current_hosp}**)、患者の入院割合(**{hosp_rate:.0%}**)、地域の人口(**{S}**)および病院の患者シェア(**{market_share:.0%}**)を元に計算されています。 + + {infected_population_warning_str} + + 初期倍加時間が**{doubling_time}**日で、回復までの時間が**{recovery_days}**日であれば、$R_0$は**{r_naught:.2f}**となり、 + 日々の増加率は**{daily_growth:.2f}%**となります。 + + {mitigation_str} + presentation-mitigation-rt-less-then-1: |+ + **緩和**: アウトブレイク発生後の社会的接触が**{relative_contact_rate:.0%}**減少すると、**感染の半減期は{doubling_time_t:.1f}**日となり、 + 実効$R_t$は**${r_t:.2f}$**、日々の増加率は**{daily_growth_t:.2f}%**となります。 + presentation-mitigation-rt-more-then-equal-1: |+ + **緩和**: アウトブレイク発生後の社会的接触が**{relative_contact_rate:.0%}**減少すると、**倍加時間は改善されて{doubling_time_t:.1f}**日となり、 + 実効$R_t$は**${r_t:.2f}$**、日々の増加率は**{daily_growth_t:.2f}%**となります。 + presentation-current-hospitalized: "現在入院しているCOVID-19患者数" + presentation-n-days: "予測する日数" + presentation-doubling-time: "今日までの倍加時間 (日数)" + presentation-current-date: "今の日付 (初期値は今日)" + presentation-date-first-hospitalized: "最初の入院患者が発生した日付 (CHIMEが初期倍加時間を推定するために使います)" + presentation-mitigation-date: "社会的距離戦略が効果を発揮し始めた日付 (おそらく実施日よりも遅れます)" + presentation-relative-contact-rate: "社会的距離戦略 (今後の社会的接触の減少率 %)" + presentation-hospitalized-rate: "入院患者率 %(総感染者数比)" + presentation-icu-rate: "集中治療患者率 %(総感染者数比)" + presentation-ventilated-rate: "人工呼吸患者率 %(総感染者数比)" + presentation-hospitalized-days: "平均入院日数" + presentation-icu-days: "平均集中治療日数" + presentation-ventilated-days: "平均人工呼吸治療日数" + presentation-market-share: "病院の患者シェア率 (%)" + presentation-population: "地域の人口" + presentation-infectious-days: "感染力を持つ期間 (日数)" + presentation-max-y-axis-set: "グラフのY軸最大値を固定値にします" + presentation-max-y-axis: "Y軸固定値" + presentation-hospital-parameters: "病院パラメータ" + presentation-spread-and-contact-parameters: "感染と接触のパラメータ" + presentation-first-hospitalized-check: "最初の入院事例の日付が既知の場合" + presentation-social-distancing-implemented: "社会的距離戦略が導入済みの場合" + presentation-severity-parameters: "深刻度パラメータ" + presentation-display-parameters: "表示パラメータ" + presentation-subscribe: "購読" + presentation-enter-email: "メールアドレス" + presentation-enter-name: "名前" + presentation-enter-affiliation: "所属" + presentation-submit: "送信" + presentation-references-acknowledgements: "参考資料と謝辞" + presentation-references-acknowledgements-text: |+ + * アメリカ病院協会(AHA)ウェビナー、2月26日、James Lawler医師 (ネブラスカ大学医療センター准教授)、「COVID-19への準備として、医療リーダーが知っておくべきこと」 + * ペレルマン医科大学の生物統計学・疫学・疫学・情報学部門の疫学准教授であるMichael Z.Levy博士による、モデル仮説の検討とレビューにおける貴重な支援をここに特筆いたします。 + * 最後に、[Code for Philly](https://codeforphilly.org/)と、このプロジェクトに[貢献](https://github.com/CodeForPhilly/chime/graphs/contributors)してくれたオープンソースコミュニティの多くのメンバーに感謝いたします。 + presentation-copyright: "© 2020, The Trustees of the University of Pennsylvania" + presentation-download: |+ + {filename} をダウンロード + admits_hospitalized: "新規入院患者数" + admits_icu: "新規集中治療患者数" + admits_ventilated: "新規人工呼吸患者数" + census_hospitalized: "累計入院患者数" + census_icu: "累計集中治療患者数" + census_ventilated: "累計人工呼吸患者数" + susceptible: "感受性保持者数" + infected: "感染者数" + recovered: "回復者数" + day: "日数" + date: "日付" \ No newline at end of file diff --git a/src/penn_chime/model/parameters.py b/src/penn_chime/model/parameters.py index 4bc59f0c..cb56e6b0 100644 --- a/src/penn_chime/model/parameters.py +++ b/src/penn_chime/model/parameters.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +import i18n from argparse import ArgumentParser from collections import namedtuple @@ -424,14 +425,17 @@ def __init__(self, **kwargs): Date(key='mitigation_date', value=self.mitigation_date) self.labels = { - "hospitalized": "Hospitalized", - "icu": "ICU", - "ventilated": "Ventilated", - "day": "Day", - "date": "Date", - "susceptible": "Susceptible", - "infected": "Infected", - "recovered": "Recovered", + "admits_hospitalized": i18n.t("admits_hospitalized"), + "admits_icu": i18n.t("admits_icu"), + "admits_ventilated": i18n.t("admits_ventilated"), + "census_hospitalized": i18n.t("census_hospitalized"), + "census_icu": i18n.t("census_icu"), + "census_ventilated": i18n.t("census_ventilated"), + "day": i18n.t("day"), + "date": i18n.t("date"), + "susceptible" :i18n.t("susceptible"), + "infected": i18n.t("infected"), + "recovered": i18n.t("recovered") } self.dispositions = { diff --git a/src/penn_chime/view/charts.py b/src/penn_chime/view/charts.py index e51db833..394f226f 100644 --- a/src/penn_chime/view/charts.py +++ b/src/penn_chime/view/charts.py @@ -2,6 +2,7 @@ from altair import Chart import pandas as pd +import i18n import numpy as np from ..constants import DATE_FORMAT @@ -15,15 +16,15 @@ def build_admits_chart( if max_y_axis is not None: y_scale.domain = (0, max_y_axis) - x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=(DATE_FORMAT))) - y = dict(shorthand="value:Q", title="Daily admissions", scale=y_scale) + x = dict(shorthand="date:T", title=i18n.t("charts-date"), axis=alt.Axis(format=(DATE_FORMAT))) + y = dict(shorthand="value:Q", title=i18n.t("charts-daily-admissions"), scale=y_scale) color = "key:N" tooltip = ["date:T", alt.Tooltip("value:Q", format=".0f", title="Admit"), "key:N"] # TODO fix the fold to allow any number of dispositions points = ( alt.Chart() - .transform_fold(fold=["admits_hospitalized", "admits_icu", "admits_ventilated"]) + .transform_fold(fold=[i18n.t("admits_hospitalized"), i18n.t("admits_icu"), i18n.t("admits_ventilated")]) .encode(x=alt.X(**x), y=alt.Y(**y), color=color, tooltip=tooltip) .mark_line(point=True) .encode( @@ -39,8 +40,13 @@ def build_admits_chart( .transform_filter(alt.datum.day == 0) .mark_rule(color="black", opacity=0.35, size=2) ) + admits_floor_df_renamed = admits_floor_df.rename({ + "admits_hospitalized": i18n.t("admits_hospitalized"), + "admits_icu": i18n.t("admits_icu"), + "admits_ventilated": i18n.t("admits_ventilated") + }, axis=1) return ( - alt.layer(points, bar, data=admits_floor_df) + alt.layer(points, bar, data=admits_floor_df_renamed) .configure_legend(orient="bottom") .interactive() ) @@ -54,15 +60,15 @@ def build_census_chart( if max_y_axis: y_scale.domain = (0, max_y_axis) - x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=(DATE_FORMAT))) - y = dict(shorthand="value:Q", title="Census", scale=y_scale) + x = dict(shorthand="date:T", title=i18n.t("charts-date"), axis=alt.Axis(format=(DATE_FORMAT))) + y = dict(shorthand="value:Q", title=i18n.t("charts-census"), scale=y_scale) color = "key:N" tooltip = ["date:T", alt.Tooltip("value:Q", format=".0f", title="Census"), "key:N"] # TODO fix the fold to allow any number of dispositions points = ( alt.Chart() - .transform_fold(fold=["census_hospitalized", "census_icu", "census_ventilated"]) + .transform_fold(fold=[i18n.t("census_hospitalized"), i18n.t("census_icu"), i18n.t("census_ventilated")]) .encode(x=alt.X(**x), y=alt.Y(**y), color=color, tooltip=tooltip) .mark_line(point=True) .encode( @@ -78,8 +84,13 @@ def build_census_chart( .transform_filter(alt.datum.day == 0) .mark_rule(color="black", opacity=0.35, size=2) ) + census_floor_df_renamed = census_floor_df.rename({ + "census_hospitalized": i18n.t("census_hospitalized"), + "census_icu": i18n.t("census_icu"), + "census_ventilated": i18n.t("census_ventilated") + }, axis=1) return ( - alt.layer(points, bar, data=census_floor_df) + alt.layer(points, bar, data=census_floor_df_renamed) .configure_legend(orient="bottom") .interactive() ) @@ -93,15 +104,15 @@ def build_sim_sir_w_date_chart( if max_y_axis is not None: y_scale.domain = (0, max_y_axis) - x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=(DATE_FORMAT))) - y = dict(shorthand="value:Q", title="Count", scale=y_scale) + x = dict(shorthand="date:T", title=i18n.t("charts-date"), axis=alt.Axis(format=(DATE_FORMAT))) + y = dict(shorthand="value:Q", title=i18n.t("charts-count"), scale=y_scale) color = "key:N" tooltip = ["key:N", "value:Q"] # TODO fix the fold to allow any number of dispositions points = ( alt.Chart() - .transform_fold(fold=["susceptible", "infected", "recovered"]) + .transform_fold(fold=[i18n.t("susceptible"), i18n.t("infected"), i18n.t("recovered")]) .encode(x=alt.X(**x), y=alt.Y(**y), color=color, tooltip=tooltip) .mark_line() .encode( @@ -117,17 +128,21 @@ def build_sim_sir_w_date_chart( .transform_filter(alt.datum.day == 0) .mark_rule(color="black", opacity=0.35, size=2) ) + sim_sir_w_date_floor_df_renamed = sim_sir_w_date_floor_df.rename({ + "susceptible": i18n.t("susceptible"), + "infected": i18n.t("infected"), + "recovered": i18n.t("recovered") + }, axis=1) return ( - alt.layer(points, bar, data=sim_sir_w_date_floor_df) + alt.layer(points, bar, data=sim_sir_w_date_floor_df_renamed) .configure_legend(orient="bottom") .interactive() ) - def build_table( *, df: pd.DataFrame, labels: Dict[str, str], modulo: int = 1 ) -> pd.DataFrame: table_df = df[np.mod(df.day, modulo) == 0].copy() table_df.date = table_df.date.dt.strftime(DATE_FORMAT) - table_df.rename(labels) - return table_df + table_df_renamed = table_df.rename(labels, axis=1) + return table_df_renamed diff --git a/src/penn_chime/view/st_app.py b/src/penn_chime/view/st_app.py index 3416285c..bd3dd94d 100644 --- a/src/penn_chime/view/st_app.py +++ b/src/penn_chime/view/st_app.py @@ -4,6 +4,12 @@ import altair as alt # type: ignore import streamlit as st # type: ignore +import i18n # type: ignore + +i18n.set('filename_format', '{locale}.{format}') +i18n.set('locale', 'en') +i18n.set('fallback', 'en') +i18n.load_path.append(os.path.dirname(__file__) + '/../locales') from ..model.parameters import Parameters from ..model.sir import Sir @@ -34,32 +40,35 @@ def main(): display_header(st, m, p) - st.subheader("New Admissions") - st.markdown("Projected number of **daily** COVID-19 admissions.") + st.subheader(i18n.t("app-new-admissions-title")) + st.markdown(i18n.t("app-new-admissions-text")) admits_chart = build_admits_chart(alt=alt, admits_floor_df=m.admits_floor_df, max_y_axis=p.max_y_axis) st.altair_chart(admits_chart, use_container_width=True) display_download_link( st, + p, filename=f"{p.current_date}_projected_admits.csv", df=m.admits_df, ) - st.subheader("Admitted Patients (Census)") - st.markdown("Projected **census** of COVID-19 patients, accounting for arrivals and discharges.") + st.subheader(i18n.t("app-admitted-patients-title")) + st.markdown(i18n.t("app-admitted-patients-text")) census_chart = build_census_chart(alt=alt, census_floor_df=m.census_floor_df, max_y_axis=p.max_y_axis) st.altair_chart(census_chart, use_container_width=True) display_download_link( st, + p, filename=f"{p.current_date}_projected_census.csv", df=m.census_df, ) - st.subheader("Susceptible, Infected, and Recovered") - st.markdown("The number of susceptible, infected, and recovered individuals in the hospital catchment region at any given moment") + st.subheader(i18n.t("app-SIR-title")) + st.markdown(i18n.t("app-SIR-text")) sim_sir_w_date_chart = build_sim_sir_w_date_chart(alt=alt, sim_sir_w_date_floor_df=m.sim_sir_w_date_floor_df) st.altair_chart(sim_sir_w_date_chart, use_container_width=True) display_download_link( st, + p, filename=f"{p.current_date}_sim_sir_w_date.csv", df=m.sim_sir_w_date_df, ) diff --git a/src/penn_chime/view/st_display.py b/src/penn_chime/view/st_display.py index 46ec9942..64ff5fc6 100644 --- a/src/penn_chime/view/st_display.py +++ b/src/penn_chime/view/st_display.py @@ -4,6 +4,7 @@ import json import pandas as pd +import i18n from ..constants import ( CHANGE_DATE, @@ -32,48 +33,24 @@ def display_header(st, m, p): infected_population_warning_str = ( - """(Warning: The number of estimated infections is greater than the total regional population. Please verify the values entered in the sidebar.)""" + i18n.t("presentation-infected-population-warning") if m.infected > p.population else "" ) st.markdown( - """ - - - """, + i18n.t("presentation-header"), unsafe_allow_html=True, ) st.markdown( - """**Notice**: *There is a high -degree of uncertainty about the details of COVID-19 infection, transmission, and the effectiveness of social distancing -measures. Long-term projections made using this simplified model of outbreak progression should be treated with extreme caution.* - """ + i18n.t("presentation-notice") ) st.markdown( - """ -This tool was developed by [Predictive Healthcare](http://predictivehealthcare.pennmedicine.org/) at -Penn Medicine to assist hospitals and public health officials with hospital capacity planning. -Please read [How to Use CHIME]({docs_url}) to customize inputs for your region.""".format(docs_url=DOCS_URL)) + i18n.t("presentation-developed-by").format(docs_url=DOCS_URL)) st.markdown( - """The estimated number of currently infected individuals is **{total_infections:.0f}**. This is based on current inputs for - Hospitalizations (**{current_hosp}**), Hospitalization rate (**{hosp_rate:.0%}**), Regional population (**{S}**), - and Hospital market share (**{market_share:.0%}**). - -{infected_population_warning_str} - -An initial doubling time of **{doubling_time}** days and a recovery time of **{recovery_days}** days imply an $R_0$ of - **{r_naught:.2f}** and daily growth rate of **{daily_growth:.2f}%**. - -**Mitigation**: A **{relative_contact_rate:.0%}** reduction in social contact after the onset of the -outbreak **{impact_statement:s} {doubling_time_t:.1f}** days, implying an effective $R_t$ of **${r_t:.2f}$** -and daily growth rate of **{daily_growth_t:.2f}%**. -""".format( + i18n.t("presentation-estimated-number-of-infection") + .format( total_infections=m.infected, current_hosp=p.current_hospitalized, hosp_rate=p.hospitalized.rate, @@ -82,17 +59,18 @@ def display_header(st, m, p): recovery_days=p.infectious_days, r_naught=m.r_naught, doubling_time=p.doubling_time, - relative_contact_rate=p.relative_contact_rate, - r_t=m.r_t, - doubling_time_t=abs(m.doubling_time_t), - impact_statement=( - "halves the infections every" - if m.r_t < 1 - else "reduces the doubling time to" - ), daily_growth=m.daily_growth_rate * 100.0, - daily_growth_t=m.daily_growth_rate_t * 100.0, infected_population_warning_str=infected_population_warning_str, + mitigation_str=( + i18n.t("presentation-mitigation-rt-less-then-1") + if m.r_t < 1 + else i18n.t("presentation-mitigation-rt-more-then-equal-1") + ).format( + relative_contact_rate=p.relative_contact_rate, + doubling_time_t=abs(m.doubling_time_t), + r_t=m.r_t, + daily_growth_t=m.daily_growth_rate_t * 100.0, + ), ) ) @@ -184,7 +162,7 @@ def display_sidebar(st, d: Parameters) -> Parameters: current_hospitalized_input = NumberInput( st_obj, - "Currently hospitalized COVID-19 patients", + i18n.t("presentation-current-hospitalized"), min_value=0, value=d.current_hospitalized, step=1, @@ -192,7 +170,7 @@ def display_sidebar(st, d: Parameters) -> Parameters: ) n_days_input = NumberInput( st_obj, - "Number of days to project", + i18n.t("presentation-n-days"), min_value=30, value=d.n_days, step=1, @@ -200,26 +178,26 @@ def display_sidebar(st, d: Parameters) -> Parameters: ) doubling_time_input = NumberInput( st_obj, - "Doubling time in days (up to today)", + i18n.t("presentation-doubling-time"), min_value=0.5, value=d.doubling_time, step=0.25, format="%f", ) current_date_input = DateInput( - st_obj, "Current date (default is today)", value=d.current_date, + st_obj, i18n.t("presentation-current-date"), value=d.current_date, ) date_first_hospitalized_input = DateInput( - st_obj, "Date of first hospitalized case (enter this date to have CHIME estimate the initial doubling time)", + st_obj, i18n.t("presentation-date-first-hospitalized"), value=d.date_first_hospitalized, ) mitigation_date_input = DateInput( - st_obj, "Date of social distancing measures effect (may be delayed from implementation)", + st_obj, i18n.t("presentation-mitigation-date"), value=d.mitigation_date ) relative_contact_pct_input = PercentInput( st_obj, - "Social distancing (% reduction in social contact going forward)", + i18n.t("presentation-relative-contact-rate"), min_value=0.0, max_value=100.0, value=d.relative_contact_rate, @@ -227,24 +205,24 @@ def display_sidebar(st, d: Parameters) -> Parameters: ) hospitalized_pct_input = PercentInput( st_obj, - "Hospitalization %(total infections)", + i18n.t("presentation-hospitalized-rate"), value=d.hospitalized.rate, min_value=FLOAT_INPUT_MIN, max_value=100.0 ) icu_pct_input = PercentInput( st_obj, - "ICU %(total infections)", + i18n.t("presentation-icu-rate"), min_value=0.0, value=d.icu.rate, step=0.05 ) ventilated_pct_input = PercentInput( - st_obj, "Ventilated %(total infections)", value=d.ventilated.rate, + st_obj, i18n.t("presentation-ventilated-rate"), value=d.ventilated.rate, ) hospitalized_days_input = NumberInput( st_obj, - "Average hospital length of stay (in days)", + i18n.t("presentation-hospitalized-days"), min_value=1, value=d.hospitalized.days, step=1, @@ -252,7 +230,7 @@ def display_sidebar(st, d: Parameters) -> Parameters: ) icu_days_input = NumberInput( st_obj, - "Average days in ICU", + i18n.t("presentation-icu-days"), min_value=1, value=d.icu.days, step=1, @@ -260,7 +238,7 @@ def display_sidebar(st, d: Parameters) -> Parameters: ) ventilated_days_input = NumberInput( st_obj, - "Average days on ventilator", + i18n.t("presentation-ventilated-days"), min_value=1, value=d.ventilated.days, step=1, @@ -268,13 +246,13 @@ def display_sidebar(st, d: Parameters) -> Parameters: ) market_share_pct_input = PercentInput( st_obj, - "Hospital market share (%)", + i18n.t("presentation-market-share"), min_value=0.5, value=d.market_share, ) population_input = NumberInput( st_obj, - "Regional population", + i18n.t("presentation-population"), min_value=1, value=(d.population), step=1, @@ -282,17 +260,17 @@ def display_sidebar(st, d: Parameters) -> Parameters: ) infectious_days_input = NumberInput( st_obj, - "Infectious days", + i18n.t("presentation-infectious-days"), min_value=1, value=d.infectious_days, step=1, format="%i", ) max_y_axis_set_input = CheckboxInput( - st_obj, "Set the Y-axis on graphs to a static value" + st_obj, i18n.t("presentation-max-y-axis-set") ) max_y_axis_input = NumberInput( - st_obj, "Y-axis static value", value=500, format="%i", step=25 + st_obj, i18n.t("presentation-max-y-axis"), value=500, format="%i", step=25 ) # Build in desired order @@ -304,8 +282,9 @@ def display_sidebar(st, d: Parameters) -> Parameters: ) st.sidebar.markdown( - "### Hospital Parameters [ℹ]({docs_url}/what-is-chime/parameters#hospital-parameters)".format( - docs_url=DOCS_URL + "### {hospital_parameters} [ℹ]({docs_url}/what-is-chime/parameters#hospital-parameters)".format( + docs_url=DOCS_URL, + hospital_parameters=i18n.t("presentation-hospital-parameters") ) ) population = population_input() @@ -314,13 +293,14 @@ def display_sidebar(st, d: Parameters) -> Parameters: current_hospitalized = current_hospitalized_input() st.sidebar.markdown( - "### Spread and Contact Parameters [ℹ]({docs_url}/what-is-chime/parameters#spread-and-contact-parameters)".format( - docs_url=DOCS_URL + "### {spread_and_contact_parameters} [ℹ]({docs_url}/what-is-chime/parameters#spread-and-contact-parameters)".format( + docs_url=DOCS_URL, + spread_and_contact_parameters=i18n.t("presentation-spread-and-contact-parameters") ) ) if st.sidebar.checkbox( - "I know the date of the first hospitalized case." + i18n.t("presentation-first-hospitalized-check") ): date_first_hospitalized = date_first_hospitalized_input() doubling_time = None @@ -329,7 +309,7 @@ def display_sidebar(st, d: Parameters) -> Parameters: date_first_hospitalized = None if st.sidebar.checkbox( - "Social distancing measures have been implemented", + i18n.t("presentation-social-distancing-implemented"), value=(d.relative_contact_rate > EPSILON) ): mitigation_date = mitigation_date_input() @@ -339,8 +319,9 @@ def display_sidebar(st, d: Parameters) -> Parameters: relative_contact_rate = EPSILON st.sidebar.markdown( - "### Severity Parameters [ℹ]({docs_url}/what-is-chime/parameters#severity-parameters)".format( - docs_url=DOCS_URL + "### {severity_parameters} [ℹ]({docs_url}/what-is-chime/parameters#severity-parameters)".format( + docs_url=DOCS_URL, + severity_parameters=i18n.t("presentation-severity-parameters") ) ) hospitalized_rate = hospitalized_pct_input() @@ -352,8 +333,9 @@ def display_sidebar(st, d: Parameters) -> Parameters: ventilated_days = ventilated_days_input() st.sidebar.markdown( - "### Display Parameters [ℹ]({docs_url}/what-is-chime/parameters#display-parameters)".format( - docs_url=DOCS_URL + "### {display_parameters} [ℹ]({docs_url}/what-is-chime/parameters#display-parameters)".format( + docs_url=DOCS_URL, + display_parameters=i18n.t("presentation-display-parameters") ) ) n_days = n_days_input() @@ -438,11 +420,11 @@ def readGoogleApiSecretsDict(): return secret def subscribe(st_obj): - st_obj.subheader ("Subscribe") - email = st_obj.text_input (label="Enter Email", value="", key="na_lower_1") - name = st_obj.text_input (label="Enter Name", value="", key="na_upper_1") - affiliation = st_obj.text_input (label="Enter Affiliation", value="", key="na_upper_2") - if st_obj.button (label="Submit", key="ta_submit_1"): + st_obj.subheader (i18n.t("presentation-subscribe")) + email = st_obj.text_input (label=i18n.t("presentation-enter-email"), value="", key="na_lower_1") + name = st_obj.text_input (label=i18n.t("presentation-enter-name"), value="", key="na_upper_1") + affiliation = st_obj.text_input (label=i18n.t("presentation-enter-affiliation"), value="", key="na_upper_2") + if st_obj.button (label=i18n.t("presentation-submit"), key="ta_submit_1"): row = [email, name, affiliation] send_subscription_to_google_sheet_secret_json(st_obj, row) @@ -459,22 +441,17 @@ def send_subscription_to_google_sheet_secret_dict(st_obj, row): spr.writeToSheet("CHIME Form Submissions", row) def display_footer(st): - st.subheader("References & Acknowledgements") + st.subheader(i18n.t("presentation-references-acknowledgements")) st.markdown( - """* AHA Webinar, Feb 26, James Lawler, MD, an associate professor University of Nebraska Medical Center, What Healthcare Leaders Need To Know: Preparing for the COVID-19 -* We would like to recognize the valuable assistance in consultation and review of model assumptions by Michael Z. Levy, PhD, Associate Professor of Epidemiology, Department of Biostatistics, Epidemiology and Informatics at the Perelman School of Medicine -* Finally we'd like to thank [Code for Philly](https://codeforphilly.org/) and the many members of the open-source community that [contributed](https://github.com/CodeForPhilly/chime/graphs/contributors) to this project. - """ + i18n.t("presentation-references-acknowledgements-text") ) - st.markdown("© 2020, The Trustees of the University of Pennsylvania") + st.markdown(i18n.t("presentation-copyright")) -def display_download_link(st, filename: str, df: pd.DataFrame): - csv = dataframe_to_base64(df) +def display_download_link(st, p, filename: str, df: pd.DataFrame): + csv = dataframe_to_base64(df.rename(p.labels, axis=1)) st.markdown( - """ - Download {filename} -""".format( + i18n.t("presentation-download").format( csv=csv, filename=filename ), unsafe_allow_html=True, diff --git a/tests/penn_chime/view/test_charts.py b/tests/penn_chime/view/test_charts.py index 01a433dd..ce96e882 100644 --- a/tests/penn_chime/view/test_charts.py +++ b/tests/penn_chime/view/test_charts.py @@ -1,5 +1,12 @@ import altair as alt import pytest +import os +import i18n + +i18n.set('filename_format', '{locale}.{format}') +i18n.set('locale', 'en') +i18n.set('fallback', 'en') +i18n.load_path.append(os.path.dirname(__file__) + '/../../../src/penn_chime/locales') from penn_chime.view.charts import ( build_admits_chart, @@ -8,11 +15,10 @@ DISPOSITION_KEYS = ("hospitalized", "icu", "ventilated") - def test_admits_chart(admits_floor_df): chart = build_admits_chart(alt=alt, admits_floor_df=admits_floor_df) assert isinstance(chart, (alt.Chart, alt.LayerChart)) - assert round(chart.data.iloc[40].admits_icu, 0) == 38 + assert round(chart.data.iloc[40][i18n.t("admits_icu")], 0) == 38 # test fx call with no params with pytest.raises(TypeError): @@ -22,8 +28,8 @@ def test_admits_chart(admits_floor_df): def test_census_chart(census_floor_df): chart = build_census_chart(alt=alt, census_floor_df=census_floor_df) assert isinstance(chart, (alt.Chart, alt.LayerChart)) - assert chart.data.iloc[1].census_hospitalized == 3 - assert chart.data.iloc[49].census_ventilated == 365 + assert chart.data.iloc[1][i18n.t("census_hospitalized")] == 3 + assert chart.data.iloc[49][i18n.t("census_ventilated")] == 365 # test fx call with no params with pytest.raises(TypeError): diff --git a/tests/penn_chime/view/test_st_display.py b/tests/penn_chime/view/test_st_display.py index aa24723f..85335e78 100644 --- a/tests/penn_chime/view/test_st_display.py +++ b/tests/penn_chime/view/test_st_display.py @@ -1,4 +1,11 @@ import pytest +import os +import i18n + +i18n.set('filename_format', '{locale}.{format}') +i18n.set('locale', 'en') +i18n.set('fallback', 'en') +i18n.load_path.append(os.path.dirname(__file__) + '/../../../src/penn_chime/locales') from penn_chime.view.st_display import display_header