~vpzom/hitide

2a92758f2a18f4b918d940979d4cffb6364c952a — Colin Reeder 1 year, 2 months ago ff37f13 + 6bdc3a6
Merge branch 'lang'
M Cargo.lock => Cargo.lock +189 -220
@@ 17,12 17,6 @@ dependencies = [

[[package]]
name = "autocfg"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"

[[package]]
name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"


@@ 34,6 28,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"

[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"

[[package]]
name = "bytes"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 63,15 63,6 @@ dependencies = [
]

[[package]]
name = "cloudabi"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
dependencies = [
 "bitflags",
]

[[package]]
name = "core-foundation"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 100,6 91,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"

[[package]]
name = "fluent"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3b6132d1377d8776409a337c6851d342aee4e85277c96ecd2755c4e0efde1d"
dependencies = [
 "fluent-bundle",
 "unic-langid",
]

[[package]]
name = "fluent-bundle"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a094d494ab2ed06077e9a95f4e47f446c376de95f6c93045dd88c499bfcd70"
dependencies = [
 "fluent-langneg",
 "fluent-syntax",
 "intl-memoizer",
 "intl_pluralrules",
 "rental",
 "smallvec",
 "unic-langid",
]

[[package]]
name = "fluent-langneg"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94"
dependencies = [
 "unic-langid",
]

[[package]]
name = "fluent-syntax"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac0f7e83d14cccbf26e165d8881dcac5891af0d85a88543c09dd72ebd31d91ba"

[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 121,12 152,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"

[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"

[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 192,6 217,15 @@ dependencies = [
]

[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
 "byteorder",
]

[[package]]
name = "getrandom"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 237,18 271,21 @@ dependencies = [
 "ammonia",
 "chrono",
 "fallible-iterator",
 "fluent",
 "fluent-langneg",
 "ginger",
 "http",
 "hyper",
 "hyper-tls",
 "lazy_static",
 "render",
 "serde",
 "serde_derive",
 "serde_json",
 "serde_urlencoded",
 "timeago",
 "tokio",
 "trout",
 "unic-langid",
 "urlencoding",
]



@@ 347,26 384,36 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe"
dependencies = [
 "autocfg 1.0.0",
 "autocfg",
]

[[package]]
name = "iovec"
version = "0.1.4"
name = "intl-memoizer"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
checksum = "8a0ed58ba6089d49f8a9a7d5e16fc9b9e2019cdf40ef270f3d465fa244d9630b"
dependencies = [
 "libc",
 "type-map",
 "unic-langid",
]

[[package]]
name = "isolang"
version = "1.0.0"
name = "intl_pluralrules"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "265ef164908329e47e753c769b14cbb27434abf0c41984dca201484022f09ce5"
checksum = "6c271cdb1f12a9feb3a017619c3ee681f971f270f6757341d6abe1f9f7a98bc3"
dependencies = [
 "phf 0.7.24",
 "phf_codegen 0.7.24",
 "tinystr",
 "unic-langid",
]

[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
dependencies = [
 "libc",
]

[[package]]


@@ 425,8 472,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae38d669396ca9b707bfc3db254bc382ddb94f57cc5c235f34623a669a01dab"
dependencies = [
 "log",
 "phf 0.8.0",
 "phf_codegen 0.8.0",
 "phf",
 "phf_codegen",
 "serde",
 "serde_derive",
 "serde_json",


@@ 531,7 578,7 @@ version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
dependencies = [
 "autocfg 1.0.0",
 "autocfg",
 "num-traits",
]



@@ 541,7 588,7 @@ version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
dependencies = [
 "autocfg 1.0.0",
 "autocfg",
]

[[package]]


@@ 570,7 617,7 @@ version = "0.9.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
dependencies = [
 "autocfg 1.0.0",
 "autocfg",
 "cc",
 "libc",
 "pkg-config",


@@ 585,30 632,11 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"

[[package]]
name = "phf"
version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18"
dependencies = [
 "phf_shared 0.7.24",
]

[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
 "phf_shared 0.8.0",
]

[[package]]
name = "phf_codegen"
version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e"
dependencies = [
 "phf_generator 0.7.24",
 "phf_shared 0.7.24",
 "phf_shared",
]

[[package]]


@@ 617,18 645,8 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
dependencies = [
 "phf_generator 0.8.0",
 "phf_shared 0.8.0",
]

[[package]]
name = "phf_generator"
version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662"
dependencies = [
 "phf_shared 0.7.24",
 "rand 0.6.5",
 "phf_generator",
 "phf_shared",
]

[[package]]


@@ 637,17 655,8 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
dependencies = [
 "phf_shared 0.8.0",
 "rand 0.7.3",
]

[[package]]
name = "phf_shared"
version = "0.7.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
dependencies = [
 "siphasher 0.2.3",
 "phf_shared",
 "rand",
]

[[package]]


@@ 656,7 665,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
 "siphasher 0.3.3",
 "siphasher",
]

[[package]]


@@ 736,6 745,12 @@ dependencies = [
]

[[package]]
name = "proc-macro-hack"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4"

[[package]]
name = "proc-macro2"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 755,45 770,16 @@ dependencies = [

[[package]]
name = "rand"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
dependencies = [
 "autocfg 0.1.7",
 "libc",
 "rand_chacha 0.1.1",
 "rand_core 0.4.2",
 "rand_hc 0.1.0",
 "rand_isaac",
 "rand_jitter",
 "rand_os",
 "rand_pcg 0.1.2",
 "rand_xorshift",
 "winapi 0.3.8",
]

[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
 "getrandom",
 "libc",
 "rand_chacha 0.2.2",
 "rand_core 0.5.1",
 "rand_hc 0.2.0",
 "rand_pcg 0.2.1",
]

[[package]]
name = "rand_chacha"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
dependencies = [
 "autocfg 0.1.7",
 "rand_core 0.3.1",
 "rand_chacha",
 "rand_core",
 "rand_hc",
 "rand_pcg",
]

[[package]]


@@ 803,26 789,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
 "ppv-lite86",
 "rand_core 0.5.1",
]

[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
 "rand_core 0.4.2",
 "rand_core",
]

[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"

[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"


@@ 832,64 803,11 @@ dependencies = [

[[package]]
name = "rand_hc"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
dependencies = [
 "rand_core 0.3.1",
]

[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
 "rand_core 0.5.1",
]

[[package]]
name = "rand_isaac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
dependencies = [
 "rand_core 0.3.1",
]

[[package]]
name = "rand_jitter"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
dependencies = [
 "libc",
 "rand_core 0.4.2",
 "winapi 0.3.8",
]

[[package]]
name = "rand_os"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
dependencies = [
 "cloudabi",
 "fuchsia-cprng",
 "libc",
 "rand_core 0.4.2",
 "rdrand",
 "winapi 0.3.8",
]

[[package]]
name = "rand_pcg"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
dependencies = [
 "autocfg 0.1.7",
 "rand_core 0.4.2",
 "rand_core",
]

[[package]]


@@ 898,25 816,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
 "rand_core 0.5.1",
]

[[package]]
name = "rand_xorshift"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
dependencies = [
 "rand_core 0.3.1",
]

[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
 "rand_core 0.3.1",
 "rand_core",
]

[[package]]


@@ 954,6 854,27 @@ dependencies = [
]

[[package]]
name = "rental"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8545debe98b2b139fb04cad8618b530e9b07c152d99a5de83c860b877d67847f"
dependencies = [
 "rental-impl",
 "stable_deref_trait",
]

[[package]]
name = "rental-impl"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "475e68978dc5b743f2f40d8e0a8fdc83f1c5e78cbf4b8fa5e74e73beebc340de"
dependencies = [
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1034,12 955,6 @@ dependencies = [

[[package]]
name = "siphasher"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"

[[package]]
name = "siphasher"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7"


@@ 1069,6 984,12 @@ dependencies = [
]

[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"

[[package]]
name = "string_cache"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 1076,7 997,7 @@ checksum = "2940c75beb4e3bf3a494cef919a747a2cb81e52571e212bfbd185074add7208a"
dependencies = [
 "lazy_static",
 "new_debug_unreachable",
 "phf_shared 0.8.0",
 "phf_shared",
 "precomputed-hash",
 "serde",
]


@@ 1087,8 1008,8 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
dependencies = [
 "phf_generator 0.8.0",
 "phf_shared 0.8.0",
 "phf_generator",
 "phf_shared",
 "proc-macro2",
 "quote",
]


@@ 1123,7 1044,7 @@ checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
dependencies = [
 "cfg-if",
 "libc",
 "rand 0.7.3",
 "rand",
 "redox_syscall",
 "remove_dir_all",
 "winapi 0.3.8",


@@ 1171,14 1092,10 @@ dependencies = [
]

[[package]]
name = "timeago"
version = "0.2.1"
name = "tinystr"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aff2f3f1ac92d664adfdea85496dceb8c044f66d62e7d953a059023385967cfc"
dependencies = [
 "chrono",
 "isolang",
]
checksum = "4bac79c4b51eda1b090b1edebfb667821bbb51f713855164dc7cec2cb8ac2ba3"

[[package]]
name = "tokio"


@@ 1256,6 1173,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382"

[[package]]
name = "type-map"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2741b1474c327d95c1f1e3b0a2c3977c8e128409c572a33af2914e7d636717"
dependencies = [
 "fxhash",
]

[[package]]
name = "unic-langid"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73328fcd730a030bdb19ddf23e192187a6b01cd98be6d3140622a89129459ce5"
dependencies = [
 "unic-langid-impl",
 "unic-langid-macros",
]

[[package]]
name = "unic-langid-impl"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a4a8eeaf0494862c1404c95ec2f4c33a2acff5076f64314b465e3ddae1b934d"
dependencies = [
 "tinystr",
]

[[package]]
name = "unic-langid-macros"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18f980d6d87e8805f2836d64b4138cc95aa7986fa63b1f51f67d5fbff64dd6e5"
dependencies = [
 "proc-macro-hack",
 "tinystr",
 "unic-langid-impl",
 "unic-langid-macros-impl",
]

[[package]]
name = "unic-langid-macros-impl"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29396ffd97e27574c3e01368b1a64267d3064969e4848e2e130ff668be9daa9f"
dependencies = [
 "proc-macro-hack",
 "quote",
 "syn",
 "unic-langid-impl",
]

[[package]]
name = "unicode-bidi"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +4 -1
@@ 23,4 23,7 @@ ammonia = "3.1.0"
urlencoding = "1.1.1"
http = "0.2.1"
chrono = "0.4.13"
timeago = "0.2.1"
fluent-langneg = "0.13.0"
fluent = "0.12.0"
lazy_static = "1.4.0"
unic-langid = { version = "0.9.0", features = ["macros"] }

A res/lang/en.ftl => res/lang/en.ftl +103 -0
@@ 0,0 1,103 @@
about = About
about_title = About this instance
about_what_is = What is lotide?
about_text1 = lotide is an attempt to build a federated forum. Users can create communities to share links and text posts and discuss them with other users, include those registered on other servers through
about_text2 = For more information or to view the source code, check out the
about_sourcehut = SourceHut page
about_versions = This instance is running hitide { $hitide_version } on { $backend_name } { $backend_version }.
add_by_remote_id = Add by ID:
all = All
all_title = The Whole Known Network
by = by
comment = Comment
comment_delete_title = Delete Comment
comment_delete_question = Delete this comment?
comment_submit = Post Comment
communities = Communities
community_create = Create Community
community_create_submit = Create
community_edit = Customize Community
community_edit_link = Customize
community_remote_note = This is a remote community, information on this page may be incomplete.
delete = delete
delete_yes = Yes, delete
description = Description
edit = Edit
fetch = Fetch
follow = Follow
follow_request_sent = Follow request sent!
follow_undo = Unfollow
home_follow_prompt1 = Why not
home_follow_prompt2 = follow some communities?
like = Like
like_undo = Unlike
local = Local
login = Login
login_signup_link = create a new account
lookup_nothing = Nothing found.
lookup_title = Lookup
name_prompt = Name:
no_cancel = No, cancel
nothing = Looks like there's nothing here.
nothing_yet = Looks like there's nothing here (yet!).
on = on
or_start = Or
password_prompt = Password:
post_delete_question = Delete this post?
post_delete_title = Delete Post
post_new = New Post
register = Register
remote = Remote
reply = reply
reply_submit = Reply
submit = Submit
submitted = Submitted
text_with_markdown = Text (markdown supported)
timeago_years =
    { $years } { $years ->
        [one] year
        *[other] years
    } ago
timeago_months =
    { $months } { $months ->
        [one] month
        *[other] months
    } ago
timeago_weeks =
    { $weeks } { $weeks ->
        [one] week
        *[other] weeks
    } ago
timeago_days =
    { $days } { $days ->
        [one] day
        *[other] days
    } ago
timeago_hours =
    { $hours } { $hours ->
        [one] hour
        *[other] hours
    } ago
timeago_minutes =
    { $minutes } { $minutes ->
        [one] minute
        *[other] minutes
    } ago
timeago_seconds =
    { $seconds } { $seconds ->
        [one] second
        *[other] seconds
    } ago
timeago_now = now
timeago_future = in the future
title = Title
to = to
url = URL
user_edit_description_prompt = Profile Description:
user_edit_not_you = You can only edit your own profile.
user_edit_submit = Save
user_edit_title = Edit Profile
user_remote_note = This is a remote user, information on this page may be incomplete.
username_prompt = Username:
view_at_source = View at Source
view_more_comments = View More Comments

A res/lang/eo.ftl => res/lang/eo.ftl +103 -0
@@ 0,0 1,103 @@
about = Pri
about_title = Pri ĉi tiu servilo
about_what_is = Kio estas lotide?
about_text1 = lotide estas provo konstrui federacian forumon. Uzantoj povas krei komunumojn por disdoni ligilojn kaj tekstpoŝtojn kaj diskuti ilin kun aliaj uzantoj, inkluzive de tiuj en aliaj serviloj per
about_text2 = Por pli da informo aŭ vidi la fontkodon, kontrolu la
about_sourcehut = SourceHut paĝon
about_versions = Ĉi tiu servilo uzas hitide { $hitide_version } kun { $backend_name } { $backend_version }.
add_by_remote_id = Aldoni per ID:
all = Ĉiuj
all_title = La Tuta Konata Reto
by = de
comment = Komento
comment_delete_title = Forigi Komenton
comment_delete_question = Ĉu vi volas forigi ĉi tiun komenton?
comment_submit = Afiŝi Komenton
communities = Komunumoj
community_create = Krei Komunumon
community_create_submit = Krei
community_edit = Agordi Komunumon
community_edit_link = Agordi
community_remote_note = Ĉi tiu estas fora komunumo, informo en ĉi tiu paĝo eble neplenas.
delete = forigi
delete_yes = Jes, forigi
description = Priskribo
edit = Redakti
fetch = Alporti
follow = Aboni
follow_request_sent = Abonado peto senditas!
follow_undo = Ne plu aboni
home_follow_prompt1 = Kial ne
home_follow_prompt2 = aboni iujn komunumojn?
like = Ŝati
like_undo = Ne plu ŝati
local = Loka
login = Ensaluti
login_signup_link = krei novan konton
lookup_nothing = Nenio troveblas.
lookup_title = Serĉi
name_prompt = Nomo:
no_cancel = Ne, nuligi
nothing = Ŝajnas, ke estas nenio ĉi tie.
nothing_yet = Ŝajnas, ke estas nenio ĉi tie (ĝis nun!).
on = sur
or_start = Aŭ
password_prompt = Pasvorto:
post_delete_question = Ĉu vi volas forigi ĉi tiun poŝton?
post_delete_title = Forigi Poŝton
post_new = Nova Poŝto
register = Registriĝi
remote = Fora
reply = respondi
reply_submit = Respondi
submit = Sendi
submitted = Afiŝita
text_with_markdown = Teksto (markdown estas permesita)
timeago_years =
    antaŭ { $years } { $years ->
        [one] jaro
        *[other] jaroj
    }
timeago_months =
    antaŭ { $months } { $months ->
        [one] monato
        *[other] monatoj
    }
timeago_weeks =
    antaŭ{ $weeks } { $weeks ->
        [one] semajno
        *[other] semajnoj
    }
timeago_days =
    antaŭ { $days } { $days ->
        [one] tago
        *[other] tagoj
    }
timeago_hours =
    antaŭ { $hours } { $hours ->
        [one] horo
        *[other] horoj
    }
timeago_minutes =
    antaŭ { $minutes } { $minutes ->
        [one] minuto
        *[other] minutoj
    }
timeago_seconds =
    antaŭ { $seconds } { $seconds ->
        [one] sekundo
        *[other] sekundoj
    }
timeago_now = nune
timeago_future = estontece
title = Titolo
to = al
url = URL
user_edit_description_prompt = Priskribo de Profilo
user_edit_not_you = Vi nur rajtas redakti vian propran profilon.
user_edit_submit = Konservi
user_edit_title = Redakti Profilon
user_remote_note = Ĉi tiu estas fora uzanto, informo en ĉi tiu paĝo eble neplenas.
username_prompt = Uzantnomo:
view_at_source = Vidi ĉe Fonto
view_more_comments = Vidi Pli da Komentoj

M src/components/mod.rs => src/components/mod.rs +36 -31
@@ 1,3 1,5 @@
pub mod timeago;

use std::borrow::{Borrow, Cow};
use std::collections::HashMap;



@@ 8,17 10,20 @@ use crate::resp_types::{
use crate::util::{abbreviate_link, author_is_me};
use crate::PageBaseData;

pub use timeago::TimeAgo;

#[render::component]
pub fn Comment<'comment, 'base_data>(
    comment: &'comment RespPostCommentInfo<'comment>,
    base_data: &'base_data PageBaseData,
pub fn Comment<'a>(
    comment: &'a RespPostCommentInfo<'a>,
    base_data: &'a PageBaseData,
    lang: &'a crate::Translator,
) {
    render::rsx! {
        <li>
            <small>
                <cite><UserLink user={comment.author.as_ref()} /></cite>
                {" "}
                <TimeAgo since={chrono::DateTime::parse_from_rfc3339(&comment.created).unwrap()} />
                <TimeAgo since={chrono::DateTime::parse_from_rfc3339(&comment.created).unwrap()} lang />
            </small>
            <Content src={comment} />
            <div class={"actionList"}>


@@ 30,18 35,18 @@ pub fn Comment<'comment, 'base_data>(
                                    if comment.your_vote.is_some() {
                                        render::rsx! {
                                            <form method={"POST"} action={format!("/comments/{}/unlike", comment.id)}>
                                                <button type={"submit"}>{"Unlike"}</button>
                                                <button type={"submit"}>{lang.tr("like_undo", None)}</button>
                                            </form>
                                        }
                                    } else {
                                        render::rsx! {
                                            <form method={"POST"} action={format!("/comments/{}/like", comment.id)}>
                                                <button type={"submit"}>{"Like"}</button>
                                                <button type={"submit"}>{lang.tr("like", None)}</button>
                                            </form>
                                        }
                                    }
                                }
                                <a href={format!("/comments/{}", comment.id)}>{"reply"}</a>
                                <a href={format!("/comments/{}", comment.id)}>{lang.tr("reply", None)}</a>
                            </>
                        })
                    } else {


@@ 51,7 56,7 @@ pub fn Comment<'comment, 'base_data>(
                {
                    if author_is_me(&comment.author, &base_data.login) {
                        Some(render::rsx! {
                            <a href={format!("/comments/{}/delete", comment.id)}>{"delete"}</a>
                            <a href={format!("/comments/{}/delete", comment.id)}>{lang.tr("delete", None)}</a>
                        })
                    } else {
                        None


@@ 66,7 71,7 @@ pub fn Comment<'comment, 'base_data>(
                                {
                                    replies.iter().map(|reply| {
                                        render::rsx! {
                                            <Comment comment={reply} base_data />
                                            <Comment comment={reply} base_data lang />
                                        }
                                    })
                                    .collect::<Vec<_>>()


@@ 80,7 85,7 @@ pub fn Comment<'comment, 'base_data>(
            {
                if comment.replies.is_none() && comment.has_replies {
                    Some(render::rsx! {
                        <ul><li><a href={format!("/comments/{}", comment.id)}>{"-> View More Comments"}</a></li></ul>
                        <ul><li><a href={format!("/comments/{}", comment.id)}>{"-> "}{lang.tr("view_more_comments", None)}</a></li></ul>
                    })
                } else {
                    None


@@ 174,6 179,7 @@ impl<'a, T: HavingContent + 'a> render::Render for Content<'a, T> {
#[render::component]
pub fn HTPage<'a, Children: render::Render>(
    base_data: &'a PageBaseData,
    lang: &'a crate::Translator,
    title: &'a str,
    children: Children,
) {


@@ 190,19 196,19 @@ pub fn HTPage<'a, Children: render::Render>(
                    <header class={"mainHeader"}>
                        <div class={"left actionList"}>
                            <a href={"/"} class={"siteName"}>{"lotide"}</a>
                            <a href={"/all"}>{"All"}</a>
                            <a href={"/communities"}>{"Communities"}</a>
                            <a href={"/about"}>{"About"}</a>
                            <a href={"/all"}>{lang.tr("all", None)}</a>
                            <a href={"/communities"}>{lang.tr("communities", None)}</a>
                            <a href={"/about"}>{lang.tr("about", None)}</a>
                        </div>
                        <div class={"right actionList"}>
                            {
                                match &base_data.login {
                                    Some(login) => Some(render::rsx! {
                                        <a href={format!("/users/{}", login.user.id)}>{"👤︎"}</a>
                                        <a href={format!("/users/{}", login.user.id)}>{Cow::Borrowed("👤︎")}</a>
                                    }),
                                    None => {
                                        Some(render::rsx! {
                                            <a href={"/login"}>{"Login"}</a>
                                            <a href={"/login"}>{lang.tr("login", None)}</a>
                                        })
                                    }
                                }


@@ 217,7 223,12 @@ pub fn HTPage<'a, Children: render::Render>(
}

#[render::component]
pub fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool, no_user: bool) {
pub fn PostItem<'a>(
    post: &'a RespPostListPost<'a>,
    in_community: bool,
    no_user: bool,
    lang: &'a crate::Translator,
) {
    render::rsx! {
        <li>
            <a href={format!("/posts/{}", post.as_ref().id)}>


@@ 236,14 247,14 @@ pub fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool,
                }
            }
            <br />
            {"Submitted"}
            {lang.tr("submitted", None)}
            {
                if no_user {
                    None
                } else {
                    Some(render::rsx! {
                        <>
                            {" by "}<UserLink user={post.author.as_ref()} />
                            {" "}{lang.tr("by", None)}{" "}<UserLink user={post.author.as_ref()} />
                        </>
                    })
                }


@@ 251,7 262,7 @@ pub fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool,
            {
                if !in_community {
                    Some(render::rsx! {
                        <>{" to "}<CommunityLink community={&post.community} /></>
                        <>{" "}{lang.tr("to", None)}{" "}<CommunityLink community={&post.community} /></>
                    })
                } else {
                    None


@@ 262,21 273,24 @@ pub fn PostItem<'post>(post: &'post RespPostListPost<'post>, in_community: bool,
}

pub struct ThingItem<'a> {
    pub lang: &'a crate::Translator,
    pub thing: &'a RespThingInfo<'a>,
}

impl<'a> render::Render for ThingItem<'a> {
    fn render_into<W: std::fmt::Write>(self, writer: &mut W) -> std::fmt::Result {
        let lang = self.lang;

        match self.thing {
            RespThingInfo::Post(post) => {
                (PostItem { post, in_community: false, no_user: true }).render_into(writer)
                (PostItem { post, in_community: false, no_user: true, lang: self.lang }).render_into(writer)
            },
            RespThingInfo::Comment(comment) => {
                (render::rsx! {
                    <li>
                        <small>
                            <a href={format!("/comments/{}", comment.id)}>{"Comment"}</a>
                            {" on "}<a href={format!("/posts/{}", comment.post.id)}>{comment.post.title.as_ref()}</a>{":"}
                            <a href={format!("/comments/{}", comment.id)}>{lang.tr("comment", None)}</a>
                            {" "}{lang.tr("on", None)}{" "}<a href={format!("/posts/{}", comment.post.id)}>{comment.post.title.as_ref()}</a>{":"}
                        </small>
                        <Content src={comment} />
                    </li>


@@ 399,12 413,3 @@ pub fn BoolSubmitButton<'a>(value: bool, do_text: &'a str, done_text: &'a str) {
        }
    }
}

#[render::component]
pub fn TimeAgo(since: chrono::DateTime<chrono::offset::FixedOffset>) {
    let since_str = since.to_rfc3339();
    let text = timeago::Formatter::new().convert_chrono(since, chrono::offset::Utc::now());
    render::rsx! {
        <span title={since_str}>{text}</span>
    }
}

A src/components/timeago.rs => src/components/timeago.rs +70 -0
@@ 0,0 1,70 @@
#[render::component]
pub fn TimeAgo<'a>(
    since: chrono::DateTime<chrono::offset::FixedOffset>,
    lang: &'a crate::Translator,
) {
    let since_str = since.to_rfc3339();

    let duration = chrono::offset::Utc::now().signed_duration_since(since);

    let (key, args) = {
        let weeks = duration.num_weeks();
        if weeks > 52 {
            let years = ((weeks as f32) / 52.18).floor() as u32;
            (
                "timeago_years",
                Some(fluent::fluent_args!["years" => years]),
            )
        } else if weeks > 5 {
            let months = (f32::from(weeks as i8) / 4.35).floor() as u8;
            (
                "timeago_months",
                Some(fluent::fluent_args!["months" => months]),
            )
        } else if weeks > 0 {
            (
                "timeago_weeks",
                Some(fluent::fluent_args!["weeks" => weeks]),
            )
        } else {
            let days = duration.num_days();
            if days > 0 {
                ("timeago_days", Some(fluent::fluent_args!["days" => days]))
            } else {
                let hours = duration.num_hours();
                if hours > 0 {
                    (
                        "timeago_hours",
                        Some(fluent::fluent_args!["hours" => hours]),
                    )
                } else {
                    let minutes = duration.num_minutes();
                    if minutes > 0 {
                        (
                            "timeago_minutes",
                            Some(fluent::fluent_args!["minutes" => minutes]),
                        )
                    } else {
                        let seconds = duration.num_seconds();

                        if seconds > 0 {
                            (
                                "timeago_seconds",
                                Some(fluent::fluent_args!["seconds" => seconds]),
                            )
                        } else if seconds < 0 {
                            ("timeago_future", None)
                        } else {
                            ("timeago_now", None)
                        }
                    }
                }
            }
        }
    };
    let text = lang.tr(key, args.as_ref()).into_owned();

    render::rsx! {
        <span title={since_str}>{text}</span>
    }
}

M src/main.rs => src/main.rs +91 -0
@@ 1,6 1,8 @@
#![allow(unused_braces)]

use crate::resp_types::RespLoginInfo;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use trout::hyper::RoutingFailureExtHyper;



@@ 54,6 56,95 @@ pub fn simple_response(
    res
}

lazy_static::lazy_static! {
    static ref LANG_MAP: HashMap<unic_langid::LanguageIdentifier, fluent::FluentResource> = {
        let mut result = HashMap::new();

        result.insert(unic_langid::langid!("en"), fluent::FluentResource::try_new(include_str!("../res/lang/en.ftl").to_owned()).expect("Failed to parse translation"));
        result.insert(unic_langid::langid!("eo"), fluent::FluentResource::try_new(include_str!("../res/lang/eo.ftl").to_owned()).expect("Failed to parse translation"));

        result
    };

    static ref LANGS: Vec<unic_langid::LanguageIdentifier> = {
        LANG_MAP.keys().cloned().collect()
    };
}

pub struct Translator {
    bundle: fluent::concurrent::FluentBundle<&'static fluent::FluentResource>,
}
impl Translator {
    pub fn tr<'a>(
        &'a self,
        key: &'static str,
        args: Option<&'a fluent::FluentArgs>,
    ) -> Cow<'a, str> {
        let pattern = self.bundle.get_message(key).and_then(|msg| msg.value);

        let pattern = match pattern {
            Some(pattern) => pattern,
            None => {
                eprintln!("Missing message in translation: {}", key);
                return Cow::Borrowed(key);
            }
        };

        let mut errors = Vec::with_capacity(0);
        let out = self.bundle.format_pattern(pattern, args, &mut errors);
        if !errors.is_empty() {
            eprintln!("Errors in translation: {:?}", errors);
        }

        out
    }
}
impl std::fmt::Debug for Translator {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "Translator")
    }
}

pub fn get_lang_for_headers(headers: &hyper::header::HeaderMap) -> Translator {
    let default = unic_langid::langid!("en");
    let languages = match headers
        .get(hyper::header::ACCEPT_LANGUAGE)
        .and_then(|x| x.to_str().ok())
    {
        Some(accept_language) => {
            let requested = fluent_langneg::accepted_languages::parse(accept_language);
            fluent_langneg::negotiate_languages(
                &requested,
                &LANGS,
                Some(&default),
                fluent_langneg::NegotiationStrategy::Filtering,
            )
        }
        None => vec![&default],
    };

    let mut bundle = fluent::concurrent::FluentBundle::new(languages.iter().map(|x| *x));
    for lang in languages {
        if let Err(errors) = bundle.add_resource(&LANG_MAP[lang]) {
            for err in errors {
                match err {
                    fluent::FluentError::Overriding { .. } => {}
                    _ => {
                        eprintln!("Failed to add language resource: {:?}", err);
                        break;
                    }
                }
            }
        }
    }

    Translator { bundle }
}

pub fn get_lang_for_req(req: &hyper::Request<hyper::Body>) -> Translator {
    get_lang_for_headers(req.headers())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let backend_host = std::env::var("BACKEND_HOST").expect("Missing BACKEND_HOST");

M src/routes/communities.rs => src/routes/communities.rs +85 -55
@@ 3,8 3,8 @@ use crate::resp_types::{
    RespCommunityInfoMaybeYour, RespMinimalCommunityInfo, RespPostListPost, RespYourFollow,
};
use crate::routes::{
    fetch_base_data, get_cookie_map, get_cookie_map_for_headers, get_cookie_map_for_req,
    html_response, res_to_error, with_auth, CookieMap,
    fetch_base_data, for_client, get_cookie_map_for_headers, get_cookie_map_for_req, html_response,
    res_to_error, CookieMap,
};
use serde_derive::Deserialize;
use std::collections::HashMap;


@@ 15,8 15,10 @@ async fn page_communities(
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_req(&req);
    let cookies = get_cookie_map_for_req(&req)?;
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    let api_res = res_to_error(
        ctx.http_client


@@ 30,14 32,16 @@ async fn page_communities(
    let api_res = hyper::body::to_bytes(api_res.into_body()).await?;
    let communities: Vec<RespMinimalCommunityInfo> = serde_json::from_slice(&api_res)?;

    let title = lang.tr("communities", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"Communities"}>
            <h1>{"Communities"}</h1>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <h1>{title.as_ref()}</h1>
            <div>
                <h2>{"Local"}</h2>
                <h2>{lang.tr("local", None)}</h2>
                {
                    if base_data.login.is_some() {
                        Some(render::rsx! { <a href={"/new_community"}>{"Create Community"}</a> })
                        Some(render::rsx! { <a href={"/new_community"}>{lang.tr("community_create", None)}</a> })
                    } else {
                        None
                    }


@@ 56,14 60,14 @@ async fn page_communities(
                </ul>
            </div>
            <div>
                <h2>{"Remote"}</h2>
                <h2>{lang.tr("remote", None)}</h2>
                <form method={"GET"} action={"/lookup"}>
                    <label>
                        {"Add by ID: "}
                        {lang.tr("add_by_remote_id", None)}{" "}
                        <input r#type={"text"} name={"query"} placeholder={"group@example.com"} />
                    </label>
                    {" "}
                    <button r#type={"submit"}>{"Fetch"}</button>
                    <button r#type={"submit"}>{lang.tr("fetch", None)}</button>
                </form>
                <ul>
                    {


@@ 89,15 93,17 @@ async fn page_community(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (community_id,) = params;

    let lang = crate::get_lang_for_req(&req);
    let cookies = get_cookie_map_for_req(&req)?;

    // TODO parallelize requests

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    let community_info_api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!(
                    "{}/api/unstable/communities/{}{}",
                    ctx.backend_host,


@@ 109,6 115,7 @@ async fn page_community(
                    },
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 121,12 128,13 @@ async fn page_community(

    let posts_api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!(
                    "{}/api/unstable/communities/{}/posts",
                    ctx.backend_host, community_id
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 141,7 149,7 @@ async fn page_community(
    let title = community_info.as_ref().name.as_ref();

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title>
        <HTPage base_data={&base_data} lang={&lang} title>
            <div class={"communitySidebar"}>
                <h2>{title}</h2>
                <div><em>{format!("@{}@{}", community_info.as_ref().name, community_info.as_ref().host)}</em></div>


@@ 151,8 159,9 @@ async fn page_community(
                    } else if let Some(remote_url) = &community_info.as_ref().remote_url {
                        Some(render::rsx! {
                            <div class={"infoBox"}>
                                {"This is a remote community, information on this page may be incomplete. "}
                                <a href={remote_url.as_ref()}>{"View at Source ↗"}</a>
                                {lang.tr("community_remote_note", None)}
                                {" "}
                                <a href={remote_url.as_ref()}>{lang.tr("view_at_source", None)}{" ↗"}</a>
                            </div>
                        })
                    } else {


@@ 166,21 175,21 @@ async fn page_community(
                                Some(RespYourFollow { accepted: true }) => {
                                    render::rsx! {
                                        <form method={"POST"} action={format!("/communities/{}/unfollow", community_id)}>
                                            <button type={"submit"}>{"Unfollow"}</button>
                                            <button type={"submit"}>{lang.tr("follow_undo", None)}</button>
                                        </form>
                                    }
                                },
                                Some(RespYourFollow { accepted: false }) => {
                                    render::rsx! {
                                        <form>
                                            <button disabled={""}>{"Follow request sent!"}</button>
                                            <button disabled={""}>{lang.tr("follow_request_sent", None)}</button>
                                        </form>
                                    }
                                },
                                None => {
                                    render::rsx! {
                                        <form method={"POST"} action={format!("/communities/{}/follow", community_id)}>
                                            <button type={"submit"}>{"Follow"}</button>
                                            <button type={"submit"}>{lang.tr("follow", None)}</button>
                                        </form>
                                    }
                                }


@@ 191,13 200,13 @@ async fn page_community(
                    }
                </p>
                <p>
                    <a href={&new_post_url}>{"New Post"}</a>
                    <a href={&new_post_url}>{lang.tr("post_new", None)}</a>
                </p>
                {
                    if community_info.you_are_moderator == Some(true) {
                        Some(render::rsx! {
                            <p>
                                <a href={format!("/communities/{}/edit", community_id)}>{"Customize"}</a>
                                <a href={format!("/communities/{}/edit", community_id)}>{lang.tr("community_edit_link", None)}</a>
                            </p>
                        })
                    } else {


@@ 208,14 217,14 @@ async fn page_community(
            </div>
            {
                if posts.is_empty() {
                    Some(render::rsx! { <p>{"Looks like there's nothing here."}</p> })
                    Some(render::rsx! { <p>{lang.tr("nothing", None)}</p> })
                } else {
                    None
                }
            }
            <ul>
                {posts.iter().map(|post| {
                    PostItem { post, in_community: true, no_user: false }
                    PostItem { post, in_community: true, no_user: false, lang: &lang }
                }).collect::<Vec<_>>()}
            </ul>
        </HTPage>


@@ 231,26 240,29 @@ async fn page_community_edit(

    let cookies = get_cookie_map_for_req(&req)?;

    page_community_edit_inner(community_id, &cookies, ctx, None, None).await
    page_community_edit_inner(community_id, req.headers(), &cookies, ctx, None, None).await
}

async fn page_community_edit_inner(
    community_id: i64,
    headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
    ctx: Arc<crate::RouteContext>,
    display_error: Option<String>,
    prev_values: Option<&HashMap<&str, serde_json::Value>>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;
    let lang = crate::get_lang_for_headers(headers);

    let community_info_api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!(
                    "{}/api/unstable/communities/{}",
                    ctx.backend_host, community_id,
                ))
                .body(Default::default())?,
                headers,
                &cookies,
            )?)
            .await?,


@@ 261,9 273,11 @@ async fn page_community_edit_inner(
    let community_info: RespCommunityInfoMaybeYour =
        { serde_json::from_slice(&community_info_api_res)? };

    let title = lang.tr("community_edit", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"Edit Community"}>
            <h1>{"Edit Community"}</h1>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <h1>{title.as_ref()}</h1>
            <h2>{community_info.as_ref().name.as_ref()}</h2>
            {
                display_error.map(|msg| {


@@ 274,11 288,11 @@ async fn page_community_edit_inner(
            }
            <form method={"POST"} action={format!("/communities/{}/edit/submit", community_id)}>
                <label>
                    {"Description:"}<br />
                    {lang.tr("description", None)}{":"}<br />
                    <MaybeFillTextArea values={&prev_values} name={"description"} default_value={Some(community_info.description.as_ref())} />
                </label>
                <div>
                    <button r#type={"submit"}>{"Submit"}</button>
                    <button r#type={"submit"}>{lang.tr("submit", None)}</button>
                </div>
            </form>
        </HTPage>


@@ 301,12 315,13 @@ async fn handler_communities_edit_submit(

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::patch(format!(
                    "{}/api/unstable/communities/{}",
                    ctx.backend_host, community_id
                ))
                .body(serde_json::to_vec(&body)?.into())?,
                &req_parts.headers,
                &cookies,
            )?)
            .await?,


@@ 315,7 330,15 @@ async fn handler_communities_edit_submit(

    match api_res {
        Err(crate::Error::RemoteError((_, message))) => {
            page_community_edit_inner(community_id, &cookies, ctx, Some(message), Some(&body)).await
            page_community_edit_inner(
                community_id,
                &req_parts.headers,
                &cookies,
                ctx,
                Some(message),
                Some(&body),
            )
            .await
        }
        Err(other) => Err(other),
        Ok(_) => Ok(hyper::Response::builder()


@@ 339,13 362,14 @@ async fn handler_community_follow(

    res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!(
                    "{}/api/unstable/communities/{}/follow",
                    ctx.backend_host, community_id
                ))
                .header(hyper::header::CONTENT_TYPE, "application/json")
                .body("{\"try_wait_for_accept\":true}".into())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 372,12 396,13 @@ async fn handler_community_unfollow(

    res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!(
                    "{}/api/unstable/communities/{}/unfollow",
                    ctx.backend_host, community_id
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 402,23 427,27 @@ async fn page_community_new_post(

    let cookies = get_cookie_map_for_req(&req)?;

    page_community_new_post_inner(community_id, &cookies, ctx, None, None).await
    page_community_new_post_inner(community_id, req.headers(), &cookies, ctx, None, None).await
}

async fn page_community_new_post_inner(
    community_id: i64,
    headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
    ctx: Arc<crate::RouteContext>,
    display_error: Option<String>,
    prev_values: Option<&HashMap<&str, serde_json::Value>>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;
    let lang = crate::get_lang_for_headers(headers);

    let submit_url = format!("/communities/{}/new_post/submit", community_id);

    let title = lang.tr("post_new", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"New Post"}>
            <h1>{"New Post"}</h1>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <h1>{title.as_ref()}</h1>
            {
                display_error.map(|msg| {
                    render::rsx! {


@@ 430,7 459,7 @@ async fn page_community_new_post_inner(
                <table>
                    <tr>
                        <td>
                            <label for={"input_title"}>{"Title:"}</label>
                            <label for={"input_title"}>{lang.tr("title", None)}{":"}</label>
                        </td>
                        <td>
                            <MaybeFillInput values={&prev_values} r#type={"text"} name={"title"} required={true} id={"input_title"} />


@@ 438,7 467,7 @@ async fn page_community_new_post_inner(
                    </tr>
                    <tr>
                        <td>
                            <label for={"input_url"}>{"URL:"}</label>
                            <label for={"input_url"}>{lang.tr("url", None)}{":"}</label>
                        </td>
                        <td>
                            <MaybeFillInput values={&prev_values} r#type={"text"} name={"href"} required={false} id={"input_url"} />


@@ 446,12 475,12 @@ async fn page_community_new_post_inner(
                    </tr>
                </table>
                <label>
                    {"Text (markdown supported):"}
                    {lang.tr("text_with_markdown", None)}{":"}
                    <br />
                    <MaybeFillTextArea values={&prev_values} name={"content_markdown"} default_value={None} />
                </label>
                <div>
                    <button r#type={"submit"}>{"Submit"}</button>
                    <button r#type={"submit"}>{lang.tr("submit", None)}</button>
                </div>
            </form>
        </HTPage>


@@ 465,17 494,10 @@ async fn handler_communities_new_post_submit(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (community_id,) = params;

    let cookies_string = req
        .headers()
        .get(hyper::header::COOKIE)
        .map(|x| x.to_str())
        .transpose()?
        .map(|x| x.to_owned());
    let cookies_string = cookies_string.as_deref();

    let cookies = get_cookie_map(cookies_string)?;
    let (req_parts, body) = req.into_parts();
    let cookies = get_cookie_map_for_headers(&req_parts.headers)?;

    let body = hyper::body::to_bytes(req.into_body()).await?;
    let body = hyper::body::to_bytes(body).await?;
    let mut body: HashMap<&str, serde_json::Value> = serde_urlencoded::from_bytes(&body)?;
    body.insert("community", community_id.into());
    if body.get("content_markdown").and_then(|x| x.as_str()) == Some("") {


@@ 487,9 509,10 @@ async fn handler_communities_new_post_submit(

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!("{}/api/unstable/posts", ctx.backend_host))
                    .body(serde_json::to_vec(&body)?.into())?,
                &req_parts.headers,
                &cookies,
            )?)
            .await?,


@@ 512,8 535,15 @@ async fn handler_communities_new_post_submit(
                .body("Successfully posted.".into())?)
        }
        Err(crate::Error::RemoteError((_, message))) => {
            page_community_new_post_inner(community_id, &cookies, ctx, Some(message), Some(&body))
                .await
            page_community_new_post_inner(
                community_id,
                &req_parts.headers,
                &cookies,
                ctx,
                Some(message),
                Some(&body),
            )
            .await
        }
        Err(other) => Err(other),
    }

M src/routes/mod.rs => src/routes/mod.rs +172 -85
@@ 54,8 54,9 @@ fn get_cookies_string(headers: &hyper::HeaderMap) -> Result<Option<&str>, crate:
        .transpose()?)
}

fn with_auth(
fn for_client(
    mut new_req: hyper::Request<hyper::Body>,
    src_headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
) -> Result<hyper::Request<hyper::Body>, hyper::header::InvalidHeaderValue> {
    let token = cookies.get("hitideToken").map(|c| c.value);


@@ 65,6 66,11 @@ fn with_auth(
            hyper::header::HeaderValue::from_str(&format!("Bearer {}", token))?,
        );
    }
    if let Some(value) = src_headers.get(hyper::header::ACCEPT_LANGUAGE) {
        new_req
            .headers_mut()
            .insert(hyper::header::ACCEPT_LANGUAGE, value.clone());
    }

    Ok(new_req)
}


@@ 72,13 78,15 @@ fn with_auth(
async fn fetch_base_data(
    backend_host: &str,
    http_client: &crate::HttpClient,
    headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
) -> Result<PageBaseData, crate::Error> {
    let login = {
        let api_res = http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!("{}/api/unstable/logins/~current", backend_host))
                    .body(Default::default())?,
                headers,
                &cookies,
            )?)
            .await?;


@@ 111,9 119,11 @@ async fn page_about(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    use std::convert::TryInto;

    let lang = crate::get_lang_for_req(&req);
    let cookies = get_cookie_map_for_req(&req)?;

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    let api_res = res_to_error(
        ctx.http_client


@@ 128,19 138,30 @@ async fn page_about(
    let api_res = hyper::body::to_bytes(api_res.into_body()).await?;
    let api_res: RespInstanceInfo = serde_json::from_slice(&api_res)?;

    let title = lang.tr("about_title", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"About"}>
            <h1>{"About this instance"}</h1>
            {"This instance is running hitide "}{env!("CARGO_PKG_VERSION")}{" on "}{api_res.software.name}{" "}{api_res.software.version}{"."}
            <h2>{"What is lotide?"}</h2>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <h1>{title.as_ref()}</h1>
            {
                lang.tr(
                    "about_versions",
                    Some(&fluent::fluent_args![
                        "hitide_version" => env!("CARGO_PKG_VERSION"),
                        "backend_name" => api_res.software.name,
                        "backend_version" => api_res.software.version
                    ])
                )
            }
            <h2>{lang.tr("about_what_is", None)}</h2>
            <p>
                {"lotide is an attempt to build a federated forum. "}
                {"Users can create communities to share links and text posts and discuss them with other users, including those registered on other servers through "}
                <a href={"https://activitypub.rocks"}>{"ActivityPub"}</a>{"."}
                {lang.tr("about_text1", None)}
                {" "}<a href={"https://activitypub.rocks"}>{"ActivityPub"}</a>{"."}
            </p>
            <p>
                {"For more information or to view the source code, check out the "}
                <a href={"https://sr.ht/~vpzom/lotide/"}>{"SourceHut page"}</a>{"."}
                {lang.tr("about_text2", None)}
                {" "}
                <a href={"https://sr.ht/~vpzom/lotide/"}>{lang.tr("about_sourcehut", None)}</a>{"."}
            </p>
        </HTPage>
    }))


@@ 155,21 176,23 @@ async fn page_comment(

    let cookies = get_cookie_map_for_req(&req)?;

    page_comment_inner(comment_id, &cookies, ctx, None, None).await
    page_comment_inner(comment_id, req.headers(), &cookies, ctx, None, None).await
}

async fn page_comment_inner(
    comment_id: i64,
    headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
    ctx: Arc<crate::RouteContext>,
    display_error: Option<String>,
    prev_values: Option<&serde_json::Value>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let lang = crate::get_lang_for_headers(headers);
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!(
                    "{}/api/unstable/comments/{}{}",
                    ctx.backend_host,


@@ 181,6 204,7 @@ async fn page_comment_inner(
                    },
                ))
                .body(Default::default())?,
                headers,
                &cookies,
            )?)
            .await?,


@@ 189,8 213,10 @@ async fn page_comment_inner(
    let api_res = hyper::body::to_bytes(api_res.into_body()).await?;
    let comment: RespPostCommentInfo<'_> = serde_json::from_slice(&api_res)?;

    let title = lang.tr("comment", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"Comment"}>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <p>
                <small><cite><UserLink user={comment.author.as_ref()} /></cite>{":"}</small>
                <Content src={&comment} />


@@ 204,13 230,13 @@ async fn page_comment_inner(
                                    if comment.your_vote.is_some() {
                                        render::rsx! {
                                            <form method={"POST"} action={format!("/comments/{}/unlike", comment.id)}>
                                                <button type={"submit"}>{"Unlike"}</button>
                                                <button type={"submit"}>{lang.tr("like_undo", None)}</button>
                                            </form>
                                        }
                                    } else {
                                        render::rsx! {
                                            <form method={"POST"} action={format!("/comments/{}/like", comment.id)}>
                                                <button type={"submit"}>{"Like"}</button>
                                                <button type={"submit"}>{lang.tr("like", None)}</button>
                                            </form>
                                        }
                                    }


@@ 224,7 250,7 @@ async fn page_comment_inner(
                {
                    if author_is_me(&comment.author, &base_data.login) {
                        Some(render::rsx! {
                            <a href={format!("/comments/{}/delete", comment.id)}>{"delete"}</a>
                            <a href={format!("/comments/{}/delete", comment.id)}>{lang.tr("delete", None)}</a>
                        })
                    } else {
                        None


@@ 245,7 271,7 @@ async fn page_comment_inner(
                            <div>
                                <MaybeFillTextArea values={&prev_values} name={"content_markdown"} default_value={None} />
                            </div>
                            <button r#type={"submit"}>{"Reply"}</button>
                            <button r#type={"submit"}>{lang.tr("reply_submit", None)}</button>
                        </form>
                    })
                } else {


@@ 256,7 282,7 @@ async fn page_comment_inner(
                {
                    comment.replies.as_ref().unwrap().iter().map(|reply| {
                        render::rsx! {
                            <Comment comment={reply} base_data={&base_data} />
                            <Comment comment={reply} base_data={&base_data} lang={&lang} />
                        }
                    }).collect::<Vec<_>>()
                }


@@ 284,7 310,8 @@ async fn page_comment_delete_inner(
    cookies: &CookieMap<'_>,
    display_error: Option<String>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let lang = crate::get_lang_for_headers(headers);
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;

    let referer = headers
        .get(hyper::header::REFERER)


@@ 292,12 319,13 @@ async fn page_comment_delete_inner(

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!(
                    "{}/api/unstable/comments/{}",
                    ctx.backend_host, comment_id
                ))
                .body(Default::default())?,
                headers,
                &cookies,
            )?)
            .await?,


@@ 306,15 334,17 @@ async fn page_comment_delete_inner(
    let api_res = hyper::body::to_bytes(api_res.into_body()).await?;
    let comment: RespPostCommentInfo<'_> = serde_json::from_slice(&api_res)?;

    let title = lang.tr("comment_delete_title", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"Delete Comment"}>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <p>
                <small><cite><UserLink user={comment.author.as_ref()} /></cite>{":"}</small>
                <br />
                <Content src={&comment} />
            </p>
            <div id={"delete"}>
                <h2>{"Delete this comment?"}</h2>
                <h2>{lang.tr("comment_delete_question", None)}</h2>
                {
                    display_error.map(|msg| {
                        render::rsx! {


@@ 332,9 362,9 @@ async fn page_comment_delete_inner(
                            None
                        }
                    }
                    <a href={format!("/comments/{}/", comment.id)}>{"No, cancel"}</a>
                    <a href={format!("/comments/{}/", comment.id)}>{lang.tr("no_cancel", None)}</a>
                    {" "}
                    <button r#type={"submit"}>{"Yes, delete"}</button>
                    <button r#type={"submit"}>{lang.tr("delete_yes", None)}</button>
                </form>
            </div>
        </HTPage>


@@ 357,12 387,13 @@ async fn handler_comment_delete_confirm(

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::delete(format!(
                    "{}/api/unstable/comments/{}",
                    ctx.backend_host, comment_id,
                ))
                .body("".into())?,
                &req_parts.headers,
                &cookies,
            )?)
            .await?,


@@ 405,12 436,13 @@ async fn handler_comment_like(

    res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!(
                    "{}/api/unstable/comments/{}/like",
                    ctx.backend_host, comment_id
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 447,12 479,13 @@ async fn handler_comment_unlike(

    res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!(
                    "{}/api/unstable/comments/{}/unlike",
                    ctx.backend_host, comment_id
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 489,12 522,13 @@ async fn handler_comment_submit_reply(

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!(
                    "{}/api/unstable/comments/{}/replies",
                    ctx.backend_host, comment_id
                ))
                .body(serde_json::to_vec(&body)?.into())?,
                &req_parts.headers,
                &cookies,
            )?)
            .await?,


@@ 507,7 541,15 @@ async fn handler_comment_submit_reply(
            .header(hyper::header::LOCATION, format!("/comments/{}", comment_id))
            .body("Successfully posted.".into())?),
        Err(crate::Error::RemoteError((status, message))) if status.is_client_error() => {
            page_comment_inner(comment_id, &cookies, ctx, Some(message), Some(&body)).await
            page_comment_inner(
                comment_id,
                &req_parts.headers,
                &cookies,
                ctx,
                Some(message),
                Some(&body),
            )
            .await
        }
        Err(other) => Err(other),
    }


@@ 527,12 569,21 @@ async fn page_login_inner(
    display_error: Option<String>,
    prev_values: Option<&serde_json::Value>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_headers(&req_parts.headers);
    let cookies = get_cookie_map_for_headers(&req_parts.headers)?;

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data = fetch_base_data(
        &ctx.backend_host,
        &ctx.http_client,
        &req_parts.headers,
        &cookies,
    )
    .await?;

    let title = lang.tr("login", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"Login"}>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            {
                display_error.map(|msg| {
                    render::rsx! {


@@ 543,22 594,22 @@ async fn page_login_inner(
            <form method={"POST"} action={"/login/submit"}>
                <table>
                    <tr>
                        <td><label for={"input_username"}>{"Username:"}</label></td>
                        <td><label for={"input_username"}>{lang.tr("username_prompt", None)}</label></td>
                        <td>
                            <MaybeFillInput values={&prev_values} r#type={"text"} name={"username"} required={true} id={"input_username"} />
                        </td>
                    </tr>
                    <tr>
                        <td><label for={"input_password"}>{"Password:"}</label></td>
                        <td><label for={"input_password"}>{lang.tr("password_prompt", None)}</label></td>
                        <td>
                            <MaybeFillInput values={&prev_values} r#type={"password"} name={"password"} required={true} id={"input_password"} />
                        </td>
                    </tr>
                </table>
                <button r#type={"submit"}>{"Login"}</button>
                <button r#type={"submit"}>{lang.tr("login", None)}</button>
            </form>
            <p>
                {"Or "}<a href={"/signup"}>{"create a new account"}</a>
                {lang.tr("or_start", None)}{" "}<a href={"/signup"}>{lang.tr("login_signup_link", None)}</a>
            </p>
        </HTPage>
    }))


@@ 632,8 683,10 @@ async fn page_lookup(
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_req(&req);
    let cookies = get_cookie_map_for_req(&req)?;
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    #[derive(Deserialize)]
    struct LookupQuery<'a> {


@@ 686,9 739,10 @@ async fn page_lookup(
            )
            .body("Redirecting…".into())?),
        api_res => {
            let title = lang.tr("lookup_title", None);
            Ok(html_response(render::html! {
                <HTPage base_data={&base_data} title={"Lookup"}>
                    <h1>{"Lookup"}</h1>
                <HTPage base_data={&base_data} lang={&lang} title={&title}>
                    <h1>{title.as_ref()}</h1>
                    <form method={"GET"} action={"/lookup"}>
                        <input r#type={"text"} name={"query"} value={query.as_deref().unwrap_or("")} />
                    </form>


@@ 697,7 751,7 @@ async fn page_lookup(
                            None => None,
                            Some(Ok(_)) => {
                                // non-empty case is handled above
                                Some(render::rsx! { <p>{Cow::Borrowed("Nothing found.")}</p> })
                                Some(render::rsx! { <p>{lang.tr("lookup_nothing", None)}</p> })
                            },
                            Some(Err(display_error)) => {
                                Some(render::rsx! {


@@ 719,20 773,24 @@ async fn page_new_community(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let cookies = get_cookie_map_for_req(&req)?;

    page_new_community_inner(ctx, &cookies, None, None).await
    page_new_community_inner(ctx, req.headers(), &cookies, None, None).await
}

async fn page_new_community_inner(
    ctx: Arc<crate::RouteContext>,
    headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
    display_error: Option<String>,
    prev_values: Option<&serde_json::Value>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let lang = crate::get_lang_for_headers(headers);
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;

    let title = lang.tr("community_create", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"New Community"}>
            <h1>{"New Community"}</h1>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <h1>{title.as_ref()}</h1>
            {
                display_error.map(|msg| {
                    render::rsx! {


@@ 743,11 801,11 @@ async fn page_new_community_inner(
            <form method={"POST"} action={"/new_community/submit"}>
                <div>
                    <label>
                        {"Name: "}<MaybeFillInput values={&prev_values} r#type={"text"} name={"name"} required={true} id={"input_name"} />
                        {lang.tr("name_prompt", None)}{" "}<MaybeFillInput values={&prev_values} r#type={"text"} name={"name"} required={true} id={"input_name"} />
                    </label>
                </div>
                <div>
                    <button r#type={"submit"}>{"Create"}</button>
                    <button r#type={"submit"}>{lang.tr("community_create_submit", None)}</button>
                </div>
            </form>
        </HTPage>


@@ 778,9 836,10 @@ async fn handler_new_community_submit(

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!("{}/api/unstable/communities", ctx.backend_host))
                    .body(serde_json::to_vec(&body)?.into())?,
                &req_parts.headers,
                &cookies,
            )?)
            .await?,


@@ 803,7 862,14 @@ async fn handler_new_community_submit(
                .body("Successfully created.".into())?)
        }
        Err(crate::Error::RemoteError((status, message))) if status.is_client_error() => {
            page_new_community_inner(ctx, &cookies, Some(message), Some(&body)).await
            page_new_community_inner(
                ctx,
                &req_parts.headers,
                &cookies,
                Some(message),
                Some(&body),
            )
            .await
        }
        Err(other) => Err(other),
    }


@@ 823,12 889,15 @@ async fn page_signup_inner(
    display_error: Option<String>,
    prev_values: Option<&serde_json::Value>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_headers(&headers);
    let cookies = get_cookie_map_for_headers(&headers)?;

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, headers, &cookies).await?;

    let title = lang.tr("register", None);

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"Register"}>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            {
                display_error.map(|msg| {
                    render::rsx! {


@@ 839,19 908,19 @@ async fn page_signup_inner(
            <form method={"POST"} action={"/signup/submit"}>
                <table>
                    <tr>
                        <td><label for={"input_username"}>{"Username:"}</label></td>
                        <td><label for={"input_username"}>{lang.tr("username_prompt", None)}</label></td>
                        <td>
                            <MaybeFillInput values={&prev_values} r#type={"text"} name={"username"} required={true} id={"input_username"} />
                        </td>
                    </tr>
                    <tr>
                        <td><label for={"input_password"}>{"Password:"}</label></td>
                        <td><label for={"input_password"}>{lang.tr("password_prompt", None)}</label></td>
                        <td>
                            <MaybeFillInput values={&prev_values} r#type={"password"} name={"password"} required={true} id={"input_password"} />
                        </td>
                    </tr>
                </table>
                <button r#type={"submit"}>{"Register"}</button>
                <button r#type={"submit"}>{lang.tr("register", None)}</button>
            </form>
        </HTPage>
    }))


@@ 913,9 982,11 @@ async fn page_user(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (user_id,) = params;

    let lang = crate::get_lang_for_req(&req);
    let cookies = get_cookie_map_for_req(&req)?;

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    let user = res_to_error(
        ctx.http_client


@@ 950,7 1021,7 @@ async fn page_user(
    let title = user.as_ref().username.as_ref();

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title>
        <HTPage base_data={&base_data} lang={&lang} title>
            <h1>{title}</h1>
            <div><em>{format!("@{}@{}", user.as_ref().username, user.as_ref().host)}</em></div>
            {


@@ 959,8 1030,8 @@ async fn page_user(
                } else if let Some(remote_url) = &user.as_ref().remote_url {
                    Some(render::rsx! {
                        <div class={"infoBox"}>
                            {"This is a remote user, information on this page may be incomplete. "}
                            <a href={remote_url.as_ref()}>{"View at Source ↗"}</a>
                            {lang.tr("user_remote_note", None)}
                            <a href={remote_url.as_ref()}>{lang.tr("view_at_source", None)}{" ↗"}</a>
                        </div>
                    })
                } else {


@@ 970,7 1041,7 @@ async fn page_user(
            {
                if let Some(login) = &base_data.login {
                    if login.user.id == user_id {
                        Some(render::rsx! { <a href={format!("/users/{}/edit", user_id)}>{"Edit"}</a> })
                        Some(render::rsx! { <a href={format!("/users/{}/edit", user_id)}>{lang.tr("edit", None)}</a> })
                    } else {
                        None
                    }


@@ 981,7 1052,7 @@ async fn page_user(
            <p>{user.description.as_ref()}</p>
            {
                if things.is_empty() {
                    Some(render::rsx! { <p>{"Looks like there's nothing here."}</p> })
                    Some(render::rsx! { <p>{lang.tr("nothing", None)}</p> })
                } else {
                    None
                }


@@ 989,7 1060,7 @@ async fn page_user(
            <ul>
                {
                    things.iter().map(|thing| {
                        ThingItem { thing }
                        ThingItem { thing, lang: &lang }
                    })
                    .collect::<Vec<_>>()
                }


@@ 1005,9 1076,13 @@ async fn page_user_edit(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (user_id,) = params;

    let lang = crate::get_lang_for_req(&req);
    let cookies = get_cookie_map_for_req(&req)?;

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    let title = lang.tr("user_edit_title", None);

    let is_me = match &base_data.login {
        None => false,


@@ 1016,9 1091,9 @@ async fn page_user_edit(

    if !is_me {
        let mut res = html_response(render::html! {
            <HTPage base_data={&base_data} title={"Edit Profile"}>
                <h1>{"Edit Profile"}</h1>
                <div class={"errorBox"}>{"You can only edit your own profile."}</div>
            <HTPage base_data={&base_data} lang={&lang} title={&title}>
                <h1>{title.as_ref()}</h1>
                <div class={"errorBox"}>{lang.tr("user_edit_not_you", None)}</div>
            </HTPage>
        });



@@ 1043,16 1118,16 @@ async fn page_user_edit(
    let user: RespUserInfo<'_> = serde_json::from_slice(&user)?;

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"Edit Profile"}>
            <h1>{"Edit Profile"}</h1>
        <HTPage base_data={&base_data} lang={&lang} title={&title}>
            <h1>{title.as_ref()}</h1>
            <form method={"POST"} action={format!("/users/{}/edit/submit", user_id)}>
                <div>
                    <label>
                        {"Profile Description:"}<br />
                        {lang.tr("user_edit_description_prompt", None)}<br />
                        <textarea name={"description"}>{user.description.as_ref()}</textarea>
                    </label>
                </div>
                <button type={"submit"}>{"Save"}</button>
                <button type={"submit"}>{lang.tr("user_edit_submit", None)}</button>
            </form>
        </HTPage>
    }))


@@ 1074,9 1149,10 @@ async fn handler_user_edit_submit(

    res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::patch(format!("{}/api/unstable/users/me", ctx.backend_host))
                    .body(serde_json::to_vec(&body)?.into())?,
                &req_parts.headers,
                &cookies,
            )?)
            .await?,


@@ 1094,22 1170,25 @@ async fn page_home(
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_req(&req);
    let cookies = get_cookie_map_for_req(&req)?;

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    if base_data.login.is_none() {
        return page_all_inner(&cookies, &base_data, ctx).await;
        return page_all_inner(req.headers(), &cookies, &base_data, ctx).await;
    }

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!(
                    "{}/api/unstable/users/me/following:posts",
                    ctx.backend_host
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 1120,13 1199,16 @@ async fn page_home(
    let api_res: Vec<RespPostListPost<'_>> = serde_json::from_slice(&api_res)?;

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"lotide"}>
        <HTPage base_data={&base_data} lang={&lang} title={"lotide"}>
            {
                if api_res.is_empty() {
                    Some(render::rsx! {
                        <p>
                            {"Looks like there's nothing here. Why not "}
                            <a href={"/communities"}>{"follow some communities"}</a>
                            {lang.tr("nothing", None)}
                            {" "}
                            {lang.tr("home_follow_prompt1", None)}
                            {" "}
                            <a href={"/communities"}>{lang.tr("home_follow_prompt2", None)}</a>
                            {"?"}
                        </p>
                    })


@@ 1136,7 1218,7 @@ async fn page_home(
            }
            <ul>
                {api_res.iter().map(|post| {
                    PostItem { post, in_community: false, no_user: false }
                    PostItem { post, in_community: false, no_user: false, lang: &lang }
                }).collect::<Vec<_>>()}
            </ul>
        </HTPage>


@@ 1150,21 1232,26 @@ async fn page_all(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let cookies = get_cookie_map_for_req(&req)?;

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    page_all_inner(&cookies, &base_data, ctx).await
    page_all_inner(req.headers(), &cookies, &base_data, ctx).await
}

async fn page_all_inner(
    headers: &hyper::header::HeaderMap,
    cookies: &CookieMap<'_>,
    base_data: &crate::PageBaseData,
    ctx: Arc<crate::RouteContext>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let lang = crate::get_lang_for_headers(headers);

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!("{}/api/unstable/posts", ctx.backend_host))
                    .body(Default::default())?,
                headers,
                &cookies,
            )?)
            .await?,


@@ 1175,13 1262,13 @@ async fn page_all_inner(
    let api_res: Vec<RespPostListPost<'_>> = serde_json::from_slice(&api_res)?;

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"lotide"}>
            <h1>{"The Whole Known Network"}</h1>
        <HTPage base_data={&base_data} lang={&lang} title={"lotide"}>
            <h1>{lang.tr("all_title", None)}</h1>
            {
                if api_res.is_empty() {
                    Some(render::rsx! {
                        <p>
                        {"Looks like there's nothing here (yet!)."}
                            {lang.tr("nothing_yet", None)}
                        </p>
                    })
                } else {


@@ 1190,7 1277,7 @@ async fn page_all_inner(
            }
            <ul>
                {api_res.iter().map(|post| {
                    PostItem { post, in_community: false, no_user: false }
                    PostItem { post, in_community: false, no_user: false, lang: &lang }
                }).collect::<Vec<_>>()}
            </ul>
        </HTPage>

M src/routes/posts.rs => src/routes/posts.rs +34 -23
@@ 1,6 1,6 @@
use super::{
    fetch_base_data, get_cookie_map_for_headers, get_cookie_map_for_req, html_response,
    res_to_error, with_auth,
    fetch_base_data, for_client, get_cookie_map_for_headers, get_cookie_map_for_req, html_response,
    res_to_error,
};
use crate::components::{Comment, CommunityLink, Content, HTPage, TimeAgo, UserLink};
use crate::resp_types::RespPostInfo;


@@ 14,13 14,15 @@ async fn page_post(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (post_id,) = params;

    let lang = crate::get_lang_for_req(&req);
    let cookies = get_cookie_map_for_req(&req)?;

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!(
                    "{}/api/unstable/posts/{}{}",
                    ctx.backend_host,


@@ 32,6 34,7 @@ async fn page_post(
                    },
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 44,7 47,7 @@ async fn page_post(
    let title = post.as_ref().as_ref().title.as_ref();

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={title}>
        <HTPage base_data={&base_data} lang={&lang} title={title}>
            <h1>{title}</h1>
            <p>
                <em>{post.score.to_string()}{" points"}</em>


@@ 54,13 57,13 @@ async fn page_post(
                        Some(if post.your_vote.is_some() {
                            render::rsx! {
                                <form method={"POST"} action={format!("/posts/{}/unlike", post_id)}>
                                    <button type={"submit"}>{"Unlike"}</button>
                                    <button type={"submit"}>{lang.tr("like_undo", None)}</button>
                                </form>
                            }
                        } else {
                            render::rsx! {
                                <form method={"POST"} action={format!("/posts/{}/like", post_id)}>
                                    <button type={"submit"}>{"Like"}</button>
                                    <button type={"submit"}>{lang.tr("like", None)}</button>
                                </form>
                            }
                        })


@@ 70,9 73,10 @@ async fn page_post(
                }
            </p>
            <p>
                {"Submitted "}<TimeAgo since={chrono::DateTime::parse_from_rfc3339(&post.as_ref().created)?} />
                {" by "}<UserLink user={post.as_ref().author.as_ref()} />
                {" to "}<CommunityLink community={&post.as_ref().community} />
                {lang.tr("submitted", None)}
                {" "}<TimeAgo since={chrono::DateTime::parse_from_rfc3339(&post.as_ref().created)?} lang={&lang} />
                {" "}{lang.tr("by", None)}{" "}<UserLink user={post.as_ref().author.as_ref()} />
                {" "}{lang.tr("to", None)}{" "}<CommunityLink community={&post.as_ref().community} />
            </p>
            {
                match &post.as_ref().href {


@@ 89,7 93,7 @@ async fn page_post(
                if author_is_me(&post.as_ref().author, &base_data.login) {
                    Some(render::rsx! {
                        <p>
                            <a href={format!("/posts/{}/delete", post_id)}>{"delete"}</a>
                            <a href={format!("/posts/{}/delete", post_id)}>{lang.tr("delete", None)}</a>
                        </p>
                    })
                } else {


@@ 105,7 109,7 @@ async fn page_post(
                                <div>
                                    <textarea name={"content_markdown"}>{()}</textarea>
                                </div>
                                <button r#type={"submit"}>{"Post Comment"}</button>
                                <button r#type={"submit"}>{lang.tr("comment_submit", None)}</button>
                            </form>
                        })
                    } else {


@@ 116,7 120,7 @@ async fn page_post(
                    {
                        post.comments.iter().map(|comment| {
                            render::rsx! {
                                <Comment comment={comment} base_data={&base_data} />
                                <Comment comment={comment} base_data={&base_data} lang={&lang} />
                            }
                        }).collect::<Vec<_>>()
                    }


@@ 133,18 137,21 @@ async fn page_post_delete(
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    let (post_id,) = params;

    let lang = crate::get_lang_for_req(&req);
    let cookies = get_cookie_map_for_req(&req)?;

    let base_data = fetch_base_data(&ctx.backend_host, &ctx.http_client, &cookies).await?;
    let base_data =
        fetch_base_data(&ctx.backend_host, &ctx.http_client, req.headers(), &cookies).await?;

    let api_res = res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::get(format!(
                    "{}/api/unstable/posts/{}",
                    ctx.backend_host, post_id
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 155,13 162,13 @@ async fn page_post_delete(
    let post: RespPostInfo = serde_json::from_slice(&api_res)?;

    Ok(html_response(render::html! {
        <HTPage base_data={&base_data} title={"Delete Post"}>
        <HTPage base_data={&base_data} lang={&lang} title={&lang.tr("post_delete_title", None)}>
            <h1>{post.as_ref().as_ref().title.as_ref()}</h1>
            <h2>{"Delete this post?"}</h2>
            <h2>{lang.tr("post_delete_question", None)}</h2>
            <form method={"POST"} action={format!("/posts/{}/delete/confirm", post.as_ref().as_ref().id)}>
                <a href={format!("/posts/{}/", post.as_ref().as_ref().id)}>{"No, cancel"}</a>
                <a href={format!("/posts/{}/", post.as_ref().as_ref().id)}>{lang.tr("no_cancel", None)}</a>
                {" "}
                <button r#type={"submit"}>{"Yes, delete"}</button>
                <button r#type={"submit"}>{lang.tr("delete_yes", None)}</button>
            </form>
        </HTPage>
    }))


@@ 178,12 185,13 @@ async fn handler_post_delete_confirm(

    res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::delete(format!(
                    "{}/api/unstable/posts/{}",
                    ctx.backend_host, post_id,
                ))
                .body("".into())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 207,12 215,13 @@ async fn handler_post_like(

    res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!(
                    "{}/api/unstable/posts/{}/like",
                    ctx.backend_host, post_id
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 236,12 245,13 @@ async fn handler_post_unlike(

    res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!(
                    "{}/api/unstable/posts/{}/unlike",
                    ctx.backend_host, post_id
                ))
                .body(Default::default())?,
                req.headers(),
                &cookies,
            )?)
            .await?,


@@ 270,12 280,13 @@ async fn handler_post_submit_reply(

    res_to_error(
        ctx.http_client
            .request(with_auth(
            .request(for_client(
                hyper::Request::post(format!(
                    "{}/api/unstable/posts/{}/replies",
                    ctx.backend_host, post_id
                ))
                .body(body.into())?,
                &req_parts.headers,
                &cookies,
            )?)
            .await?,