diff --git a/locales/en.mjs b/locales/en.mjs index 55ab066f..751a5622 100644 --- a/locales/en.mjs +++ b/locales/en.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"-K0AvT\":[\"Disconnect\"],\"-K9EZb\":[\"Add email…\"],\"-Ya-b9\":[\"Failed to save\"],\"-b7T3G\":[\"Updated\"],\"-iDCtG\":[\"Your settings\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Search dumps, users, playlists…\"],\"1QfxQT\":[\"Dismiss\"],\"1YKhJ3\":[\"1 new message\"],\"1njn7W\":[\"Light\"],\"1utXA6\":[\"Dumps\"],\"26iNma\":[\"Post comment\"],\"2DoBvq\":[\"Feeds\"],\"2Hlmdt\":[\"Write a reply…\"],\"2_DHgA\":[\"URL slug\"],\"2nrsEn\":[\"deleted message\"],\"2rwzb1\":[\"You haven't signed in to another gerbeur with this account yet.\"],\"2wxgft\":[\"Rename\"],\"2ygf_L\":[\"← Back\"],\"32TndD\":[\"Blocked\"],\"3KKSM4\":[\"private\"],\"3QuNUr\":[\"Several people are typing…\"],\"3SEwY1\":[\"Delete category \\\"\",[\"0\"],\"\\\"? This cannot be undone.\"],\"3qkggm\":[\"Fullscreen\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" followed your playlist <1>\",[\"1\"],\"\"],\"48e2R8\":[\"Edit email\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Dumped!\"],\"4GKuCs\":[\"Login failed\"],\"4HH9iB\":[\"Not following anyone yet.\"],\"4OjqAQ\":[\"Keep editing\"],\"4RtQ1k\":[\"Unfollow \",[\"targetUsername\"]],\"4c-qBx\":[\"Your password has been changed. You can now log in.\"],\"4yj9xV\":[\"Live updates are temporarily disconnected. Trying to reconnect…\"],\"5KHDCi\":[[\"0\"],\" and \",[\"1\"],\" are typing…\"],\"5NZ3tg\":[\"Turning off…\"],\"5TviPn\":[\"Cancel search\"],\"5WRdOd\":[\"<0>Top this week: <1>\",[\"0\"],\"\"],\"5cC8f2\":[\"Edited \",[\"0\"]],\"5oD9f_\":[\"Earlier\"],\"6BV5h6\":[\"This dump was deleted, or made private.\"],\"6C4kKS\":[\"Other instances\"],\"6Qly-0\":[\"a comment\"],\"6YE0P6\":[\"Editing \",[\"0\"]],\"6YtxFj\":[\"Name\"],\"6_dCYd\":[\"Overview\"],\"6gRgw8\":[\"Retry\"],\"7PHCIN\":[\"Cancel removal\"],\"7PzzBU\":[\"User\"],\"7d1a0d\":[\"Public\"],\"7noMWp\":[\"Nothing happened this week.\"],\"7sNhEz\":[\"Username\"],\"8-8abB\":[\"Disconnected — reconnecting…\"],\"8F1i42\":[\"Page not found\"],\"8JplJJ\":[\"Unsubscribed\"],\"8TgExA\":[\"<0>\",[\"0\"],\" created the playlist <1>\",[\"1\"],\"\"],\"8Ug9jB\":[\"Related\"],\"8ZsakT\":[\"Password\"],\"8rPWOT\":[\"Can't sign in there\"],\"92o1-U\":[\"Failed to delete\"],\"9BruTc\":[\"Why?\"],\"9eEYEq\":[\"This sign-in link is incomplete.\"],\"9l4qcT\":[\"Drop a replacement here\"],\"9uI_rE\":[\"Undo\"],\"A0y396\":[\"+ Invite someone\"],\"A1taO8\":[\"Search\"],\"AEfu-D\":[\"In collections\"],\"AHZflp\":[\"Chat\"],\"AQbgNR\":[\"Follow \",[\"targetUsername\"]],\"AS7WoE\":[\"Start fresh\"],\"ATGYL1\":[\"Email address\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"Ade-6d\":[\"Live updates unavailable.\"],\"AeXO77\":[\"Account\"],\"AhNP40\":[\"Seek\"],\"AnRu_j\":[\"Cancel reply\"],\"AwUsnG\":[\"Instances\"],\"B4LeYP\":[\"Delete failed (\",[\"0\"],\")\"],\"BKVDJg\":[\"created \",[\"ago\"]],\"BjitBo\":[\"Download \",[\"0\"]],\"BlpJ2G\":[\"Picked up where you left off.\"],\"CI50ct\":[\"Upvoted\"],\"CJkEfx\":[\"Block\"],\"CL-DkX\":[\"An instance's URL\"],\"C_qQE4\":[\"Already dumped by \",[\"0\"],\" \",[\"1\"]],\"Cf_gxi\":[\"Turn off the weekly digest?\"],\"Cj24wt\":[\"Registering…\"],\"ClFfRL\":[\"Playlist categories\"],\"CnqayG\":[\"Couldn't update. Try again.\"],\"Czya-u\":[\"Users of a trusted instance sign in here with their account there. Those of a blocked one can't, and users here can't sign in to it. Users of any other instance need an invite.\"],\"D8DRYV\":[\"Delete this message?\"],\"DI3NBO\":[\"Open on \",[\"0\"]],\"DPfwMq\":[\"Done\"],\"DdeHXH\":[\"Delete this dump? This cannot be undone.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" upvoted <1>\",[\"1\"],\"\"],\"E417RG\":[\"You won't get the weekly digest any more. You can turn it back on in your settings.\"],\"EBU6O6\":[\"Connection lost — this message may not have been sent\"],\"ECiS12\":[\"View dump →\"],\"ED8n0z\":[\"the chat\"],\"EarrCe\":[\"Search emoji\"],\"Evcm2Z\":[\"Close without posting? The attached file won't be kept — everything you typed will.\"],\"EwVrWF\":[\"The dump couldn't be removed. Try again.\"],\"ExR0Fr\":[\"URL is required.\"],\"F5Js1v\":[\"Unfollow playlist\"],\"FdTlEG\":[\"Track \",[\"0\"]],\"FgAxTj\":[\"Log out\"],\"Fxf4jq\":[\"Description (optional)\"],\"G8OFDZ\":[\"<0>This week: <1>\",[\"figures\"],\"\"],\"GCWCCo\":[\"Close player\"],\"GDvlUT\":[\"Role\"],\"GLukaH\":[\"Moderator\"],\"GNSsCc\":[\"Failed to create playlist\"],\"GQ38dl\":[\"Chat (\",[\"unreadChatCount\"],\" unread)\"],\"GY6uSc\":[\"Next →\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" characters: letters, numbers, or underscores\"],\"GptGxg\":[\"Change password\"],\"Gv5nA5\":[\"Unexpected response format\"],\"H4MGkI\":[\"Expand player\"],\"H4o4sk\":[\"No one yet.\"],\"H8pzW-\":[\"Failed to update avatar\"],\"HTLDA4\":[\"Loading more…\"],\"H_PnT8\":[\"Insert emoji\"],\"I-x669\":[\"Invitees\"],\"IF7RiR\":[\"Cover\"],\"IO9KL_\":[\"Next track\"],\"IZX7TO\":[\"Failed to generate invite\"],\"IagCbF\":[\"URL\"],\"IgrLD_\":[\"Pause\"],\"IhK90M\":[\"Letters, numbers and underscores only\"],\"ImOQa9\":[\"Reply\"],\"JJ-Bhk\":[\"Your email address\"],\"JLasL-\":[\"Couldn't unsubscribe\"],\"JRQitQ\":[\"Confirm new password\"],\"JXr41k\":[\"<0>\",[\"0\"],\" commented on <1>\",[\"1\"],\"\"],\"Jd58Fo\":[\"Hot\"],\"Jf0PuK\":[\"Go to login\"],\"JlFcis\":[\"Send\"],\"JrFTcr\":[\"Connecting…\"],\"K7tIrx\":[\"Category\"],\"KDGWg5\":[\"Remove from playlist\"],\"KDw4GX\":[\"Try again\"],\"KKZxBU\":[\"Uploading \",[\"0\"],\"%\"],\"K_F6pa\":[\"Saving…\"],\"KtYF-t\":[\"Sounds\"],\"L-rMC9\":[\"Reset to default\"],\"L7_rsx\":[\"Uploading image…\"],\"MHrjPM\":[\"Title\"],\"MKEPCY\":[\"Follow\"],\"MTb2Ot\":[\"No messages yet. Say hello!\"],\"MbkoRn\":[\"Or join with your account on another gerbeur\"],\"Mj6oCJ\":[[\"0\"],\" is typing…\"],\"MrIc3x\":[\"Or log in with your account on another gerbeur\"],\"N40H-G\":[\"All\"],\"N6j2JH\":[\"Edit \",[\"0\"]],\"NUrY9o\":[\"Categories\"],\"Nu4oKW\":[\"Description\"],\"O32mmL\":[\"Edit description\"],\"O3oNi5\":[\"Email\"],\"Oprv1v\":[\"Password (min. \",[\"0\"],\" characters)\"],\"P5MuiB\":[\"Add image\"],\"PJTy3N\":[[\"totalUnread\",\"plural\",{\"one\":[\"#\",\" new\"],\"other\":[\"#\",\" new\"]}]],\"PN3pBl\":[\"No reply from the server — this message may not have been sent\"],\"PUe8Yk\":[\"Collapse player\"],\"PiH3UR\":[\"Copied!\"],\"Pn2B7_\":[\"Current password\"],\"Pwqkdw\":[\"Loading…\"],\"Q4WUjR\":[\"Search emoji…\"],\"Q6n4F4\":[\"Refresh metadata\"],\"QGaQRo\":[\"Failed to update role\"],\"QKsaQr\":[\"or <0>browse files\"],\"QLtPBd\":[\"No dumps in this playlist yet.\"],\"QctOgc\":[\"Could not connect to the server. Check your connection and try again.\"],\"R7SsBE\":[\"Mute\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Delete this playlist? This cannot be undone.\"],\"RLe7Vk\":[\"Checking…\"],\"RTksSy\":[\"<0>\",[\"0\"],\" started following you\"],\"RaKjrM\":[\"Failed to save edit\"],\"RcUHRT\":[\"Followed\"],\"RfwZxd\":[\"Reset password\"],\"Rrp6-J\":[\"Log in to like\"],\"RvVJBH\":[\"Refreshing stream…\"],\"S1xV5B\":[\"At most \",[\"maxLength\"],\" characters.\"],\"SBTElJ\":[\"Searching…\"],\"SLaP1b\":[\"Not from here\"],\"SMaFdc\":[\"Unsubscribe\"],\"Sad2tK\":[\"Sending…\"],\"So7gCD\":[\"Paste a link…\"],\"StovX6\":[\"This page does not exist.\"],\"Sxm8rQ\":[\"Users\"],\"T8ejtZ\":[\"This comment couldn't be deleted. Try again.\"],\"T91vKp\":[\"Play\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" was added to <1>\",[\"1\"],\"\"],\"TAkYGZ\":[\"Your account lives on \",[\"home\"],\": sign in to other instances from there.\"],\"TAtbho\":[\"Loading messages…\"],\"TN382O\":[\"Invalid link\"],\"ToADL_\":[\"<0>\",[\"name\"],\" (\",[\"host\"],\") asks to sign you in with your account here, <1>@\",[\"username\"],\". It will receive your username, your email address and your avatar.\"],\"Tv9vbB\":[\"Follow playlist\"],\"Tz0i8g\":[\"Settings\"],\"U3pytU\":[\"Admin\"],\"UNMVei\":[\"Forgot password?\"],\"UOV0wi\":[\"Signing you in…\"],\"UOZith\":[\"Failed to post\"],\"URAieT\":[\"Log in to vote\"],\"V38P77\":[\"Only its poster or a moderator can edit this dump.\"],\"VCoEm-\":[\"Back to login\"],\"VKsaTi\":[\"Replying to\"],\"VLAd8f\":[\"Max \",[\"limit\"]],\"VLINRu\":[\"Your gerbeur's URL\"],\"V_e7nf\":[\"Set new password\"],\"VnNJbN\":[\"From playlists\"],\"VskHIx\":[\"Delete category\"],\"VyTYmS\":[\"Change avatar\"],\"W5A0Ly\":[\"An unexpected error occurred.\"],\"W9FRBT\":[\"Like\"],\"WAcnnR\":[\"The image couldn't be uploaded.\"],\"WE_r9K\":[\"Failed to create category\"],\"WMw63e\":[\"Couldn't reach that instance\"],\"WfffrI\":[\"slug\"],\"WhimMi\":[\"Reset failed\"],\"WpXcBJ\":[\"Nothing here yet.\"],\"XHqhoS\":[\"Couldn't load the chat.\"],\"XJVYo8\":[\"Why & where\"],\"XJy2oN\":[\"Logging in…\"],\"XWbTfu\":[\"last used \",[\"0\"]],\"Xan6QP\":[\"New dump\"],\"XgRtUf\":[\"Could not change password\"],\"XnL-Eu\":[\"No users match \\\"\",[\"q\"],\"\\\".\"],\"YK1Dhc\":[\"a post\"],\"YWnSuq\":[\"This unsubscribe link is missing or malformed.\"],\"YZxXK8\":[\"Close emoji picker\"],\"YcjhhX\":[\"User management\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" liked your comment on <1>\",[\"1\"],\"\"],\"YpkCca\":[\"No followed playlists yet.\"],\"Z5HWHd\":[\"On\"],\"ZBdbv9\":[\"Edit title\"],\"ZCpU0u\":[\"No playlists match \\\"\",[\"q\"],\"\\\".\"],\"ZWxjuD\":[\"Some images couldn't be uploaded.\"],\"ZmD2o6\":[\"Create & Add\"],\"_0oJwI\":[\"Trust\"],\"_3O5R_\":[\"Request failed\"],\"_DneUO\":[\"updated \",[\"ago\"]],\"_DwR-n\":[\"Creating…\"],\"_I1_HN\":[\"Lowercase letters, numbers and single hyphens only\"],\"_R_sGB\":[\"Password changed successfully.\"],\"_aept4\":[\"Post reply\"],\"_cF7Rs\":[\"Volume\"],\"_eZzBf\":[\"Sign in elsewhere\"],\"_karOe\":[\"You're offline — not sent\"],\"_nT6AE\":[\"New password\"],\"_t4W-i\":[\"From people\"],\"aAIQg2\":[\"Appearance\"],\"aDvLhk\":[\"Add a comment…\"],\"aFOsPT\":[\"Most dumps\"],\"az8lvo\":[\"Off\"],\"b3Thhd\":[\"Upload failed\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"bQhwn-\":[\"Loading playlist…\"],\"b_1P1D\":[\"Renaming…\"],\"b_VouM\":[\"Your account lives on \",[\"home\"],\": your password, email address, avatar and username change there.\"],\"bzSI52\":[\"Discard\"],\"cILfnJ\":[\"Remove file\"],\"cNu4my\":[[\"0\"],\" more online: \",[\"restNames\"]],\"cYP9Sb\":[\"+ Playlist\"],\"cbeBbZ\":[\"At least \",[\"0\"],\" characters\"],\"cnGeoo\":[\"Delete\"],\"dAs22m\":[\"Replace file\"],\"dEgA5A\":[\"Cancel\"],\"dMizp8\":[\"New playlist\"],\"dau1Pl\":[[\"unseenCount\"],\" new messages\"],\"digest.community\":[\"Community\"],\"digest.count.comments\":[[\"n\",\"plural\",{\"one\":[\"comment\"],\"other\":[\"comments\"]}]],\"digest.count.upvotes\":[[\"n\",\"plural\",{\"one\":[\"upvote\"],\"other\":[\"upvotes\"]}]],\"digest.invitedBy\":[\"invited by @\",[\"username\"]],\"digest.invitedByYou\":[\"invited by you\"],\"digest.monthly\":[\"Monthly recap\"],\"digest.monthly.off\":[\"Turn off monthly recaps\"],\"digest.newMembers\":[\"New members\"],\"digest.newPlaylists\":[\"New public playlists\"],\"digest.stat.chat\":[[\"n\",\"plural\",{\"one\":[\"chat message\"],\"other\":[\"chat messages\"]}]],\"digest.stat.dumps\":[[\"n\",\"plural\",{\"one\":[\"dump\"],\"other\":[\"dumps\"]}]],\"digest.topMonth\":[\"Top this month\"],\"digest.topWeek\":[\"Top this week\"],\"digest.weekly\":[\"Weekly digest\"],\"digest.weekly.off\":[\"Turn off weekly digests\"],\"dqwCfR\":[\"Couldn't load a preview for this link — check it, or post it as-is and refresh the preview later.\"],\"dznB-Y\":[\"Only images can be dropped here.\"],\"eFSqvc\":[\"Failed to post reply\"],\"eIEHHV\":[\"Change skin tone\"],\"eOfXq3\":[\"Title is required.\"],\"ePK91l\":[\"Edit\"],\"eaUTwS\":[\"Send reset link\"],\"ecUA8p\":[\"Today\"],\"ef9nPf\":[\"Loading dump…\"],\"eloqXQ\":[\"You can post it anyway.\"],\"email.digest.addedTo\":[\"added to \",[\"playlist\"]],\"email.digest.andMore\":[\"and \",[\"n\"],\" more\"],\"email.digest.by\":[\"by @\",[\"username\"]],\"email.digest.chatWeek\":[[\"n\",\"plural\",{\"one\":[\"#\",\" message this week\"],\"other\":[\"#\",\" messages this week\"]}]],\"email.digest.communityPage\":[\"See it all on the community page\"],\"email.digest.count.followers\":[[\"n\",\"plural\",{\"one\":[\"new follower\"],\"other\":[\"new followers\"]}]],\"email.digest.count.likes\":[[\"n\",\"plural\",{\"one\":[\"comment like\"],\"other\":[\"comment likes\"]}]],\"email.digest.count.mentions\":[[\"n\",\"plural\",{\"one\":[\"mention\"],\"other\":[\"mentions\"]}]],\"email.digest.count.playlistFollows\":[[\"n\",\"plural\",{\"one\":[\"playlist follow\"],\"other\":[\"playlist follows\"]}]],\"email.digest.forYou\":[\"For you\"],\"email.digest.hl.comment\":[[\"who\"],\" commented on your dump \",[\"target\"]],\"email.digest.hl.commentLike\":[[\"who\"],\" liked your comment on \",[\"target\"]],\"email.digest.hl.mentionComment\":[[\"who\"],\" mentioned you in a comment on \",[\"target\"]],\"email.digest.hl.mentionEveryone\":[[\"who\"],\" mentioned everyone in \",[\"target\"]],\"email.digest.hl.mentionIn\":[[\"who\"],\" mentioned you in \",[\"target\"]],\"email.digest.hl.mentionPlaylist\":[[\"who\"],\" mentioned you in the playlist \",[\"target\"]],\"email.digest.hl.playlistFollow\":[[\"who\"],\" followed your playlist \",[\"target\"]],\"email.digest.hl.userFollow\":[[\"who\"],\" started following you\"],\"email.digest.inChat\":[\"In the chat\"],\"email.digest.monthly.reason\":[\"You're getting the monthly recap because it's on in your settings.\"],\"email.digest.monthly.subject\":[\"The month on \",[\"site\"],\", \",[\"range\"]],\"email.digest.monthly.title\":[\"The month on \",[\"site\"]],\"email.digest.more\":[[\"n\"],\" more on your Followed feed\"],\"email.digest.network\":[\"From people you follow\"],\"email.digest.openChat\":[\"Open the chat\"],\"email.digest.playlistMeta\":[\"by @\",[\"username\"],\" · \",[\"n\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"email.digest.pre.members\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new member\"],\"other\":[\"#\",\" new members\"]}]],\"email.digest.pre.network\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new dump from people you follow\"],\"other\":[\"#\",\" new dumps from people you follow\"]}]],\"email.digest.pre.playlists\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new playlist\"],\"other\":[\"#\",\" new playlists\"]}]],\"email.digest.pre.topMonth\":[[\"n\",\"plural\",{\"one\":[\"the most upvoted dump of the month\"],\"other\":[\"the \",\"#\",\" most upvoted dumps of the month\"]}]],\"email.digest.pre.topWeek\":[[\"n\",\"plural\",{\"one\":[\"the most upvoted dump of the week\"],\"other\":[\"the \",\"#\",\" most upvoted dumps of the week\"]}]],\"email.digest.seeAll\":[[\"n\",\"plural\",{\"one\":[\"See the notification\"],\"other\":[\"See all \",\"#\",\" notifications\"]}]],\"email.digest.theChat\":[\"the chat\"],\"email.digest.totals\":[[\"dumps\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}],\", \",[\"upvotes\",\"plural\",{\"one\":[\"#\",\" upvote\"],\"other\":[\"#\",\" upvotes\"]}],\" and \",[\"comments\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}],\".\"],\"email.digest.upload\":[\"upload\"],\"email.digest.weekly.reason\":[\"You're getting the weekly digest because it's on in your settings.\"],\"email.digest.weekly.subject\":[\"Your week on \",[\"site\"],\", \",[\"range\"]],\"email.digest.weekly.title\":[\"Your week, \",[\"username\"]],\"email.digest.yours\":[\"yours\"],\"email.reset.body\":[\"Someone asked to reset the password of your \",[\"site\"],\" account. The button below lets you choose a new one. It works once, for \",[\"hours\",\"plural\",{\"one\":[\"#\",\" hour\"],\"other\":[\"#\",\" hours\"]}],\".\"],\"email.reset.button\":[\"Reset password\"],\"email.reset.fallback\":[\"If the button doesn't work, paste this link into your browser:\"],\"email.reset.hi\":[\"Hi \",[\"username\"],\",\"],\"email.reset.ignore\":[\"If you didn't ask for this, ignore this email. Your password stays the same.\"],\"email.reset.preheader\":[\"The link works once, for \",[\"hours\",\"plural\",{\"one\":[\"#\",\" hour\"],\"other\":[\"#\",\" hours\"]}],\".\"],\"email.reset.reason\":[\"You're getting this email because a password reset was requested for this address.\"],\"email.reset.subject\":[\"Reset your \",[\"site\"],\" password\"],\"email.reset.title\":[\"Reset your password\"],\"email.settings\":[\"Settings\"],\"email.welcome.body\":[\"# Welcome to \",[\"site\"],\"!\\n\\nHi **\",[\"username\"],\"**,\\n\\nYour account has been created successfully. Welcome aboard!\"],\"email.welcome.button\":[\"Open \",[\"site\"]],\"email.welcome.digests\":[\"Once a week you'll get a digest of what happened here, and once a month a recap of the most upvoted dumps. You can turn either off in your settings.\"],\"email.welcome.preheader\":[\"Your account is ready.\"],\"email.welcome.reason\":[\"You're getting this email because an account was created on \",[\"site\"],\" with this address.\"],\"email.welcome.subject\":[\"Welcome to \",[\"site\"]],\"en9o7K\":[\"Failed to post comment\"],\"etFQQS\":[\"What makes it worth it?\"],\"f3cEDO\":[\"Change image\"],\"fC6mXb\":[\"Upvote\"],\"fI-mNw\":[\"Playlists\"],\"fgLNSM\":[\"Register\"],\"fn1ZQX\":[\"Sign in to \",[\"name\"],\"?\"],\"gANddk\":[\"Uploading…\"],\"gGx5tM\":[\"Editing\"],\"gIQQwD\":[\"Failed to load\"],\"gIpLuy\":[\"A link is required\"],\"gL9KKi\":[\"Who can see it\"],\"gLfZlz\":[\"Add to playlist\"],\"gjJ-sb\":[\"Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects.\"],\"goWEhR\":[\"Previous track\"],\"hBuUKa\":[\"Change password…\"],\"hD7w09\":[\"You've reached the end.\"],\"hJSliC\":[\"<0>\",[\"0\"],\" posted <1>\",[\"1\"],\"\"],\"hLAe-z\":[\"Back to the login page\"],\"hT8hgO\":[\"Reset to the suggested title\"],\"hYgDIe\":[\"Create\"],\"he3ygx\":[\"Copy\"],\"hpCc_Z\":[\"Close anyway\"],\"i3uTFh\":[\"Dump categories\"],\"i7K_Te\":[\"Who am I?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Go home\"],\"ipYn7W\":[\"If that address is registered you'll receive a reset link shortly.\"],\"isRobC\":[\"New\"],\"jTGs8A\":[\"This site doesn't send email, so no digest goes out.\"],\"jZNg18\":[\"Account on <0>\",[\"host\"],\"\"],\"jbernk\":[\"Loading profile…\"],\"jnrIbz\":[\"Upvoted (\",[\"0\"],\")\"],\"joEmfT\":[\"Server unreachable\"],\"jrZTZl\":[\"No dumps yet. Be the first!\"],\"k112DD\":[\"Load older messages\"],\"k36iJ-\":[\"Failed to save category\"],\"k4b5_X\":[\"edited\"],\"kLttbL\":[\"Registration failed\"],\"lUanmi\":[\"You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"lcfvr_\":[\"Delete this comment?\"],\"lpIMne\":[\"Passwords do not match\"],\"m9Uf3I\":[\"Users of a trusted instance sign in here with their account there. Those of a blocked one can't. Users of any other instance need an invite.\"],\"mDJo8W\":[\"Dumps (\",[\"0\"],\")\"],\"mbJgSu\":[\"<0>\",[\"0\"],\" joined, invited by <1>\",[\"1\"],\"\"],\"mb_Wju\":[\"Drop a file here, or paste one\"],\"mt6O6E\":[\"This is a mirage.\"],\"nbm5sI\":[\"No dumps match \\\"\",[\"q\"],\"\\\".\"],\"nrjqON\":[\"Checking invite…\"],\"nucyoV\":[\"<0>\",[\"0\"],\" mentioned everyone in <1>\",[\"where\"],\"\"],\"nwtY4N\":[\"Something went wrong\"],\"nx4kaN\":[\"Could not save\"],\"oSrMRT\":[\"Mine\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"and \",\"#\",\" earlier dump\"],\"other\":[\"and \",\"#\",\" earlier dumps\"]}]],\"oW3yYY\":[\"This month\"],\"ogtYkT\":[\"Password updated\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" mentioned you in <1>\",[\"where\"],\"\"],\"pSheLH\":[\"No invitees yet.\"],\"pd3FCq\":[\"That file is over \",[\"limit\"],\".\"],\"pvnfJD\":[\"Dark\"],\"q018gJ\":[\"Couldn't add that instance\"],\"qIMfNQ\":[\"Delete playlist\"],\"qItrqH\":[\"You won't get the monthly recap any more. You can turn it back on in your settings.\"],\"qVdjD5\":[\"Trusted\"],\"qbDAcy\":[\"Dump it\"],\"qgx_78\":[\"Follow some public playlists to see their dumps here.\"],\"qnUJBI\":[\"The new order couldn't be saved. Try again.\"],\"qsqt0w\":[\"Invite link\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Dump\"],\"rCbqPX\":[\"This invite link is missing, expired, or already used.\"],\"rDMzDU\":[\"Type a message…\"],\"rg9pXu\":[\"Search failed\"],\"rjgjpq\":[\"<0>\",[\"0\"],\" joined\"],\"rn6SBY\":[\"Unmute\"],\"sEXJPi\":[\"Link or file\"],\"sGeXL3\":[\"Thumbnail\"],\"sQia9P\":[\"Log in\"],\"sTiqbm\":[\"invited by\"],\"sdP5Aa\":[\"[deleted]\"],\"sgX7B-\":[\"Single use, valid for 7 days.\"],\"shHs8T\":[\"Enter a query to search.\"],\"smeBfS\":[\"Invalid invite\"],\"tOHgQw\":[\"Continue as @\",[\"username\"]],\"tXNIqW\":[\"This sign-in was started elsewhere. Start again from the login page.\"],\"teaneZ\":[\"image\"],\"tfDRzk\":[\"Save\"],\"tvmuQ0\":[\"Color scheme\"],\"tyiiHe\":[\"Nothing happened this month.\"],\"u1lDX2\":[\"Fetching preview…\"],\"u8pttd\":[\"Change it on \",[\"home\"]],\"uACFmj\":[\"Log in with \",[\"0\"]],\"uD0qXQ\":[\"Drop a file here\"],\"uISnsY\":[\"Could not delete\"],\"uMGUnV\":[\"No playlists yet.\"],\"ub1EEL\":[\"edited \",[\"0\"]],\"uomLas\":[\"Add category\"],\"v-Wp--\":[\"Open source\"],\"vJBF1r\":[\"Posting…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" unread)\"],\"vQMkHu\":[\"Remove vote\"],\"vbo92d\":[\"<0>Online now: \",[\"names\"]],\"vuosjb\":[\"User menu\"],\"w61W3L\":[\"Remove \",[\"0\"]],\"wckWOP\":[\"Manage\"],\"wixIgH\":[\"Already have an account? <0>Log in\"],\"wrFDaD\":[\"Users here can't sign in to a blocked instance.\"],\"wtxFp4\":[\"File too large (max \",[\"max\"],\").\"],\"x6tjuK\":[\"Default tab\"],\"xEWkgZ\":[\"← Back to all dumps\"],\"xGVfLh\":[\"Continue\"],\"xPHtx0\":[\"Submit search\"],\"xVuNgt\":[\"+ New playlist\"],\"xc9O_u\":[\"Delete dump\"],\"y1G_DI\":[\"Could not rename\"],\"y6sq5j\":[\"Following\"],\"yA_6BX\":[\"View all →\"],\"yBBtRm\":[\"Follow some users to see their dumps here.\"],\"yByRxz\":[\"This week\"],\"yQ2kGp\":[\"Load more\"],\"y_0uwd\":[\"Yesterday\"],\"yxlcv5\":[\"Turn off the monthly recap?\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"z0ROB3\":[\"Remove like\"],\"z1uNN0\":[\"No emoji found.\"],\"zVuxvN\":[\"Refreshing…\"],\"zwBp5t\":[\"Private\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"-K0AvT\":[\"Disconnect\"],\"-K9EZb\":[\"Add email…\"],\"-Ya-b9\":[\"Failed to save\"],\"-b7T3G\":[\"Updated\"],\"-iDCtG\":[\"Your settings\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Search dumps, users, playlists…\"],\"1QfxQT\":[\"Dismiss\"],\"1YKhJ3\":[\"1 new message\"],\"1njn7W\":[\"Light\"],\"1utXA6\":[\"Dumps\"],\"26iNma\":[\"Post comment\"],\"2DoBvq\":[\"Feeds\"],\"2Hlmdt\":[\"Write a reply…\"],\"2_DHgA\":[\"URL slug\"],\"2nrsEn\":[\"deleted message\"],\"2rwzb1\":[\"You haven't signed in to another gerbeur with this account yet.\"],\"2wxgft\":[\"Rename\"],\"2ygf_L\":[\"← Back\"],\"32TndD\":[\"Blocked\"],\"3ESfuy\":[\"Add a description…\"],\"3KKSM4\":[\"private\"],\"3QuNUr\":[\"Several people are typing…\"],\"3SEwY1\":[\"Delete category \\\"\",[\"0\"],\"\\\"? This cannot be undone.\"],\"3qkggm\":[\"Fullscreen\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" followed your playlist <1>\",[\"1\"],\"\"],\"48e2R8\":[\"Edit email\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Dumped!\"],\"4GKuCs\":[\"Login failed\"],\"4HH9iB\":[\"Not following anyone yet.\"],\"4OjqAQ\":[\"Keep editing\"],\"4RtQ1k\":[\"Unfollow \",[\"targetUsername\"]],\"4c-qBx\":[\"Your password has been changed. You can now log in.\"],\"4yj9xV\":[\"Live updates are temporarily disconnected. Trying to reconnect…\"],\"5KHDCi\":[[\"0\"],\" and \",[\"1\"],\" are typing…\"],\"5NZ3tg\":[\"Turning off…\"],\"5TviPn\":[\"Cancel search\"],\"5WRdOd\":[\"<0>Top this week: <1>\",[\"0\"],\"\"],\"5cC8f2\":[\"Edited \",[\"0\"]],\"5oD9f_\":[\"Earlier\"],\"6BV5h6\":[\"This dump was deleted, or made private.\"],\"6C4kKS\":[\"Other instances\"],\"6Qly-0\":[\"a comment\"],\"6YE0P6\":[\"Editing \",[\"0\"]],\"6YtxFj\":[\"Name\"],\"6_dCYd\":[\"Overview\"],\"6gRgw8\":[\"Retry\"],\"7PHCIN\":[\"Cancel removal\"],\"7PzzBU\":[\"User\"],\"7d1a0d\":[\"Public\"],\"7noMWp\":[\"Nothing happened this week.\"],\"7sNhEz\":[\"Username\"],\"8-8abB\":[\"Disconnected — reconnecting…\"],\"8F1i42\":[\"Page not found\"],\"8JplJJ\":[\"Unsubscribed\"],\"8TgExA\":[\"<0>\",[\"0\"],\" created the playlist <1>\",[\"1\"],\"\"],\"8Ug9jB\":[\"Related\"],\"8ZsakT\":[\"Password\"],\"8rPWOT\":[\"Can't sign in there\"],\"92o1-U\":[\"Failed to delete\"],\"9BruTc\":[\"Why?\"],\"9eEYEq\":[\"This sign-in link is incomplete.\"],\"9l4qcT\":[\"Drop a replacement here\"],\"9uI_rE\":[\"Undo\"],\"A0y396\":[\"+ Invite someone\"],\"A1taO8\":[\"Search\"],\"AEfu-D\":[\"In collections\"],\"AHZflp\":[\"Chat\"],\"AQbgNR\":[\"Follow \",[\"targetUsername\"]],\"AS7WoE\":[\"Start fresh\"],\"ATGYL1\":[\"Email address\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"Ade-6d\":[\"Live updates unavailable.\"],\"AeXO77\":[\"Account\"],\"AhNP40\":[\"Seek\"],\"AnRu_j\":[\"Cancel reply\"],\"AwUsnG\":[\"Instances\"],\"B4LeYP\":[\"Delete failed (\",[\"0\"],\")\"],\"BKVDJg\":[\"created \",[\"ago\"]],\"BjitBo\":[\"Download \",[\"0\"]],\"BlpJ2G\":[\"Picked up where you left off.\"],\"CI50ct\":[\"Upvoted\"],\"CJkEfx\":[\"Block\"],\"CL-DkX\":[\"An instance's URL\"],\"C_qQE4\":[\"Already dumped by \",[\"0\"],\" \",[\"1\"]],\"Cf_gxi\":[\"Turn off the weekly digest?\"],\"Cj24wt\":[\"Registering…\"],\"ClFfRL\":[\"Playlist categories\"],\"CnqayG\":[\"Couldn't update. Try again.\"],\"Czya-u\":[\"Users of a trusted instance sign in here with their account there. Those of a blocked one can't, and users here can't sign in to it. Users of any other instance need an invite.\"],\"D8DRYV\":[\"Delete this message?\"],\"DI3NBO\":[\"Open on \",[\"0\"]],\"DPfwMq\":[\"Done\"],\"DdeHXH\":[\"Delete this dump? This cannot be undone.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" upvoted <1>\",[\"1\"],\"\"],\"E417RG\":[\"You won't get the weekly digest any more. You can turn it back on in your settings.\"],\"EBU6O6\":[\"Connection lost — this message may not have been sent\"],\"ECiS12\":[\"View dump →\"],\"ED8n0z\":[\"the chat\"],\"EarrCe\":[\"Search emoji\"],\"Evcm2Z\":[\"Close without posting? The attached file won't be kept — everything you typed will.\"],\"EwVrWF\":[\"The dump couldn't be removed. Try again.\"],\"ExR0Fr\":[\"URL is required.\"],\"F5Js1v\":[\"Unfollow playlist\"],\"FdTlEG\":[\"Track \",[\"0\"]],\"FgAxTj\":[\"Log out\"],\"Fxf4jq\":[\"Description (optional)\"],\"G8OFDZ\":[\"<0>This week: <1>\",[\"figures\"],\"\"],\"GCWCCo\":[\"Close player\"],\"GDvlUT\":[\"Role\"],\"GLukaH\":[\"Moderator\"],\"GNSsCc\":[\"Failed to create playlist\"],\"GQ38dl\":[\"Chat (\",[\"unreadChatCount\"],\" unread)\"],\"GY6uSc\":[\"Next →\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" characters: letters, numbers, or underscores\"],\"GptGxg\":[\"Change password\"],\"Gv5nA5\":[\"Unexpected response format\"],\"H4MGkI\":[\"Expand player\"],\"H4o4sk\":[\"No one yet.\"],\"H5X-EM\":[\"Edit note\"],\"H8pzW-\":[\"Failed to update avatar\"],\"HTLDA4\":[\"Loading more…\"],\"H_PnT8\":[\"Insert emoji\"],\"I-x669\":[\"Invitees\"],\"IF7RiR\":[\"Cover\"],\"IO9KL_\":[\"Next track\"],\"IZX7TO\":[\"Failed to generate invite\"],\"IagCbF\":[\"URL\"],\"IgrLD_\":[\"Pause\"],\"IhK90M\":[\"Letters, numbers and underscores only\"],\"ImOQa9\":[\"Reply\"],\"JJ-Bhk\":[\"Your email address\"],\"JLasL-\":[\"Couldn't unsubscribe\"],\"JRQitQ\":[\"Confirm new password\"],\"JXr41k\":[\"<0>\",[\"0\"],\" commented on <1>\",[\"1\"],\"\"],\"Jd58Fo\":[\"Hot\"],\"Jf0PuK\":[\"Go to login\"],\"JlFcis\":[\"Send\"],\"JrFTcr\":[\"Connecting…\"],\"K7tIrx\":[\"Category\"],\"KDGWg5\":[\"Remove from playlist\"],\"KDw4GX\":[\"Try again\"],\"KKZxBU\":[\"Uploading \",[\"0\"],\"%\"],\"K_F6pa\":[\"Saving…\"],\"KtYF-t\":[\"Sounds\"],\"L-rMC9\":[\"Reset to default\"],\"L7_rsx\":[\"Uploading image…\"],\"MHrjPM\":[\"Title\"],\"MKEPCY\":[\"Follow\"],\"MTb2Ot\":[\"No messages yet. Say hello!\"],\"MbkoRn\":[\"Or join with your account on another gerbeur\"],\"Mj6oCJ\":[[\"0\"],\" is typing…\"],\"MrIc3x\":[\"Or log in with your account on another gerbeur\"],\"N40H-G\":[\"All\"],\"N6j2JH\":[\"Edit \",[\"0\"]],\"NUrY9o\":[\"Categories\"],\"Nu4oKW\":[\"Description\"],\"O32mmL\":[\"Edit description\"],\"O3oNi5\":[\"Email\"],\"Oprv1v\":[\"Password (min. \",[\"0\"],\" characters)\"],\"P5MuiB\":[\"Add image\"],\"PJTy3N\":[[\"totalUnread\",\"plural\",{\"one\":[\"#\",\" new\"],\"other\":[\"#\",\" new\"]}]],\"PN3pBl\":[\"No reply from the server — this message may not have been sent\"],\"PUe8Yk\":[\"Collapse player\"],\"PiH3UR\":[\"Copied!\"],\"Pn2B7_\":[\"Current password\"],\"Pwqkdw\":[\"Loading…\"],\"Q4WUjR\":[\"Search emoji…\"],\"Q6n4F4\":[\"Refresh metadata\"],\"QGaQRo\":[\"Failed to update role\"],\"QKsaQr\":[\"or <0>browse files\"],\"QLtPBd\":[\"No dumps in this playlist yet.\"],\"QctOgc\":[\"Could not connect to the server. Check your connection and try again.\"],\"R7SsBE\":[\"Mute\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Delete this playlist? This cannot be undone.\"],\"RLe7Vk\":[\"Checking…\"],\"RTksSy\":[\"<0>\",[\"0\"],\" started following you\"],\"RaKjrM\":[\"Failed to save edit\"],\"RcUHRT\":[\"Followed\"],\"RfwZxd\":[\"Reset password\"],\"Rrp6-J\":[\"Log in to like\"],\"RvVJBH\":[\"Refreshing stream…\"],\"S1xV5B\":[\"At most \",[\"maxLength\"],\" characters.\"],\"SBTElJ\":[\"Searching…\"],\"SLaP1b\":[\"Not from here\"],\"SMaFdc\":[\"Unsubscribe\"],\"Sad2tK\":[\"Sending…\"],\"So7gCD\":[\"Paste a link…\"],\"StovX6\":[\"This page does not exist.\"],\"Sxm8rQ\":[\"Users\"],\"T8ejtZ\":[\"This comment couldn't be deleted. Try again.\"],\"T91vKp\":[\"Play\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" was added to <1>\",[\"1\"],\"\"],\"TAkYGZ\":[\"Your account lives on \",[\"home\"],\": sign in to other instances from there.\"],\"TAtbho\":[\"Loading messages…\"],\"TN382O\":[\"Invalid link\"],\"ToADL_\":[\"<0>\",[\"name\"],\" (\",[\"host\"],\") asks to sign you in with your account here, <1>@\",[\"username\"],\". It will receive your username, your email address and your avatar.\"],\"Tv9vbB\":[\"Follow playlist\"],\"Tz0i8g\":[\"Settings\"],\"U3pytU\":[\"Admin\"],\"UNMVei\":[\"Forgot password?\"],\"UOV0wi\":[\"Signing you in…\"],\"UOZith\":[\"Failed to post\"],\"URAieT\":[\"Log in to vote\"],\"V38P77\":[\"Only its poster or a moderator can edit this dump.\"],\"VCoEm-\":[\"Back to login\"],\"VKsaTi\":[\"Replying to\"],\"VLAd8f\":[\"Max \",[\"limit\"]],\"VLINRu\":[\"Your gerbeur's URL\"],\"V_e7nf\":[\"Set new password\"],\"VnNJbN\":[\"From playlists\"],\"VskHIx\":[\"Delete category\"],\"VyTYmS\":[\"Change avatar\"],\"W5A0Ly\":[\"An unexpected error occurred.\"],\"W9FRBT\":[\"Like\"],\"WAcnnR\":[\"The image couldn't be uploaded.\"],\"WE_r9K\":[\"Failed to create category\"],\"WMw63e\":[\"Couldn't reach that instance\"],\"WfffrI\":[\"slug\"],\"WhimMi\":[\"Reset failed\"],\"WpXcBJ\":[\"Nothing here yet.\"],\"XHqhoS\":[\"Couldn't load the chat.\"],\"XJVYo8\":[\"Why & where\"],\"XJy2oN\":[\"Logging in…\"],\"XWbTfu\":[\"last used \",[\"0\"]],\"Xan6QP\":[\"New dump\"],\"XgRtUf\":[\"Could not change password\"],\"XnL-Eu\":[\"No users match \\\"\",[\"q\"],\"\\\".\"],\"YK1Dhc\":[\"a post\"],\"YWnSuq\":[\"This unsubscribe link is missing or malformed.\"],\"YZxXK8\":[\"Close emoji picker\"],\"YcjhhX\":[\"User management\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" liked your comment on <1>\",[\"1\"],\"\"],\"YpkCca\":[\"No followed playlists yet.\"],\"Z5HWHd\":[\"On\"],\"ZBdbv9\":[\"Edit title\"],\"ZCpU0u\":[\"No playlists match \\\"\",[\"q\"],\"\\\".\"],\"ZWxjuD\":[\"Some images couldn't be uploaded.\"],\"ZmD2o6\":[\"Create & Add\"],\"_0oJwI\":[\"Trust\"],\"_3O5R_\":[\"Request failed\"],\"_DneUO\":[\"updated \",[\"ago\"]],\"_DwR-n\":[\"Creating…\"],\"_I1_HN\":[\"Lowercase letters, numbers and single hyphens only\"],\"_R_sGB\":[\"Password changed successfully.\"],\"_aept4\":[\"Post reply\"],\"_cF7Rs\":[\"Volume\"],\"_eZzBf\":[\"Sign in elsewhere\"],\"_karOe\":[\"You're offline — not sent\"],\"_nT6AE\":[\"New password\"],\"_t4W-i\":[\"From people\"],\"aAIQg2\":[\"Appearance\"],\"aDvLhk\":[\"Add a comment…\"],\"aFOsPT\":[\"Most dumps\"],\"az8lvo\":[\"Off\"],\"b3Thhd\":[\"Upload failed\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"bQhwn-\":[\"Loading playlist…\"],\"b_1P1D\":[\"Renaming…\"],\"b_VouM\":[\"Your account lives on \",[\"home\"],\": your password, email address, avatar and username change there.\"],\"bzSI52\":[\"Discard\"],\"cILfnJ\":[\"Remove file\"],\"cNu4my\":[[\"0\"],\" more online: \",[\"restNames\"]],\"cYP9Sb\":[\"+ Playlist\"],\"cbeBbZ\":[\"At least \",[\"0\"],\" characters\"],\"cnGeoo\":[\"Delete\"],\"dAs22m\":[\"Replace file\"],\"dEgA5A\":[\"Cancel\"],\"dMizp8\":[\"New playlist\"],\"dau1Pl\":[[\"unseenCount\"],\" new messages\"],\"digest.community\":[\"Community\"],\"digest.count.comments\":[[\"n\",\"plural\",{\"one\":[\"comment\"],\"other\":[\"comments\"]}]],\"digest.count.upvotes\":[[\"n\",\"plural\",{\"one\":[\"upvote\"],\"other\":[\"upvotes\"]}]],\"digest.invitedBy\":[\"invited by @\",[\"username\"]],\"digest.invitedByYou\":[\"invited by you\"],\"digest.monthly\":[\"Monthly recap\"],\"digest.monthly.off\":[\"Turn off monthly recaps\"],\"digest.newMembers\":[\"New members\"],\"digest.newPlaylists\":[\"New public playlists\"],\"digest.stat.chat\":[[\"n\",\"plural\",{\"one\":[\"chat message\"],\"other\":[\"chat messages\"]}]],\"digest.stat.dumps\":[[\"n\",\"plural\",{\"one\":[\"dump\"],\"other\":[\"dumps\"]}]],\"digest.topMonth\":[\"Top this month\"],\"digest.topWeek\":[\"Top this week\"],\"digest.weekly\":[\"Weekly digest\"],\"digest.weekly.off\":[\"Turn off weekly digests\"],\"dqwCfR\":[\"Couldn't load a preview for this link — check it, or post it as-is and refresh the preview later.\"],\"dznB-Y\":[\"Only images can be dropped here.\"],\"eFSqvc\":[\"Failed to post reply\"],\"eIEHHV\":[\"Change skin tone\"],\"eOfXq3\":[\"Title is required.\"],\"ePK91l\":[\"Edit\"],\"eaUTwS\":[\"Send reset link\"],\"ecUA8p\":[\"Today\"],\"ef9nPf\":[\"Loading dump…\"],\"eloqXQ\":[\"You can post it anyway.\"],\"email.digest.addedTo\":[\"added to \",[\"playlist\"]],\"email.digest.andMore\":[\"and \",[\"n\"],\" more\"],\"email.digest.by\":[\"by @\",[\"username\"]],\"email.digest.chatWeek\":[[\"n\",\"plural\",{\"one\":[\"#\",\" message this week\"],\"other\":[\"#\",\" messages this week\"]}]],\"email.digest.communityPage\":[\"See it all on the community page\"],\"email.digest.count.followers\":[[\"n\",\"plural\",{\"one\":[\"new follower\"],\"other\":[\"new followers\"]}]],\"email.digest.count.likes\":[[\"n\",\"plural\",{\"one\":[\"comment like\"],\"other\":[\"comment likes\"]}]],\"email.digest.count.mentions\":[[\"n\",\"plural\",{\"one\":[\"mention\"],\"other\":[\"mentions\"]}]],\"email.digest.count.playlistFollows\":[[\"n\",\"plural\",{\"one\":[\"playlist follow\"],\"other\":[\"playlist follows\"]}]],\"email.digest.forYou\":[\"For you\"],\"email.digest.hl.comment\":[[\"who\"],\" commented on your dump \",[\"target\"]],\"email.digest.hl.commentLike\":[[\"who\"],\" liked your comment on \",[\"target\"]],\"email.digest.hl.mentionComment\":[[\"who\"],\" mentioned you in a comment on \",[\"target\"]],\"email.digest.hl.mentionEveryone\":[[\"who\"],\" mentioned everyone in \",[\"target\"]],\"email.digest.hl.mentionIn\":[[\"who\"],\" mentioned you in \",[\"target\"]],\"email.digest.hl.mentionPlaylist\":[[\"who\"],\" mentioned you in the playlist \",[\"target\"]],\"email.digest.hl.playlistFollow\":[[\"who\"],\" followed your playlist \",[\"target\"]],\"email.digest.hl.userFollow\":[[\"who\"],\" started following you\"],\"email.digest.inChat\":[\"In the chat\"],\"email.digest.monthly.reason\":[\"You're getting the monthly recap because it's on in your settings.\"],\"email.digest.monthly.subject\":[\"The month on \",[\"site\"],\", \",[\"range\"]],\"email.digest.monthly.title\":[\"The month on \",[\"site\"]],\"email.digest.more\":[[\"n\"],\" more on your Followed feed\"],\"email.digest.network\":[\"From people you follow\"],\"email.digest.openChat\":[\"Open the chat\"],\"email.digest.playlistMeta\":[\"by @\",[\"username\"],\" · \",[\"n\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"email.digest.pre.members\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new member\"],\"other\":[\"#\",\" new members\"]}]],\"email.digest.pre.network\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new dump from people you follow\"],\"other\":[\"#\",\" new dumps from people you follow\"]}]],\"email.digest.pre.playlists\":[[\"n\",\"plural\",{\"one\":[\"#\",\" new playlist\"],\"other\":[\"#\",\" new playlists\"]}]],\"email.digest.pre.topMonth\":[[\"n\",\"plural\",{\"one\":[\"the most upvoted dump of the month\"],\"other\":[\"the \",\"#\",\" most upvoted dumps of the month\"]}]],\"email.digest.pre.topWeek\":[[\"n\",\"plural\",{\"one\":[\"the most upvoted dump of the week\"],\"other\":[\"the \",\"#\",\" most upvoted dumps of the week\"]}]],\"email.digest.seeAll\":[[\"n\",\"plural\",{\"one\":[\"See the notification\"],\"other\":[\"See all \",\"#\",\" notifications\"]}]],\"email.digest.theChat\":[\"the chat\"],\"email.digest.totals\":[[\"dumps\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}],\", \",[\"upvotes\",\"plural\",{\"one\":[\"#\",\" upvote\"],\"other\":[\"#\",\" upvotes\"]}],\" and \",[\"comments\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}],\".\"],\"email.digest.upload\":[\"upload\"],\"email.digest.weekly.reason\":[\"You're getting the weekly digest because it's on in your settings.\"],\"email.digest.weekly.subject\":[\"Your week on \",[\"site\"],\", \",[\"range\"]],\"email.digest.weekly.title\":[\"Your week, \",[\"username\"]],\"email.digest.yours\":[\"yours\"],\"email.reset.body\":[\"Someone asked to reset the password of your \",[\"site\"],\" account. The button below lets you choose a new one. It works once, for \",[\"hours\",\"plural\",{\"one\":[\"#\",\" hour\"],\"other\":[\"#\",\" hours\"]}],\".\"],\"email.reset.button\":[\"Reset password\"],\"email.reset.fallback\":[\"If the button doesn't work, paste this link into your browser:\"],\"email.reset.hi\":[\"Hi \",[\"username\"],\",\"],\"email.reset.ignore\":[\"If you didn't ask for this, ignore this email. Your password stays the same.\"],\"email.reset.preheader\":[\"The link works once, for \",[\"hours\",\"plural\",{\"one\":[\"#\",\" hour\"],\"other\":[\"#\",\" hours\"]}],\".\"],\"email.reset.reason\":[\"You're getting this email because a password reset was requested for this address.\"],\"email.reset.subject\":[\"Reset your \",[\"site\"],\" password\"],\"email.reset.title\":[\"Reset your password\"],\"email.settings\":[\"Settings\"],\"email.welcome.body\":[\"# Welcome to \",[\"site\"],\"!\\n\\nHi **\",[\"username\"],\"**,\\n\\nYour account has been created successfully. Welcome aboard!\"],\"email.welcome.button\":[\"Open \",[\"site\"]],\"email.welcome.digests\":[\"Once a week you'll get a digest of what happened here, and once a month a recap of the most upvoted dumps. You can turn either off in your settings.\"],\"email.welcome.preheader\":[\"Your account is ready.\"],\"email.welcome.reason\":[\"You're getting this email because an account was created on \",[\"site\"],\" with this address.\"],\"email.welcome.subject\":[\"Welcome to \",[\"site\"]],\"en9o7K\":[\"Failed to post comment\"],\"etFQQS\":[\"What makes it worth it?\"],\"f3cEDO\":[\"Change image\"],\"fC6mXb\":[\"Upvote\"],\"fI-mNw\":[\"Playlists\"],\"fgLNSM\":[\"Register\"],\"fn1ZQX\":[\"Sign in to \",[\"name\"],\"?\"],\"gANddk\":[\"Uploading…\"],\"gGx5tM\":[\"Editing\"],\"gIQQwD\":[\"Failed to load\"],\"gIpLuy\":[\"A link is required\"],\"gL9KKi\":[\"Who can see it\"],\"gLfZlz\":[\"Add to playlist\"],\"gjJ-sb\":[\"Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects.\"],\"goWEhR\":[\"Previous track\"],\"hBuUKa\":[\"Change password…\"],\"hD7w09\":[\"You've reached the end.\"],\"hJSliC\":[\"<0>\",[\"0\"],\" posted <1>\",[\"1\"],\"\"],\"hLAe-z\":[\"Back to the login page\"],\"hT8hgO\":[\"Reset to the suggested title\"],\"hYgDIe\":[\"Create\"],\"he3ygx\":[\"Copy\"],\"hpCc_Z\":[\"Close anyway\"],\"i3uTFh\":[\"Dump categories\"],\"i7K_Te\":[\"Who am I?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Go home\"],\"ipYn7W\":[\"If that address is registered you'll receive a reset link shortly.\"],\"isRobC\":[\"New\"],\"jTGs8A\":[\"This site doesn't send email, so no digest goes out.\"],\"jZNg18\":[\"Account on <0>\",[\"host\"],\"\"],\"jbernk\":[\"Loading profile…\"],\"jnrIbz\":[\"Upvoted (\",[\"0\"],\")\"],\"joEmfT\":[\"Server unreachable\"],\"jrZTZl\":[\"No dumps yet. Be the first!\"],\"k112DD\":[\"Load older messages\"],\"k36iJ-\":[\"Failed to save category\"],\"k4b5_X\":[\"edited\"],\"kLttbL\":[\"Registration failed\"],\"lUanmi\":[\"You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"lcfvr_\":[\"Delete this comment?\"],\"lpIMne\":[\"Passwords do not match\"],\"m9Uf3I\":[\"Users of a trusted instance sign in here with their account there. Those of a blocked one can't. Users of any other instance need an invite.\"],\"mDJo8W\":[\"Dumps (\",[\"0\"],\")\"],\"mbJgSu\":[\"<0>\",[\"0\"],\" joined, invited by <1>\",[\"1\"],\"\"],\"mb_Wju\":[\"Drop a file here, or paste one\"],\"mt6O6E\":[\"This is a mirage.\"],\"nbm5sI\":[\"No dumps match \\\"\",[\"q\"],\"\\\".\"],\"nrjqON\":[\"Checking invite…\"],\"nucyoV\":[\"<0>\",[\"0\"],\" mentioned everyone in <1>\",[\"where\"],\"\"],\"nwtY4N\":[\"Something went wrong\"],\"nx4kaN\":[\"Could not save\"],\"oSrMRT\":[\"Mine\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"and \",\"#\",\" earlier dump\"],\"other\":[\"and \",\"#\",\" earlier dumps\"]}]],\"oW3yYY\":[\"This month\"],\"ogtYkT\":[\"Password updated\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" mentioned you in <1>\",[\"where\"],\"\"],\"pSheLH\":[\"No invitees yet.\"],\"pd3FCq\":[\"That file is over \",[\"limit\"],\".\"],\"pvnfJD\":[\"Dark\"],\"q018gJ\":[\"Couldn't add that instance\"],\"qIMfNQ\":[\"Delete playlist\"],\"qItrqH\":[\"You won't get the monthly recap any more. You can turn it back on in your settings.\"],\"qVdjD5\":[\"Trusted\"],\"qbDAcy\":[\"Dump it\"],\"qgx_78\":[\"Follow some public playlists to see their dumps here.\"],\"qnUJBI\":[\"The new order couldn't be saved. Try again.\"],\"qsqt0w\":[\"Invite link\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Dump\"],\"rCbqPX\":[\"This invite link is missing, expired, or already used.\"],\"rDMzDU\":[\"Type a message…\"],\"rg9pXu\":[\"Search failed\"],\"rjgjpq\":[\"<0>\",[\"0\"],\" joined\"],\"rn6SBY\":[\"Unmute\"],\"sEXJPi\":[\"Link or file\"],\"sGeXL3\":[\"Thumbnail\"],\"sQia9P\":[\"Log in\"],\"sTiqbm\":[\"invited by\"],\"sdP5Aa\":[\"[deleted]\"],\"sgX7B-\":[\"Single use, valid for 7 days.\"],\"shHs8T\":[\"Enter a query to search.\"],\"smeBfS\":[\"Invalid invite\"],\"tOHgQw\":[\"Continue as @\",[\"username\"]],\"tXNIqW\":[\"This sign-in was started elsewhere. Start again from the login page.\"],\"teaneZ\":[\"image\"],\"tfDRzk\":[\"Save\"],\"tvmuQ0\":[\"Color scheme\"],\"tyiiHe\":[\"Nothing happened this month.\"],\"u1lDX2\":[\"Fetching preview…\"],\"u8pttd\":[\"Change it on \",[\"home\"]],\"uACFmj\":[\"Log in with \",[\"0\"]],\"uD0qXQ\":[\"Drop a file here\"],\"uISnsY\":[\"Could not delete\"],\"uMGUnV\":[\"No playlists yet.\"],\"ub1EEL\":[\"edited \",[\"0\"]],\"uomLas\":[\"Add category\"],\"v-Wp--\":[\"Open source\"],\"vJBF1r\":[\"Posting…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" unread)\"],\"vQMkHu\":[\"Remove vote\"],\"vbo92d\":[\"<0>Online now: \",[\"names\"]],\"vuosjb\":[\"User menu\"],\"w61W3L\":[\"Remove \",[\"0\"]],\"wckWOP\":[\"Manage\"],\"wixIgH\":[\"Already have an account? <0>Log in\"],\"wrFDaD\":[\"Users here can't sign in to a blocked instance.\"],\"wtxFp4\":[\"File too large (max \",[\"max\"],\").\"],\"x6tjuK\":[\"Default tab\"],\"xEWkgZ\":[\"← Back to all dumps\"],\"xGVfLh\":[\"Continue\"],\"xPHtx0\":[\"Submit search\"],\"xVuNgt\":[\"+ New playlist\"],\"xc9O_u\":[\"Delete dump\"],\"y1G_DI\":[\"Could not rename\"],\"y6sq5j\":[\"Following\"],\"yA_6BX\":[\"View all →\"],\"yBBtRm\":[\"Follow some users to see their dumps here.\"],\"yByRxz\":[\"This week\"],\"yQ2kGp\":[\"Load more\"],\"y_0uwd\":[\"Yesterday\"],\"yxlcv5\":[\"Turn off the monthly recap?\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"z0ROB3\":[\"Remove like\"],\"z1uNN0\":[\"No emoji found.\"],\"zVuxvN\":[\"Refreshing…\"],\"zwBp5t\":[\"Private\"]}"); \ No newline at end of file diff --git a/locales/en.po b/locales/en.po index 11c51b18..76a1f0ec 100644 --- a/locales/en.po +++ b/locales/en.po @@ -206,14 +206,14 @@ msgstr "# Welcome to {site}!\n" "Your account has been created successfully. Welcome aboard!" #: src/components/dump/create/DumpCreateModal.tsx:736 -#: src/pages/PlaylistDetail.tsx:98 +#: src/pages/PlaylistDetail.tsx:99 #: src/pages/UserPublicProfile.tsx:129 msgid "← Back" msgstr "← Back" -#: src/pages/Dump.tsx:375 -#: src/pages/Dump.tsx:400 -#: src/pages/Dump.tsx:627 +#: src/pages/Dump.tsx:374 +#: src/pages/Dump.tsx:399 +#: src/pages/Dump.tsx:560 #: src/pages/DumpEdit.tsx:234 msgid "← Back to all dumps" msgstr "← Back to all dumps" @@ -226,7 +226,7 @@ msgstr "+ Invite someone" msgid "+ New playlist" msgstr "+ New playlist" -#: src/pages/Dump.tsx:474 +#: src/pages/Dump.tsx:451 msgid "+ Playlist" msgstr "+ Playlist" @@ -252,7 +252,7 @@ msgid "<0>{0} followed your playlist <1>{1}" msgstr "<0>{0} followed your playlist <1>{1}" #. placeholder {0}: event.username -#: src/components/social/CommunityFeed.tsx:144 +#: src/components/social/CommunityFeed.tsx:145 msgid "<0>{0} joined" msgstr "<0>{0} joined" @@ -311,16 +311,16 @@ msgstr "<0>{0} was added to <1>{1}" msgid "<0>{name} ({host}) asks to sign you in with your account here, <1>@{username}. It will receive your username, your email address and your avatar." msgstr "<0>{name} ({host}) asks to sign you in with your account here, <1>@{username}. It will receive your username, your email address and your avatar." -#: src/components/social/CommunityFeed.tsx:155 +#: src/components/social/CommunityFeed.tsx:159 msgid "<0>Online now: {names}" msgstr "<0>Online now: {names}" -#: src/components/social/CommunityFeed.tsx:162 +#: src/components/social/CommunityFeed.tsx:166 msgid "<0>This week: <1>{figures}" msgstr "<0>This week: <1>{figures}" #. placeholder {0}: top.title -#: src/components/social/CommunityFeed.tsx:170 +#: src/components/social/CommunityFeed.tsx:174 msgid "<0>Top this week: <1>{0}" msgstr "<0>Top this week: <1>{0}" @@ -344,7 +344,7 @@ msgstr "a post" msgid "Account" msgstr "Account" -#: src/components/profile/ProfileHeader.tsx:144 +#: src/components/profile/ProfileHeader.tsx:151 msgid "Account on <0>{host}" msgstr "Account on <0>{host}" @@ -352,12 +352,17 @@ msgstr "Account on <0>{host}" msgid "Add a comment…" msgstr "Add a comment…" +#: src/components/playlist/PlaylistHeader.tsx:117 +#: src/components/playlist/PlaylistHeader.tsx:128 +msgid "Add a description…" +msgstr "Add a description…" + #: src/components/category/CategoryManager.tsx:115 #: src/components/category/CategoryManager.tsx:116 msgid "Add category" msgstr "Add category" -#: src/components/profile/ProfileHeader.tsx:173 +#: src/components/profile/ProfileHeader.tsx:168 msgid "Add email…" msgstr "Add email…" @@ -470,12 +475,10 @@ msgstr "Can't sign in there" #: src/components/comment/CommentForm.tsx:127 #: src/components/dump/create/DumpCreateModal.tsx:746 #: src/components/form/FormActions.tsx:42 +#: src/components/form/InlineEdit.tsx:277 #: src/components/playlist/PlaylistEditForm.tsx:222 -#: src/components/profile/DescriptionEditor.tsx:97 -#: src/components/profile/EmailEditor.tsx:83 #: src/components/shared/ConfirmModal.tsx:63 #: src/pages/Authorize.tsx:158 -#: src/pages/Dump.tsx:516 #: src/pages/DumpEdit.tsx:561 msgid "Cancel" msgstr "Cancel" @@ -501,7 +504,7 @@ msgstr "Categories" msgid "Category" msgstr "Category" -#: src/components/profile/ProfileHeader.tsx:102 +#: src/components/profile/ProfileHeader.tsx:109 msgid "Change avatar" msgstr "Change avatar" @@ -721,7 +724,7 @@ msgstr "Delete failed ({0})" #: src/components/playlist/PlaylistCard.tsx:172 #: src/components/playlist/PlaylistEditForm.tsx:213 #: src/components/profile/ProfilePlaylistsTab.tsx:224 -#: src/pages/PlaylistDetail.tsx:172 +#: src/pages/PlaylistDetail.tsx:174 msgid "Delete playlist" msgstr "Delete playlist" @@ -738,7 +741,7 @@ msgid "Delete this message?" msgstr "Delete this message?" #: src/components/profile/ProfilePlaylistsTab.tsx:223 -#: src/pages/PlaylistDetail.tsx:171 +#: src/pages/PlaylistDetail.tsx:173 msgid "Delete this playlist? This cannot be undone." msgstr "Delete this playlist? This cannot be undone." @@ -827,8 +830,8 @@ msgstr "Earlier" #: src/components/chat/ChatMessageItem.tsx:108 #: src/components/chat/PendingItem.tsx:93 #: src/components/comment/CommentNode.tsx:256 -#: src/components/playlist/PlaylistHeader.tsx:64 -#: src/pages/Dump.tsx:623 +#: src/components/playlist/PlaylistHeader.tsx:104 +#: src/pages/Dump.tsx:556 msgid "Edit" msgstr "Edit" @@ -837,16 +840,22 @@ msgstr "Edit" msgid "Edit {0}" msgstr "Edit {0}" -#: src/components/profile/ProfileDescription.tsx:58 +#: src/components/playlist/PlaylistHeader.tsx:113 +#: src/components/profile/ProfileDescription.tsx:45 msgid "Edit description" msgstr "Edit description" -#: src/components/profile/ProfileHeader.tsx:171 +#: src/components/profile/ProfileHeader.tsx:161 msgid "Edit email" msgstr "Edit email" +#: src/pages/Dump.tsx:510 +msgid "Edit note" +msgstr "Edit note" + #: src/components/dump/create/TitleField.tsx:69 -#: src/pages/Dump.tsx:533 +#: src/components/playlist/PlaylistHeader.tsx:84 +#: src/pages/Dump.tsx:459 msgid "Edit title" msgstr "Edit title" @@ -859,8 +868,8 @@ msgstr "edited" #. placeholder {0}: relativeTime(message.updatedAt) #: src/components/chat/ChatMessageItem.tsx:132 #: src/components/comment/CommentNode.tsx:205 -#: src/components/playlist/PlaylistHeader.tsx:99 -#: src/pages/Dump.tsx:576 +#: src/components/playlist/PlaylistHeader.tsx:158 +#: src/pages/Dump.tsx:492 msgid "edited {0}" msgstr "edited {0}" @@ -869,8 +878,8 @@ msgstr "edited {0}" #. placeholder {0}: message.updatedAt.toLocaleString() #: src/components/chat/ChatMessageItem.tsx:128 #: src/components/comment/CommentNode.tsx:203 -#: src/components/playlist/PlaylistHeader.tsx:97 -#: src/pages/Dump.tsx:574 +#: src/components/playlist/PlaylistHeader.tsx:156 +#: src/pages/Dump.tsx:490 msgid "Edited {0}" msgstr "Edited {0}" @@ -936,8 +945,6 @@ msgstr "Failed to post reply" #: src/components/federation/GrantList.tsx:98 #: src/components/federation/PeerManager.tsx:158 #: src/components/playlist/PlaylistEditForm.tsx:207 -#: src/components/profile/DescriptionEditor.tsx:99 -#: src/components/profile/EmailEditor.tsx:86 msgid "Failed to save" msgstr "Failed to save" @@ -949,7 +956,7 @@ msgstr "Failed to save category" msgid "Failed to save edit" msgstr "Failed to save edit" -#: src/components/profile/ProfileHeader.tsx:182 +#: src/components/profile/ProfileHeader.tsx:174 msgid "Failed to update avatar" msgstr "Failed to update avatar" @@ -1064,7 +1071,7 @@ msgstr "If you didn't ask for this, ignore this email. Your password stays the s msgid "image" msgstr "image" -#: src/pages/Dump.tsx:643 +#: src/pages/Dump.tsx:576 msgid "In collections" msgstr "In collections" @@ -1095,7 +1102,7 @@ msgstr "Invalid link" msgid "Invite link" msgstr "Invite link" -#: src/components/profile/ProfileHeader.tsx:126 +#: src/components/profile/ProfileHeader.tsx:133 msgid "invited by" msgstr "invited by" @@ -1165,7 +1172,7 @@ msgstr "Load more" msgid "Load older messages" msgstr "Load older messages" -#: src/pages/Dump.tsx:359 +#: src/pages/Dump.tsx:358 #: src/pages/DumpEdit.tsx:210 msgid "Loading dump…" msgstr "Loading dump…" @@ -1178,7 +1185,7 @@ msgstr "Loading messages…" msgid "Loading more…" msgstr "Loading more…" -#: src/pages/PlaylistDetail.tsx:82 +#: src/pages/PlaylistDetail.tsx:83 msgid "Loading playlist…" msgstr "Loading playlist…" @@ -1548,9 +1555,9 @@ msgstr "Previous track" #: src/components/dump/DumpCard.tsx:144 #: src/components/dump/JournalCard.tsx:146 #: src/components/playlist/PlaylistCard.tsx:127 -#: src/components/playlist/PlaylistHeader.tsx:81 +#: src/components/playlist/PlaylistHeader.tsx:140 #: src/components/playlist/PlaylistMembershipPanel.tsx:81 -#: src/pages/Dump.tsx:582 +#: src/pages/Dump.tsx:498 msgid "private" msgstr "private" @@ -1559,7 +1566,7 @@ msgid "Private" msgstr "Private" #: src/components/playlist/PlaylistCard.tsx:126 -#: src/components/playlist/PlaylistHeader.tsx:81 +#: src/components/playlist/PlaylistHeader.tsx:140 msgid "public" msgstr "public" @@ -1593,7 +1600,7 @@ msgstr "Registering…" msgid "Registration failed" msgstr "Registration failed" -#: src/pages/Dump.tsx:634 +#: src/pages/Dump.tsx:567 msgid "Related" msgstr "Related" @@ -1680,7 +1687,7 @@ msgid "email.reset.title" msgstr "Reset your password" #: src/components/chat/PendingItem.tsx:86 -#: src/pages/Dump.tsx:393 +#: src/pages/Dump.tsx:392 #: src/pages/DumpEdit.tsx:227 msgid "Retry" msgstr "Retry" @@ -1692,20 +1699,16 @@ msgstr "Role" #: src/components/category/CategoryManager.tsx:232 #: src/components/chat/ChatEditBox.tsx:85 #: src/components/comment/CommentNode.tsx:216 +#: src/components/form/InlineEdit.tsx:269 #: src/components/playlist/PlaylistEditForm.tsx:229 -#: src/components/profile/DescriptionEditor.tsx:89 -#: src/components/profile/EmailEditor.tsx:75 -#: src/pages/Dump.tsx:508 #: src/pages/DumpEdit.tsx:564 msgid "Save" msgstr "Save" #: src/components/comment/CommentNode.tsx:217 +#: src/components/form/InlineEdit.tsx:266 #: src/components/playlist/PlaylistEditForm.tsx:226 #: src/components/profile/ChangePasswordModal.tsx:119 -#: src/components/profile/DescriptionEditor.tsx:86 -#: src/components/profile/EmailEditor.tsx:72 -#: src/pages/Dump.tsx:507 #: src/pages/ResetPassword.tsx:191 msgid "Saving…" msgstr "Saving…" @@ -1815,7 +1818,7 @@ msgstr "Someone asked to reset the password of your {site} account. The button b #: src/components/form/FormError.tsx:31 #: src/components/layout/PageError.tsx:29 #: src/contexts/ws/WSProvider.tsx:738 -#: src/pages/PlaylistDetail.tsx:155 +#: src/pages/PlaylistDetail.tsx:157 msgid "Something went wrong" msgstr "Something went wrong" @@ -1879,7 +1882,7 @@ msgstr "The new order couldn't be saved. Try again." msgid "This comment couldn't be deleted. Try again." msgstr "This comment couldn't be deleted. Try again." -#: src/pages/Dump.tsx:368 +#: src/pages/Dump.tsx:367 msgid "This dump was deleted, or made private." msgstr "This dump was deleted, or made private." @@ -2033,7 +2036,7 @@ msgstr "updated {ago}" msgid "email.digest.upload" msgstr "upload" -#: src/components/profile/ProfileHeader.tsx:79 +#: src/components/profile/ProfileHeader.tsx:80 msgid "Upload failed" msgstr "Upload failed" @@ -2136,12 +2139,14 @@ msgid "email.welcome.subject" msgstr "Welcome to {site}" #: src/components/dump/create/DumpCreateModal.tsx:705 +#: src/pages/Dump.tsx:514 +#: src/pages/Dump.tsx:521 #: src/pages/DumpEdit.tsx:539 msgid "What makes it worth it?" msgstr "What makes it worth it?" -#: src/components/profile/DescriptionEditor.tsx:78 -#: src/components/profile/ProfileDescription.tsx:79 +#: src/components/profile/ProfileDescription.tsx:62 +#: src/components/profile/ProfileDescription.tsx:72 msgid "Who am I?" msgstr "Who am I?" diff --git a/locales/fr.mjs b/locales/fr.mjs index 50193975..4410e4cd 100644 --- a/locales/fr.mjs +++ b/locales/fr.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"-K0AvT\":[\"Déconnecter\"],\"-K9EZb\":[\"Ajouter un e-mail…\"],\"-Ya-b9\":[\"Enregistrement échoué\"],\"-b7T3G\":[\"Mises à jour\"],\"-iDCtG\":[\"Vos paramètres\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Rechercher des recos, utilisateurs, collections…\"],\"1QfxQT\":[\"Fermer\"],\"1YKhJ3\":[\"1 nouveau message\"],\"1njn7W\":[\"Clair\"],\"1utXA6\":[\"Recos\"],\"26iNma\":[\"Publier le commentaire\"],\"2DoBvq\":[\"Flux\"],\"2Hlmdt\":[\"Écrire une réponse…\"],\"2_DHgA\":[\"Identifiant d’URL\"],\"2nrsEn\":[\"message supprimé\"],\"2rwzb1\":[\"Vous ne vous êtes encore connecté à aucun autre gerbeur avec ce compte.\"],\"2wxgft\":[\"Renommer\"],\"2ygf_L\":[\"← Retour\"],\"32TndD\":[\"Bloquée\"],\"3KKSM4\":[\"privé\"],\"3QuNUr\":[\"Plusieurs personnes écrivent…\"],\"3SEwY1\":[\"Supprimer la catégorie \\\"\",[\"0\"],\"\\\" ? Cette action est irréversible.\"],\"3qkggm\":[\"Plein écran\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" a suivi votre collection <1>\",[\"1\"],\"\"],\"48e2R8\":[\"Modifier l'e-mail\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Recommandé !\"],\"4GKuCs\":[\"Connexion échouée\"],\"4HH9iB\":[\"Aucun abonnement pour le moment.\"],\"4OjqAQ\":[\"Continuer l'édition\"],\"4RtQ1k\":[\"Ne plus suivre \",[\"targetUsername\"]],\"4c-qBx\":[\"Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter.\"],\"4yj9xV\":[\"Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…\"],\"5KHDCi\":[[\"0\"],\" et \",[\"1\"],\" sont en train d'écrire…\"],\"5NZ3tg\":[\"Désactivation…\"],\"5TviPn\":[\"Annuler la recherche\"],\"5WRdOd\":[\"<0>En tête cette semaine : <1>\",[\"0\"],\"\"],\"5cC8f2\":[\"Modifié le \",[\"0\"]],\"5oD9f_\":[\"Plus tôt\"],\"6BV5h6\":[\"Cette reco a été supprimée, ou rendue privée.\"],\"6C4kKS\":[\"Autres instances\"],\"6Qly-0\":[\"un commentaire\"],\"6YE0P6\":[\"Édition de \",[\"0\"]],\"6YtxFj\":[\"Nom\"],\"6_dCYd\":[\"Aperçu\"],\"6gRgw8\":[\"Réessayer\"],\"7PHCIN\":[\"Annuler la suppression\"],\"7PzzBU\":[\"Utilisateur\"],\"7d1a0d\":[\"Public\"],\"7noMWp\":[\"Rien de neuf cette semaine.\"],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8-8abB\":[\"Déconnecté — reconnexion…\"],\"8F1i42\":[\"Page introuvable\"],\"8JplJJ\":[\"Désabonné\"],\"8TgExA\":[\"<0>\",[\"0\"],\" a créé la playlist <1>\",[\"1\"],\"\"],\"8Ug9jB\":[\"Connexe\"],\"8ZsakT\":[\"Mot de passe\"],\"8rPWOT\":[\"Connexion impossible\"],\"92o1-U\":[\"Suppression échouée\"],\"9BruTc\":[\"Pourquoi ?\"],\"9eEYEq\":[\"Ce lien de connexion est incomplet.\"],\"9l4qcT\":[\"Déposez un fichier de remplacement ici\"],\"9uI_rE\":[\"Annuler\"],\"A0y396\":[\"+ Inviter quelqu'un\"],\"A1taO8\":[\"Rechercher\"],\"AEfu-D\":[\"Dans les collections\"],\"AHZflp\":[\"Tribune\"],\"AQbgNR\":[\"Suivre \",[\"targetUsername\"]],\"AS7WoE\":[\"Repartir de zéro\"],\"ATGYL1\":[\"Adresse e-mail\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"Ade-6d\":[\"Mises à jour en direct indisponibles.\"],\"AeXO77\":[\"Compte\"],\"AhNP40\":[\"Position\"],\"AnRu_j\":[\"Annuler la réponse\"],\"AwUsnG\":[\"Instances\"],\"B4LeYP\":[\"Échec de la suppression (\",[\"0\"],\")\"],\"BKVDJg\":[\"créée \",[\"ago\"]],\"BjitBo\":[\"Télécharger \",[\"0\"]],\"BlpJ2G\":[\"Reprise de votre brouillon.\"],\"CI50ct\":[\"Validé\"],\"CJkEfx\":[\"Bloquer\"],\"CL-DkX\":[\"URL d'une instance\"],\"C_qQE4\":[\"Déjà recommandé par \",[\"0\"],\" \",[\"1\"]],\"Cf_gxi\":[\"Ne plus recevoir le résumé hebdomadaire ?\"],\"Cj24wt\":[\"Inscription…\"],\"ClFfRL\":[\"Catégories de collections\"],\"CnqayG\":[\"La mise à jour a échoué. Réessayez.\"],\"Czya-u\":[\"Les utilisateurs d'une instance approuvée se connectent ici avec leur compte de là-bas. Ceux d'une instance bloquée ne le peuvent pas, et les utilisateurs d'ici ne peuvent pas s'y connecter. Ceux de toute autre instance ont besoin d'une invitation.\"],\"D8DRYV\":[\"Supprimer ce message ?\"],\"DI3NBO\":[\"Ouvrir sur \",[\"0\"]],\"DPfwMq\":[\"Terminé\"],\"DdeHXH\":[\"Supprimer cette reco ? Cette action est irréversible.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" a validé <1>\",[\"1\"],\"\"],\"E417RG\":[\"Vous ne recevrez plus le résumé hebdomadaire. Vous pouvez le réactiver dans vos paramètres.\"],\"EBU6O6\":[\"Connexion perdue — ce message n'a peut-être pas été envoyé\"],\"ECiS12\":[\"Voir la reco →\"],\"ED8n0z\":[\"la tribune\"],\"EarrCe\":[\"Rechercher un emoji\"],\"Evcm2Z\":[\"Fermer sans publier ? Le fichier joint sera perdu — ce que vous avez écrit sera conservé.\"],\"EwVrWF\":[\"Le dump n'a pas pu être retiré. Réessayez.\"],\"ExR0Fr\":[\"L'URL est obligatoire.\"],\"F5Js1v\":[\"Ne plus suivre la collection\"],\"FdTlEG\":[\"Piste \",[\"0\"]],\"FgAxTj\":[\"Se déconnecter\"],\"Fxf4jq\":[\"Description (facultatif)\"],\"G8OFDZ\":[\"<0>Cette semaine : <1>\",[\"figures\"],\"\"],\"GCWCCo\":[\"Fermer le lecteur\"],\"GDvlUT\":[\"Rôle\"],\"GLukaH\":[\"Modérateur\"],\"GNSsCc\":[\"Impossible de créer la collection\"],\"GQ38dl\":[[\"unreadChatCount\"],\" messages non lus\"],\"GY6uSc\":[\"Suivant →\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" caractères : lettres, chiffres ou tirets bas\"],\"GptGxg\":[\"Changer le mot de passe\"],\"Gv5nA5\":[\"Format de réponse inattendu\"],\"H4MGkI\":[\"Agrandir le lecteur\"],\"H4o4sk\":[\"Personne pour le moment.\"],\"H8pzW-\":[\"Impossible de mettre à jour l'avatar\"],\"HTLDA4\":[\"Chargement…\"],\"H_PnT8\":[\"Insérer un emoji\"],\"I-x669\":[\"Invités\"],\"IF7RiR\":[\"Couverture\"],\"IO9KL_\":[\"Piste suivante\"],\"IZX7TO\":[\"Impossible de générer une invitation\"],\"IagCbF\":[\"URL\"],\"IgrLD_\":[\"Pause\"],\"IhK90M\":[\"Lettres, chiffres et tirets bas seulement\"],\"ImOQa9\":[\"Répondre\"],\"JJ-Bhk\":[\"Votre adresse e-mail\"],\"JLasL-\":[\"Désabonnement impossible\"],\"JRQitQ\":[\"Confirmer le nouveau mot de passe\"],\"JXr41k\":[\"<0>\",[\"0\"],\" a commenté sur <1>\",[\"1\"],\"\"],\"Jd58Fo\":[\"Tendances\"],\"Jf0PuK\":[\"Aller à la connexion\"],\"JlFcis\":[\"Envoyer\"],\"JrFTcr\":[\"Connexion…\"],\"K7tIrx\":[\"Catégorie\"],\"KDGWg5\":[\"Retirer de la collection\"],\"KDw4GX\":[\"Réessayer\"],\"KKZxBU\":[\"Envoi \",[\"0\"],\" %\"],\"K_F6pa\":[\"Enregistrement…\"],\"KtYF-t\":[\"Sons\"],\"L-rMC9\":[\"Réinitialiser par défaut\"],\"L7_rsx\":[\"Envoi de l'image…\"],\"MHrjPM\":[\"Titre\"],\"MKEPCY\":[\"Suivre\"],\"MTb2Ot\":[\"Aucun message pour l'instant. Dites bonjour !\"],\"MbkoRn\":[\"Ou rejoignez-nous avec votre compte d'un autre gerbeur\"],\"Mj6oCJ\":[[\"0\"],\" est en train d'écrire…\"],\"MrIc3x\":[\"Ou connectez-vous avec votre compte d'un autre gerbeur\"],\"N40H-G\":[\"Tout\"],\"N6j2JH\":[\"Modifier \",[\"0\"]],\"NUrY9o\":[\"Catégories\"],\"Nu4oKW\":[\"Description\"],\"O32mmL\":[\"Modifier la description\"],\"O3oNi5\":[\"E-mail\"],\"Oprv1v\":[\"Mot de passe (min. \",[\"0\"],\" caractères)\"],\"P5MuiB\":[\"Ajouter une image\"],\"PJTy3N\":[[\"totalUnread\",\"plural\",{\"one\":[\"#\",\" nouvelle\"],\"other\":[\"#\",\" nouvelles\"]}]],\"PN3pBl\":[\"Pas de réponse du serveur — ce message n'a peut-être pas été envoyé\"],\"PUe8Yk\":[\"Réduire le lecteur\"],\"PiH3UR\":[\"Copié !\"],\"Pn2B7_\":[\"Mot de passe actuel\"],\"Pwqkdw\":[\"Chargement…\"],\"Q4WUjR\":[\"Rechercher un emoji…\"],\"Q6n4F4\":[\"Actualiser les métadonnées\"],\"QGaQRo\":[\"Erreur lors de la mise à jour du rôle\"],\"QKsaQr\":[\"ou <0>parcourir les fichiers\"],\"QLtPBd\":[\"Aucune reco dans cette collection pour l'instant.\"],\"QctOgc\":[\"Impossible de joindre le serveur. Vérifiez votre connexion et réessayez.\"],\"R7SsBE\":[\"Couper le son\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Supprimer cette collection ? Cette action est irréversible.\"],\"RLe7Vk\":[\"Vérification…\"],\"RTksSy\":[\"<0>\",[\"0\"],\" a commencé à vous suivre\"],\"RaKjrM\":[\"Impossible d'enregistrer la modification\"],\"RcUHRT\":[\"Suivi\"],\"RfwZxd\":[\"Réinitialiser le mot de passe\"],\"Rrp6-J\":[\"Se connecter pour aimer\"],\"RvVJBH\":[\"Actualisation du flux…\"],\"S1xV5B\":[[\"maxLength\"],\" caractères au maximum.\"],\"SBTElJ\":[\"Recherche…\"],\"SLaP1b\":[\"Compte d'ailleurs\"],\"SMaFdc\":[\"Se désabonner\"],\"Sad2tK\":[\"Envoi…\"],\"So7gCD\":[\"Collez un lien…\"],\"StovX6\":[\"Rien à voir, circulez.\"],\"Sxm8rQ\":[\"Utilisateurs\"],\"T8ejtZ\":[\"Ce commentaire n'a pas pu être supprimé. Réessayez.\"],\"T91vKp\":[\"Lecture\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" a été ajouté à <1>\",[\"1\"],\"\"],\"TAkYGZ\":[\"Votre compte est sur \",[\"home\"],\" : connectez-vous aux autres instances depuis là-bas.\"],\"TAtbho\":[\"Chargement des messages…\"],\"TN382O\":[\"Lien invalide\"],\"ToADL_\":[\"<0>\",[\"name\"],\" (\",[\"host\"],\") demande à vous connecter avec votre compte d'ici, <1>@\",[\"username\"],\". Il recevra votre nom d'utilisateur, votre adresse e-mail et votre avatar.\"],\"Tv9vbB\":[\"Suivre la collection\"],\"Tz0i8g\":[\"Paramètres\"],\"U3pytU\":[\"Administrateur\"],\"UNMVei\":[\"Mot de passe oublié ?\"],\"UOV0wi\":[\"Connexion…\"],\"UOZith\":[\"Publication échouée\"],\"URAieT\":[\"Se connecter pour valider\"],\"V38P77\":[\"Seuls son auteur et les modérateurs peuvent modifier ce dump.\"],\"VCoEm-\":[\"Retour à la connexion\"],\"VKsaTi\":[\"En réponse à\"],\"VLAd8f\":[\"Max \",[\"limit\"]],\"VLINRu\":[\"URL de votre gerbeur\"],\"V_e7nf\":[\"Définir un nouveau mot de passe\"],\"VnNJbN\":[\"De collections\"],\"VskHIx\":[\"Supprimer la catégorie\"],\"VyTYmS\":[\"Changer l'avatar\"],\"W5A0Ly\":[\"Une erreur inattendue s'est produite.\"],\"W9FRBT\":[\"Aimer\"],\"WAcnnR\":[\"L'image n'a pas pu être envoyée.\"],\"WE_r9K\":[\"Échec de la création de la catégorie\"],\"WMw63e\":[\"Impossible de joindre cette instance\"],\"WfffrI\":[\"identifiant\"],\"WhimMi\":[\"Échec de la réinitialisation\"],\"WpXcBJ\":[\"Rien ici pour l'instant.\"],\"XHqhoS\":[\"Impossible de charger le chat.\"],\"XJVYo8\":[\"Pourquoi & où\"],\"XJy2oN\":[\"Connexion…\"],\"XWbTfu\":[\"utilisée \",[\"0\"]],\"Xan6QP\":[\"Nouvelle reco\"],\"XgRtUf\":[\"Impossible de changer le mot de passe\"],\"XnL-Eu\":[\"Aucun utilisateur ne correspond à « \",[\"q\"],\" ».\"],\"YK1Dhc\":[\"une publication\"],\"YWnSuq\":[\"Ce lien de désabonnement est absent ou mal formé.\"],\"YZxXK8\":[\"Fermer le sélecteur d'emoji\"],\"YcjhhX\":[\"Gestion utilisateur\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" a aimé votre commentaire sur <1>\",[\"1\"],\"\"],\"YpkCca\":[\"Pas encore de collections suivies.\"],\"Z5HWHd\":[\"Activés\"],\"ZBdbv9\":[\"Modifier le titre\"],\"ZCpU0u\":[\"Aucune collection ne correspond à « \",[\"q\"],\" ».\"],\"ZWxjuD\":[\"Certaines images n'ont pas pu être envoyées.\"],\"ZmD2o6\":[\"Créer et ajouter\"],\"_0oJwI\":[\"Approuver\"],\"_3O5R_\":[\"Échec de la demande\"],\"_DneUO\":[\"mise à jour \",[\"ago\"]],\"_DwR-n\":[\"Création…\"],\"_I1_HN\":[\"Minuscules, chiffres et tirets simples seulement\"],\"_R_sGB\":[\"Mot de passe modifié avec succès.\"],\"_aept4\":[\"Publier la réponse\"],\"_cF7Rs\":[\"Volume\"],\"_eZzBf\":[\"Se connecter ailleurs\"],\"_karOe\":[\"Hors ligne — message non envoyé\"],\"_nT6AE\":[\"Nouveau mot de passe\"],\"_t4W-i\":[\"De personnes\"],\"aAIQg2\":[\"Apparence\"],\"aDvLhk\":[\"Ajouter un commentaire…\"],\"aFOsPT\":[\"Les plus fournies\"],\"az8lvo\":[\"Désactivés\"],\"b3Thhd\":[\"Envoi échoué\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"bQhwn-\":[\"Chargement de la collection…\"],\"b_1P1D\":[\"Renommage…\"],\"b_VouM\":[\"Votre compte est sur \",[\"home\"],\" : votre mot de passe, votre adresse e-mail, votre avatar et votre nom d'utilisateur se changent là-bas.\"],\"bzSI52\":[\"Abandonner\"],\"cILfnJ\":[\"Supprimer le fichier\"],\"cNu4my\":[[\"0\"],\" de plus en ligne : \",[\"restNames\"]],\"cYP9Sb\":[\"+ Collection\"],\"cbeBbZ\":[\"Au moins \",[\"0\"],\" caractères\"],\"cnGeoo\":[\"Supprimer\"],\"dAs22m\":[\"Remplacer le fichier\"],\"dEgA5A\":[\"Annuler\"],\"dMizp8\":[\"Nouvelle collection\"],\"dau1Pl\":[[\"unseenCount\"],\" nouveaux messages\"],\"digest.community\":[\"Communauté\"],\"digest.count.comments\":[[\"n\",\"plural\",{\"one\":[\"commentaire\"],\"other\":[\"commentaires\"]}]],\"digest.count.upvotes\":[[\"n\",\"plural\",{\"one\":[\"vote\"],\"other\":[\"votes\"]}]],\"digest.invitedBy\":[\"sur invitation de @\",[\"username\"]],\"digest.invitedByYou\":[\"sur votre invitation\"],\"digest.monthly\":[\"Récap mensuel\"],\"digest.monthly.off\":[\"Ne plus recevoir le récap mensuel\"],\"digest.newMembers\":[\"Nouveaux membres\"],\"digest.newPlaylists\":[\"Nouvelles collections publiques\"],\"digest.stat.chat\":[[\"n\",\"plural\",{\"one\":[\"message dans la tribune\"],\"other\":[\"messages dans la tribune\"]}]],\"digest.stat.dumps\":[[\"n\",\"plural\",{\"one\":[\"reco\"],\"other\":[\"recos\"]}]],\"digest.topMonth\":[\"Les plus validées du mois\"],\"digest.topWeek\":[\"Les plus validées de la semaine\"],\"digest.weekly\":[\"Résumé hebdomadaire\"],\"digest.weekly.off\":[\"Ne plus recevoir le résumé hebdomadaire\"],\"dqwCfR\":[\"Impossible de charger un aperçu pour ce lien — vérifiez-le, ou publiez-le tel quel et actualisez l'aperçu plus tard.\"],\"dznB-Y\":[\"Seules les images peuvent être déposées ici.\"],\"eFSqvc\":[\"Impossible de publier la réponse\"],\"eIEHHV\":[\"Changer la couleur de peau\"],\"eOfXq3\":[\"Un titre est requis.\"],\"ePK91l\":[\"Modifier\"],\"eaUTwS\":[\"Envoyer le lien de réinitialisation\"],\"ecUA8p\":[\"Aujourd'hui\"],\"ef9nPf\":[\"Chargement de la reco…\"],\"eloqXQ\":[\"Vous pouvez la publier quand même.\"],\"email.digest.addedTo\":[\"ajoutée à \",[\"playlist\"]],\"email.digest.andMore\":[\"et \",[\"n\"],\" de plus\"],\"email.digest.by\":[\"par @\",[\"username\"]],\"email.digest.chatWeek\":[[\"n\",\"plural\",{\"one\":[\"#\",\" message cette semaine\"],\"other\":[\"#\",\" messages cette semaine\"]}]],\"email.digest.communityPage\":[\"Tout voir sur la page de la communauté\"],\"email.digest.count.followers\":[[\"n\",\"plural\",{\"one\":[\"nouvel abonné\"],\"other\":[\"nouveaux abonnés\"]}]],\"email.digest.count.likes\":[[\"n\",\"plural\",{\"one\":[\"j'aime sur vos commentaires\"],\"other\":[\"j'aime sur vos commentaires\"]}]],\"email.digest.count.mentions\":[[\"n\",\"plural\",{\"one\":[\"mention\"],\"other\":[\"mentions\"]}]],\"email.digest.count.playlistFollows\":[[\"n\",\"plural\",{\"one\":[\"abonnement à vos collections\"],\"other\":[\"abonnements à vos collections\"]}]],\"email.digest.forYou\":[\"Pour vous\"],\"email.digest.hl.comment\":[[\"who\"],\" a commenté votre reco \",[\"target\"]],\"email.digest.hl.commentLike\":[[\"who\"],\" a aimé votre commentaire sur \",[\"target\"]],\"email.digest.hl.mentionComment\":[[\"who\"],\" vous a mentionné dans un commentaire sur \",[\"target\"]],\"email.digest.hl.mentionEveryone\":[[\"who\"],\" a mentionné tout le monde dans \",[\"target\"]],\"email.digest.hl.mentionIn\":[[\"who\"],\" vous a mentionné dans \",[\"target\"]],\"email.digest.hl.mentionPlaylist\":[[\"who\"],\" vous a mentionné dans la collection \",[\"target\"]],\"email.digest.hl.playlistFollow\":[[\"who\"],\" a suivi votre collection \",[\"target\"]],\"email.digest.hl.userFollow\":[[\"who\"],\" vous suit désormais\"],\"email.digest.inChat\":[\"Dans la tribune\"],\"email.digest.monthly.reason\":[\"Vous recevez ce récap mensuel car il est activé dans vos paramètres.\"],\"email.digest.monthly.subject\":[\"Le mois sur \",[\"site\"],\", \",[\"range\"]],\"email.digest.monthly.title\":[\"Le mois sur \",[\"site\"]],\"email.digest.more\":[[\"n\"],\" de plus dans votre fil Suivi\"],\"email.digest.network\":[\"De ceux que vous suivez\"],\"email.digest.openChat\":[\"Ouvrir la tribune\"],\"email.digest.playlistMeta\":[\"par @\",[\"username\"],\" · \",[\"n\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"email.digest.pre.members\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouveau membre\"],\"other\":[\"#\",\" nouveaux membres\"]}]],\"email.digest.pre.network\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouvelle reco de ceux que vous suivez\"],\"other\":[\"#\",\" nouvelles recos de ceux que vous suivez\"]}]],\"email.digest.pre.playlists\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouvelle collection\"],\"other\":[\"#\",\" nouvelles collections\"]}]],\"email.digest.pre.topMonth\":[[\"n\",\"plural\",{\"one\":[\"la reco la plus validée du mois\"],\"other\":[\"les \",\"#\",\" recos les plus validées du mois\"]}]],\"email.digest.pre.topWeek\":[[\"n\",\"plural\",{\"one\":[\"la reco la plus validée de la semaine\"],\"other\":[\"les \",\"#\",\" recos les plus validées de la semaine\"]}]],\"email.digest.seeAll\":[[\"n\",\"plural\",{\"one\":[\"Voir la notification\"],\"other\":[\"Voir les \",\"#\",\" notifications\"]}]],\"email.digest.theChat\":[\"la tribune\"],\"email.digest.totals\":[[\"dumps\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}],\", \",[\"upvotes\",\"plural\",{\"one\":[\"#\",\" vote\"],\"other\":[\"#\",\" votes\"]}],\" et \",[\"comments\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}],\".\"],\"email.digest.upload\":[\"fichier\"],\"email.digest.weekly.reason\":[\"Vous recevez ce résumé hebdomadaire car il est activé dans vos paramètres.\"],\"email.digest.weekly.subject\":[\"Votre semaine sur \",[\"site\"],\", \",[\"range\"]],\"email.digest.weekly.title\":[\"Votre semaine, \",[\"username\"]],\"email.digest.yours\":[\"à vous\"],\"email.reset.body\":[\"Quelqu'un a demandé à réinitialiser le mot de passe de votre compte \",[\"site\"],\". Le bouton ci-dessous vous permet d'en choisir un nouveau. Il ne fonctionne qu'une fois, pendant \",[\"hours\",\"plural\",{\"one\":[\"#\",\" heure\"],\"other\":[\"#\",\" heures\"]}],\".\"],\"email.reset.button\":[\"Choisir un nouveau mot de passe\"],\"email.reset.fallback\":[\"Si le bouton ne fonctionne pas, collez ce lien dans votre navigateur :\"],\"email.reset.hi\":[\"Bonjour \",[\"username\"],\",\"],\"email.reset.ignore\":[\"Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail. Votre mot de passe ne change pas.\"],\"email.reset.preheader\":[\"Le lien ne fonctionne qu'une fois, pendant \",[\"hours\",\"plural\",{\"one\":[\"#\",\" heure\"],\"other\":[\"#\",\" heures\"]}],\".\"],\"email.reset.reason\":[\"Vous recevez cet e-mail car une réinitialisation de mot de passe a été demandée pour cette adresse.\"],\"email.reset.subject\":[\"Réinitialisez votre mot de passe \",[\"site\"]],\"email.reset.title\":[\"Réinitialiser votre mot de passe\"],\"email.settings\":[\"Paramètres\"],\"email.welcome.body\":[\"# Bienvenue sur \",[\"site\"],\" !\\n\\nBonjour **\",[\"username\"],\"**,\\n\\nVotre compte a bien été créé. Bienvenue à bord !\"],\"email.welcome.button\":[\"Ouvrir \",[\"site\"]],\"email.welcome.digests\":[\"Chaque semaine, vous recevrez un résumé de ce qui s'est passé ici, et chaque mois un récap des recos les plus validées. Vous pouvez désactiver l'un ou l'autre dans vos paramètres.\"],\"email.welcome.preheader\":[\"Votre compte est prêt.\"],\"email.welcome.reason\":[\"Vous recevez cet e-mail car un compte \",[\"site\"],\" a été créé avec cette adresse.\"],\"email.welcome.subject\":[\"Bienvenue sur \",[\"site\"]],\"en9o7K\":[\"Impossible de publier le commentaire\"],\"etFQQS\":[\"Pourquoi on en voudrait ?\"],\"f3cEDO\":[\"Changer l'image\"],\"fC6mXb\":[\"Valider\"],\"fI-mNw\":[\"Collections\"],\"fgLNSM\":[\"S'inscrire\"],\"fn1ZQX\":[\"Se connecter à \",[\"name\"],\" ?\"],\"gANddk\":[\"Envoi…\"],\"gGx5tM\":[\"Modification\"],\"gIQQwD\":[\"Chargement échoué\"],\"gIpLuy\":[\"Il faut un lien\"],\"gL9KKi\":[\"Qui peut la voir\"],\"gLfZlz\":[\"Ajouter à la collection\"],\"gjJ-sb\":[\"Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion.\"],\"goWEhR\":[\"Piste précédente\"],\"hBuUKa\":[\"Changer le mot de passe…\"],\"hD7w09\":[\"Vous avez tout lu, tout vu, tout bu.\"],\"hJSliC\":[\"<0>\",[\"0\"],\" a publié <1>\",[\"1\"],\"\"],\"hLAe-z\":[\"Retour à la page de connexion\"],\"hT8hgO\":[\"Rétablir le titre suggéré\"],\"hYgDIe\":[\"Créer\"],\"he3ygx\":[\"Copier\"],\"hpCc_Z\":[\"Fermer quand même\"],\"i3uTFh\":[\"Catégories de recos\"],\"i7K_Te\":[\"Qui suis-je ?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Accueil\"],\"ipYn7W\":[\"Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu.\"],\"isRobC\":[\"Nouveau\"],\"jTGs8A\":[\"Ce site n'envoie pas d'e-mails, aucun résumé ne part donc.\"],\"jZNg18\":[\"Compte sur <0>\",[\"host\"],\"\"],\"jbernk\":[\"Chargement du profil…\"],\"jnrIbz\":[\"Validé (\",[\"0\"],\")\"],\"joEmfT\":[\"Serveur inaccessible\"],\"jrZTZl\":[\"Pas encore de recos. Soyez le premier !\"],\"k112DD\":[\"Charger les messages plus anciens\"],\"k36iJ-\":[\"Échec de l’enregistrement de la catégorie\"],\"k4b5_X\":[\"modifié\"],\"kLttbL\":[\"Inscription échouée\"],\"lUanmi\":[\"Vous serez notifié lorsque quelqu'un suit vos collections, valide vos recos ou publie du nouveau contenu.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"lcfvr_\":[\"Supprimer ce commentaire ?\"],\"lpIMne\":[\"Les mots de passe ne correspondent pas\"],\"m9Uf3I\":[\"Les utilisateurs d'une instance approuvée se connectent ici avec leur compte de là-bas. Ceux d'une instance bloquée ne le peuvent pas. Ceux de toute autre instance ont besoin d'une invitation.\"],\"mDJo8W\":[\"Recos (\",[\"0\"],\")\"],\"mbJgSu\":[\"<0>\",[\"0\"],\" a rejoint le site, sur invitation de <1>\",[\"1\"],\"\"],\"mb_Wju\":[\"Déposez un fichier ici, ou collez-en un\"],\"mt6O6E\":[\"C'est un mirage.\"],\"nbm5sI\":[\"Aucune reco ne correspond à « \",[\"q\"],\" ».\"],\"nrjqON\":[\"Vérification de l'invitation…\"],\"nucyoV\":[\"<0>\",[\"0\"],\" a mentionné tout le monde dans <1>\",[\"where\"],\"\"],\"nwtY4N\":[\"Une erreur est survenue\"],\"nx4kaN\":[\"Sauvegarde impossible\"],\"oSrMRT\":[\"Les miennes\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"et \",\"#\",\" reco plus ancienne\"],\"other\":[\"et \",\"#\",\" recos plus anciennes\"]}]],\"oW3yYY\":[\"Ce mois-ci\"],\"ogtYkT\":[\"Mot de passe mis à jour\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" vous a mentionné dans <1>\",[\"where\"],\"\"],\"pSheLH\":[\"Aucun invité pour le moment.\"],\"pd3FCq\":[\"Ce fichier dépasse \",[\"limit\"],\".\"],\"pvnfJD\":[\"Sombre\"],\"q018gJ\":[\"Impossible d'ajouter cette instance\"],\"qIMfNQ\":[\"Supprimer la collection\"],\"qItrqH\":[\"Vous ne recevrez plus le récap mensuel. Vous pouvez le réactiver dans vos paramètres.\"],\"qVdjD5\":[\"Approuvée\"],\"qbDAcy\":[\"Recommander\"],\"qgx_78\":[\"Suivez des collections publiques pour voir leurs recos ici.\"],\"qnUJBI\":[\"Le nouvel ordre n'a pas pu être enregistré. Réessayez.\"],\"qsqt0w\":[\"Lien d'invitation\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Reco\"],\"rCbqPX\":[\"Ce lien d'invitation est manquant, expiré ou déjà utilisé.\"],\"rDMzDU\":[\"Écrivez un message…\"],\"rg9pXu\":[\"Recherche échouée\"],\"rjgjpq\":[\"<0>\",[\"0\"],\" a rejoint le site\"],\"rn6SBY\":[\"Rétablir le son\"],\"sEXJPi\":[\"Lien ou fichier\"],\"sGeXL3\":[\"Miniature\"],\"sQia9P\":[\"Se connecter\"],\"sTiqbm\":[\"invité par\"],\"sdP5Aa\":[\"[supprimé]\"],\"sgX7B-\":[\"Usage unique, valable 7 jours.\"],\"shHs8T\":[\"Saisissez une recherche.\"],\"smeBfS\":[\"Invitation invalide\"],\"tOHgQw\":[\"Continuer en tant que @\",[\"username\"]],\"tXNIqW\":[\"Cette connexion a été commencée ailleurs. Recommencez depuis la page de connexion.\"],\"teaneZ\":[\"image\"],\"tfDRzk\":[\"Enregistrer\"],\"tvmuQ0\":[\"Thème de couleur\"],\"tyiiHe\":[\"Rien de neuf ce mois-ci.\"],\"u1lDX2\":[\"Récupération de l'aperçu…\"],\"u8pttd\":[\"Le changer sur \",[\"home\"]],\"uACFmj\":[\"Se connecter avec \",[\"0\"]],\"uD0qXQ\":[\"Déposez un fichier ici\"],\"uISnsY\":[\"Suppression impossible\"],\"uMGUnV\":[\"Pas encore de collections.\"],\"ub1EEL\":[\"modifié \",[\"0\"]],\"uomLas\":[\"Ajouter une catégorie\"],\"v-Wp--\":[\"Ouvrir la source\"],\"vJBF1r\":[\"Publication…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" non lues)\"],\"vQMkHu\":[\"Retirer le vote\"],\"vbo92d\":[\"<0>En ligne : \",[\"names\"]],\"vuosjb\":[\"Menu utilisateur\"],\"w61W3L\":[\"Retirer \",[\"0\"]],\"wckWOP\":[\"Administration\"],\"wixIgH\":[\"Vous avez déjà un compte ? <0>Se connecter\"],\"wrFDaD\":[\"Les utilisateurs d'ici ne peuvent pas se connecter à une instance bloquée.\"],\"wtxFp4\":[\"Fichier trop volumineux (max \",[\"max\"],\").\"],\"x6tjuK\":[\"Onglet par défaut\"],\"xEWkgZ\":[\"← Retour à toutes les recos\"],\"xGVfLh\":[\"Continuer\"],\"xPHtx0\":[\"Lancer la recherche\"],\"xVuNgt\":[\"+ Nouvelle collection\"],\"xc9O_u\":[\"Supprimer la reco\"],\"y1G_DI\":[\"Renommage impossible\"],\"y6sq5j\":[\"Abonné\"],\"yA_6BX\":[\"Tout voir →\"],\"yBBtRm\":[\"Suivez des utilisateurs pour voir leurs recos ici.\"],\"yByRxz\":[\"Cette semaine\"],\"yQ2kGp\":[\"Charger plus\"],\"y_0uwd\":[\"Hier\"],\"yxlcv5\":[\"Ne plus recevoir le récap mensuel ?\"],\"yz7wBu\":[\"Fermer\"],\"yzF66j\":[\"Lien\"],\"z0ROB3\":[\"Retirer le j'aime\"],\"z1uNN0\":[\"Aucun emoji trouvé.\"],\"zVuxvN\":[\"Actualisation…\"],\"zwBp5t\":[\"Privé\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"-K0AvT\":[\"Déconnecter\"],\"-K9EZb\":[\"Ajouter un e-mail…\"],\"-Ya-b9\":[\"Enregistrement échoué\"],\"-b7T3G\":[\"Mises à jour\"],\"-iDCtG\":[\"Vos paramètres\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Rechercher des recos, utilisateurs, collections…\"],\"1QfxQT\":[\"Fermer\"],\"1YKhJ3\":[\"1 nouveau message\"],\"1njn7W\":[\"Clair\"],\"1utXA6\":[\"Recos\"],\"26iNma\":[\"Publier le commentaire\"],\"2DoBvq\":[\"Flux\"],\"2Hlmdt\":[\"Écrire une réponse…\"],\"2_DHgA\":[\"Identifiant d’URL\"],\"2nrsEn\":[\"message supprimé\"],\"2rwzb1\":[\"Vous ne vous êtes encore connecté à aucun autre gerbeur avec ce compte.\"],\"2wxgft\":[\"Renommer\"],\"2ygf_L\":[\"← Retour\"],\"32TndD\":[\"Bloquée\"],\"3ESfuy\":[\"Ajouter une description…\"],\"3KKSM4\":[\"privé\"],\"3QuNUr\":[\"Plusieurs personnes écrivent…\"],\"3SEwY1\":[\"Supprimer la catégorie \\\"\",[\"0\"],\"\\\" ? Cette action est irréversible.\"],\"3qkggm\":[\"Plein écran\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" a suivi votre collection <1>\",[\"1\"],\"\"],\"48e2R8\":[\"Modifier l'e-mail\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Recommandé !\"],\"4GKuCs\":[\"Connexion échouée\"],\"4HH9iB\":[\"Aucun abonnement pour le moment.\"],\"4OjqAQ\":[\"Continuer l'édition\"],\"4RtQ1k\":[\"Ne plus suivre \",[\"targetUsername\"]],\"4c-qBx\":[\"Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter.\"],\"4yj9xV\":[\"Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…\"],\"5KHDCi\":[[\"0\"],\" et \",[\"1\"],\" sont en train d'écrire…\"],\"5NZ3tg\":[\"Désactivation…\"],\"5TviPn\":[\"Annuler la recherche\"],\"5WRdOd\":[\"<0>En tête cette semaine : <1>\",[\"0\"],\"\"],\"5cC8f2\":[\"Modifié le \",[\"0\"]],\"5oD9f_\":[\"Plus tôt\"],\"6BV5h6\":[\"Cette reco a été supprimée, ou rendue privée.\"],\"6C4kKS\":[\"Autres instances\"],\"6Qly-0\":[\"un commentaire\"],\"6YE0P6\":[\"Édition de \",[\"0\"]],\"6YtxFj\":[\"Nom\"],\"6_dCYd\":[\"Aperçu\"],\"6gRgw8\":[\"Réessayer\"],\"7PHCIN\":[\"Annuler la suppression\"],\"7PzzBU\":[\"Utilisateur\"],\"7d1a0d\":[\"Public\"],\"7noMWp\":[\"Rien de neuf cette semaine.\"],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8-8abB\":[\"Déconnecté — reconnexion…\"],\"8F1i42\":[\"Page introuvable\"],\"8JplJJ\":[\"Désabonné\"],\"8TgExA\":[\"<0>\",[\"0\"],\" a créé la playlist <1>\",[\"1\"],\"\"],\"8Ug9jB\":[\"Connexe\"],\"8ZsakT\":[\"Mot de passe\"],\"8rPWOT\":[\"Connexion impossible\"],\"92o1-U\":[\"Suppression échouée\"],\"9BruTc\":[\"Pourquoi ?\"],\"9eEYEq\":[\"Ce lien de connexion est incomplet.\"],\"9l4qcT\":[\"Déposez un fichier de remplacement ici\"],\"9uI_rE\":[\"Annuler\"],\"A0y396\":[\"+ Inviter quelqu'un\"],\"A1taO8\":[\"Rechercher\"],\"AEfu-D\":[\"Dans les collections\"],\"AHZflp\":[\"Tribune\"],\"AQbgNR\":[\"Suivre \",[\"targetUsername\"]],\"AS7WoE\":[\"Repartir de zéro\"],\"ATGYL1\":[\"Adresse e-mail\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"Ade-6d\":[\"Mises à jour en direct indisponibles.\"],\"AeXO77\":[\"Compte\"],\"AhNP40\":[\"Position\"],\"AnRu_j\":[\"Annuler la réponse\"],\"AwUsnG\":[\"Instances\"],\"B4LeYP\":[\"Échec de la suppression (\",[\"0\"],\")\"],\"BKVDJg\":[\"créée \",[\"ago\"]],\"BjitBo\":[\"Télécharger \",[\"0\"]],\"BlpJ2G\":[\"Reprise de votre brouillon.\"],\"CI50ct\":[\"Validé\"],\"CJkEfx\":[\"Bloquer\"],\"CL-DkX\":[\"URL d'une instance\"],\"C_qQE4\":[\"Déjà recommandé par \",[\"0\"],\" \",[\"1\"]],\"Cf_gxi\":[\"Ne plus recevoir le résumé hebdomadaire ?\"],\"Cj24wt\":[\"Inscription…\"],\"ClFfRL\":[\"Catégories de collections\"],\"CnqayG\":[\"La mise à jour a échoué. Réessayez.\"],\"Czya-u\":[\"Les utilisateurs d'une instance approuvée se connectent ici avec leur compte de là-bas. Ceux d'une instance bloquée ne le peuvent pas, et les utilisateurs d'ici ne peuvent pas s'y connecter. Ceux de toute autre instance ont besoin d'une invitation.\"],\"D8DRYV\":[\"Supprimer ce message ?\"],\"DI3NBO\":[\"Ouvrir sur \",[\"0\"]],\"DPfwMq\":[\"Terminé\"],\"DdeHXH\":[\"Supprimer cette reco ? Cette action est irréversible.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" a validé <1>\",[\"1\"],\"\"],\"E417RG\":[\"Vous ne recevrez plus le résumé hebdomadaire. Vous pouvez le réactiver dans vos paramètres.\"],\"EBU6O6\":[\"Connexion perdue — ce message n'a peut-être pas été envoyé\"],\"ECiS12\":[\"Voir la reco →\"],\"ED8n0z\":[\"la tribune\"],\"EarrCe\":[\"Rechercher un emoji\"],\"Evcm2Z\":[\"Fermer sans publier ? Le fichier joint sera perdu — ce que vous avez écrit sera conservé.\"],\"EwVrWF\":[\"Le dump n'a pas pu être retiré. Réessayez.\"],\"ExR0Fr\":[\"L'URL est obligatoire.\"],\"F5Js1v\":[\"Ne plus suivre la collection\"],\"FdTlEG\":[\"Piste \",[\"0\"]],\"FgAxTj\":[\"Se déconnecter\"],\"Fxf4jq\":[\"Description (facultatif)\"],\"G8OFDZ\":[\"<0>Cette semaine : <1>\",[\"figures\"],\"\"],\"GCWCCo\":[\"Fermer le lecteur\"],\"GDvlUT\":[\"Rôle\"],\"GLukaH\":[\"Modérateur\"],\"GNSsCc\":[\"Impossible de créer la collection\"],\"GQ38dl\":[[\"unreadChatCount\"],\" messages non lus\"],\"GY6uSc\":[\"Suivant →\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" caractères : lettres, chiffres ou tirets bas\"],\"GptGxg\":[\"Changer le mot de passe\"],\"Gv5nA5\":[\"Format de réponse inattendu\"],\"H4MGkI\":[\"Agrandir le lecteur\"],\"H4o4sk\":[\"Personne pour le moment.\"],\"H5X-EM\":[\"Modifier la note\"],\"H8pzW-\":[\"Impossible de mettre à jour l'avatar\"],\"HTLDA4\":[\"Chargement…\"],\"H_PnT8\":[\"Insérer un emoji\"],\"I-x669\":[\"Invités\"],\"IF7RiR\":[\"Couverture\"],\"IO9KL_\":[\"Piste suivante\"],\"IZX7TO\":[\"Impossible de générer une invitation\"],\"IagCbF\":[\"URL\"],\"IgrLD_\":[\"Pause\"],\"IhK90M\":[\"Lettres, chiffres et tirets bas seulement\"],\"ImOQa9\":[\"Répondre\"],\"JJ-Bhk\":[\"Votre adresse e-mail\"],\"JLasL-\":[\"Désabonnement impossible\"],\"JRQitQ\":[\"Confirmer le nouveau mot de passe\"],\"JXr41k\":[\"<0>\",[\"0\"],\" a commenté sur <1>\",[\"1\"],\"\"],\"Jd58Fo\":[\"Tendances\"],\"Jf0PuK\":[\"Aller à la connexion\"],\"JlFcis\":[\"Envoyer\"],\"JrFTcr\":[\"Connexion…\"],\"K7tIrx\":[\"Catégorie\"],\"KDGWg5\":[\"Retirer de la collection\"],\"KDw4GX\":[\"Réessayer\"],\"KKZxBU\":[\"Envoi \",[\"0\"],\" %\"],\"K_F6pa\":[\"Enregistrement…\"],\"KtYF-t\":[\"Sons\"],\"L-rMC9\":[\"Réinitialiser par défaut\"],\"L7_rsx\":[\"Envoi de l'image…\"],\"MHrjPM\":[\"Titre\"],\"MKEPCY\":[\"Suivre\"],\"MTb2Ot\":[\"Aucun message pour l'instant. Dites bonjour !\"],\"MbkoRn\":[\"Ou rejoignez-nous avec votre compte d'un autre gerbeur\"],\"Mj6oCJ\":[[\"0\"],\" est en train d'écrire…\"],\"MrIc3x\":[\"Ou connectez-vous avec votre compte d'un autre gerbeur\"],\"N40H-G\":[\"Tout\"],\"N6j2JH\":[\"Modifier \",[\"0\"]],\"NUrY9o\":[\"Catégories\"],\"Nu4oKW\":[\"Description\"],\"O32mmL\":[\"Modifier la description\"],\"O3oNi5\":[\"E-mail\"],\"Oprv1v\":[\"Mot de passe (min. \",[\"0\"],\" caractères)\"],\"P5MuiB\":[\"Ajouter une image\"],\"PJTy3N\":[[\"totalUnread\",\"plural\",{\"one\":[\"#\",\" nouvelle\"],\"other\":[\"#\",\" nouvelles\"]}]],\"PN3pBl\":[\"Pas de réponse du serveur — ce message n'a peut-être pas été envoyé\"],\"PUe8Yk\":[\"Réduire le lecteur\"],\"PiH3UR\":[\"Copié !\"],\"Pn2B7_\":[\"Mot de passe actuel\"],\"Pwqkdw\":[\"Chargement…\"],\"Q4WUjR\":[\"Rechercher un emoji…\"],\"Q6n4F4\":[\"Actualiser les métadonnées\"],\"QGaQRo\":[\"Erreur lors de la mise à jour du rôle\"],\"QKsaQr\":[\"ou <0>parcourir les fichiers\"],\"QLtPBd\":[\"Aucune reco dans cette collection pour l'instant.\"],\"QctOgc\":[\"Impossible de joindre le serveur. Vérifiez votre connexion et réessayez.\"],\"R7SsBE\":[\"Couper le son\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Supprimer cette collection ? Cette action est irréversible.\"],\"RLe7Vk\":[\"Vérification…\"],\"RTksSy\":[\"<0>\",[\"0\"],\" a commencé à vous suivre\"],\"RaKjrM\":[\"Impossible d'enregistrer la modification\"],\"RcUHRT\":[\"Suivi\"],\"RfwZxd\":[\"Réinitialiser le mot de passe\"],\"Rrp6-J\":[\"Se connecter pour aimer\"],\"RvVJBH\":[\"Actualisation du flux…\"],\"S1xV5B\":[[\"maxLength\"],\" caractères au maximum.\"],\"SBTElJ\":[\"Recherche…\"],\"SLaP1b\":[\"Compte d'ailleurs\"],\"SMaFdc\":[\"Se désabonner\"],\"Sad2tK\":[\"Envoi…\"],\"So7gCD\":[\"Collez un lien…\"],\"StovX6\":[\"Rien à voir, circulez.\"],\"Sxm8rQ\":[\"Utilisateurs\"],\"T8ejtZ\":[\"Ce commentaire n'a pas pu être supprimé. Réessayez.\"],\"T91vKp\":[\"Lecture\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" a été ajouté à <1>\",[\"1\"],\"\"],\"TAkYGZ\":[\"Votre compte est sur \",[\"home\"],\" : connectez-vous aux autres instances depuis là-bas.\"],\"TAtbho\":[\"Chargement des messages…\"],\"TN382O\":[\"Lien invalide\"],\"ToADL_\":[\"<0>\",[\"name\"],\" (\",[\"host\"],\") demande à vous connecter avec votre compte d'ici, <1>@\",[\"username\"],\". Il recevra votre nom d'utilisateur, votre adresse e-mail et votre avatar.\"],\"Tv9vbB\":[\"Suivre la collection\"],\"Tz0i8g\":[\"Paramètres\"],\"U3pytU\":[\"Administrateur\"],\"UNMVei\":[\"Mot de passe oublié ?\"],\"UOV0wi\":[\"Connexion…\"],\"UOZith\":[\"Publication échouée\"],\"URAieT\":[\"Se connecter pour valider\"],\"V38P77\":[\"Seuls son auteur et les modérateurs peuvent modifier ce dump.\"],\"VCoEm-\":[\"Retour à la connexion\"],\"VKsaTi\":[\"En réponse à\"],\"VLAd8f\":[\"Max \",[\"limit\"]],\"VLINRu\":[\"URL de votre gerbeur\"],\"V_e7nf\":[\"Définir un nouveau mot de passe\"],\"VnNJbN\":[\"De collections\"],\"VskHIx\":[\"Supprimer la catégorie\"],\"VyTYmS\":[\"Changer l'avatar\"],\"W5A0Ly\":[\"Une erreur inattendue s'est produite.\"],\"W9FRBT\":[\"Aimer\"],\"WAcnnR\":[\"L'image n'a pas pu être envoyée.\"],\"WE_r9K\":[\"Échec de la création de la catégorie\"],\"WMw63e\":[\"Impossible de joindre cette instance\"],\"WfffrI\":[\"identifiant\"],\"WhimMi\":[\"Échec de la réinitialisation\"],\"WpXcBJ\":[\"Rien ici pour l'instant.\"],\"XHqhoS\":[\"Impossible de charger le chat.\"],\"XJVYo8\":[\"Pourquoi & où\"],\"XJy2oN\":[\"Connexion…\"],\"XWbTfu\":[\"utilisée \",[\"0\"]],\"Xan6QP\":[\"Nouvelle reco\"],\"XgRtUf\":[\"Impossible de changer le mot de passe\"],\"XnL-Eu\":[\"Aucun utilisateur ne correspond à « \",[\"q\"],\" ».\"],\"YK1Dhc\":[\"une publication\"],\"YWnSuq\":[\"Ce lien de désabonnement est absent ou mal formé.\"],\"YZxXK8\":[\"Fermer le sélecteur d'emoji\"],\"YcjhhX\":[\"Gestion utilisateur\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" a aimé votre commentaire sur <1>\",[\"1\"],\"\"],\"YpkCca\":[\"Pas encore de collections suivies.\"],\"Z5HWHd\":[\"Activés\"],\"ZBdbv9\":[\"Modifier le titre\"],\"ZCpU0u\":[\"Aucune collection ne correspond à « \",[\"q\"],\" ».\"],\"ZWxjuD\":[\"Certaines images n'ont pas pu être envoyées.\"],\"ZmD2o6\":[\"Créer et ajouter\"],\"_0oJwI\":[\"Approuver\"],\"_3O5R_\":[\"Échec de la demande\"],\"_DneUO\":[\"mise à jour \",[\"ago\"]],\"_DwR-n\":[\"Création…\"],\"_I1_HN\":[\"Minuscules, chiffres et tirets simples seulement\"],\"_R_sGB\":[\"Mot de passe modifié avec succès.\"],\"_aept4\":[\"Publier la réponse\"],\"_cF7Rs\":[\"Volume\"],\"_eZzBf\":[\"Se connecter ailleurs\"],\"_karOe\":[\"Hors ligne — message non envoyé\"],\"_nT6AE\":[\"Nouveau mot de passe\"],\"_t4W-i\":[\"De personnes\"],\"aAIQg2\":[\"Apparence\"],\"aDvLhk\":[\"Ajouter un commentaire…\"],\"aFOsPT\":[\"Les plus fournies\"],\"az8lvo\":[\"Désactivés\"],\"b3Thhd\":[\"Envoi échoué\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"bQhwn-\":[\"Chargement de la collection…\"],\"b_1P1D\":[\"Renommage…\"],\"b_VouM\":[\"Votre compte est sur \",[\"home\"],\" : votre mot de passe, votre adresse e-mail, votre avatar et votre nom d'utilisateur se changent là-bas.\"],\"bzSI52\":[\"Abandonner\"],\"cILfnJ\":[\"Supprimer le fichier\"],\"cNu4my\":[[\"0\"],\" de plus en ligne : \",[\"restNames\"]],\"cYP9Sb\":[\"+ Collection\"],\"cbeBbZ\":[\"Au moins \",[\"0\"],\" caractères\"],\"cnGeoo\":[\"Supprimer\"],\"dAs22m\":[\"Remplacer le fichier\"],\"dEgA5A\":[\"Annuler\"],\"dMizp8\":[\"Nouvelle collection\"],\"dau1Pl\":[[\"unseenCount\"],\" nouveaux messages\"],\"digest.community\":[\"Communauté\"],\"digest.count.comments\":[[\"n\",\"plural\",{\"one\":[\"commentaire\"],\"other\":[\"commentaires\"]}]],\"digest.count.upvotes\":[[\"n\",\"plural\",{\"one\":[\"vote\"],\"other\":[\"votes\"]}]],\"digest.invitedBy\":[\"sur invitation de @\",[\"username\"]],\"digest.invitedByYou\":[\"sur votre invitation\"],\"digest.monthly\":[\"Récap mensuel\"],\"digest.monthly.off\":[\"Ne plus recevoir le récap mensuel\"],\"digest.newMembers\":[\"Nouveaux membres\"],\"digest.newPlaylists\":[\"Nouvelles collections publiques\"],\"digest.stat.chat\":[[\"n\",\"plural\",{\"one\":[\"message dans la tribune\"],\"other\":[\"messages dans la tribune\"]}]],\"digest.stat.dumps\":[[\"n\",\"plural\",{\"one\":[\"reco\"],\"other\":[\"recos\"]}]],\"digest.topMonth\":[\"Les plus validées du mois\"],\"digest.topWeek\":[\"Les plus validées de la semaine\"],\"digest.weekly\":[\"Résumé hebdomadaire\"],\"digest.weekly.off\":[\"Ne plus recevoir le résumé hebdomadaire\"],\"dqwCfR\":[\"Impossible de charger un aperçu pour ce lien — vérifiez-le, ou publiez-le tel quel et actualisez l'aperçu plus tard.\"],\"dznB-Y\":[\"Seules les images peuvent être déposées ici.\"],\"eFSqvc\":[\"Impossible de publier la réponse\"],\"eIEHHV\":[\"Changer la couleur de peau\"],\"eOfXq3\":[\"Un titre est requis.\"],\"ePK91l\":[\"Modifier\"],\"eaUTwS\":[\"Envoyer le lien de réinitialisation\"],\"ecUA8p\":[\"Aujourd'hui\"],\"ef9nPf\":[\"Chargement de la reco…\"],\"eloqXQ\":[\"Vous pouvez la publier quand même.\"],\"email.digest.addedTo\":[\"ajoutée à \",[\"playlist\"]],\"email.digest.andMore\":[\"et \",[\"n\"],\" de plus\"],\"email.digest.by\":[\"par @\",[\"username\"]],\"email.digest.chatWeek\":[[\"n\",\"plural\",{\"one\":[\"#\",\" message cette semaine\"],\"other\":[\"#\",\" messages cette semaine\"]}]],\"email.digest.communityPage\":[\"Tout voir sur la page de la communauté\"],\"email.digest.count.followers\":[[\"n\",\"plural\",{\"one\":[\"nouvel abonné\"],\"other\":[\"nouveaux abonnés\"]}]],\"email.digest.count.likes\":[[\"n\",\"plural\",{\"one\":[\"j'aime sur vos commentaires\"],\"other\":[\"j'aime sur vos commentaires\"]}]],\"email.digest.count.mentions\":[[\"n\",\"plural\",{\"one\":[\"mention\"],\"other\":[\"mentions\"]}]],\"email.digest.count.playlistFollows\":[[\"n\",\"plural\",{\"one\":[\"abonnement à vos collections\"],\"other\":[\"abonnements à vos collections\"]}]],\"email.digest.forYou\":[\"Pour vous\"],\"email.digest.hl.comment\":[[\"who\"],\" a commenté votre reco \",[\"target\"]],\"email.digest.hl.commentLike\":[[\"who\"],\" a aimé votre commentaire sur \",[\"target\"]],\"email.digest.hl.mentionComment\":[[\"who\"],\" vous a mentionné dans un commentaire sur \",[\"target\"]],\"email.digest.hl.mentionEveryone\":[[\"who\"],\" a mentionné tout le monde dans \",[\"target\"]],\"email.digest.hl.mentionIn\":[[\"who\"],\" vous a mentionné dans \",[\"target\"]],\"email.digest.hl.mentionPlaylist\":[[\"who\"],\" vous a mentionné dans la collection \",[\"target\"]],\"email.digest.hl.playlistFollow\":[[\"who\"],\" a suivi votre collection \",[\"target\"]],\"email.digest.hl.userFollow\":[[\"who\"],\" vous suit désormais\"],\"email.digest.inChat\":[\"Dans la tribune\"],\"email.digest.monthly.reason\":[\"Vous recevez ce récap mensuel car il est activé dans vos paramètres.\"],\"email.digest.monthly.subject\":[\"Le mois sur \",[\"site\"],\", \",[\"range\"]],\"email.digest.monthly.title\":[\"Le mois sur \",[\"site\"]],\"email.digest.more\":[[\"n\"],\" de plus dans votre fil Suivi\"],\"email.digest.network\":[\"De ceux que vous suivez\"],\"email.digest.openChat\":[\"Ouvrir la tribune\"],\"email.digest.playlistMeta\":[\"par @\",[\"username\"],\" · \",[\"n\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"email.digest.pre.members\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouveau membre\"],\"other\":[\"#\",\" nouveaux membres\"]}]],\"email.digest.pre.network\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouvelle reco de ceux que vous suivez\"],\"other\":[\"#\",\" nouvelles recos de ceux que vous suivez\"]}]],\"email.digest.pre.playlists\":[[\"n\",\"plural\",{\"one\":[\"#\",\" nouvelle collection\"],\"other\":[\"#\",\" nouvelles collections\"]}]],\"email.digest.pre.topMonth\":[[\"n\",\"plural\",{\"one\":[\"la reco la plus validée du mois\"],\"other\":[\"les \",\"#\",\" recos les plus validées du mois\"]}]],\"email.digest.pre.topWeek\":[[\"n\",\"plural\",{\"one\":[\"la reco la plus validée de la semaine\"],\"other\":[\"les \",\"#\",\" recos les plus validées de la semaine\"]}]],\"email.digest.seeAll\":[[\"n\",\"plural\",{\"one\":[\"Voir la notification\"],\"other\":[\"Voir les \",\"#\",\" notifications\"]}]],\"email.digest.theChat\":[\"la tribune\"],\"email.digest.totals\":[[\"dumps\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}],\", \",[\"upvotes\",\"plural\",{\"one\":[\"#\",\" vote\"],\"other\":[\"#\",\" votes\"]}],\" et \",[\"comments\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}],\".\"],\"email.digest.upload\":[\"fichier\"],\"email.digest.weekly.reason\":[\"Vous recevez ce résumé hebdomadaire car il est activé dans vos paramètres.\"],\"email.digest.weekly.subject\":[\"Votre semaine sur \",[\"site\"],\", \",[\"range\"]],\"email.digest.weekly.title\":[\"Votre semaine, \",[\"username\"]],\"email.digest.yours\":[\"à vous\"],\"email.reset.body\":[\"Quelqu'un a demandé à réinitialiser le mot de passe de votre compte \",[\"site\"],\". Le bouton ci-dessous vous permet d'en choisir un nouveau. Il ne fonctionne qu'une fois, pendant \",[\"hours\",\"plural\",{\"one\":[\"#\",\" heure\"],\"other\":[\"#\",\" heures\"]}],\".\"],\"email.reset.button\":[\"Choisir un nouveau mot de passe\"],\"email.reset.fallback\":[\"Si le bouton ne fonctionne pas, collez ce lien dans votre navigateur :\"],\"email.reset.hi\":[\"Bonjour \",[\"username\"],\",\"],\"email.reset.ignore\":[\"Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail. Votre mot de passe ne change pas.\"],\"email.reset.preheader\":[\"Le lien ne fonctionne qu'une fois, pendant \",[\"hours\",\"plural\",{\"one\":[\"#\",\" heure\"],\"other\":[\"#\",\" heures\"]}],\".\"],\"email.reset.reason\":[\"Vous recevez cet e-mail car une réinitialisation de mot de passe a été demandée pour cette adresse.\"],\"email.reset.subject\":[\"Réinitialisez votre mot de passe \",[\"site\"]],\"email.reset.title\":[\"Réinitialiser votre mot de passe\"],\"email.settings\":[\"Paramètres\"],\"email.welcome.body\":[\"# Bienvenue sur \",[\"site\"],\" !\\n\\nBonjour **\",[\"username\"],\"**,\\n\\nVotre compte a bien été créé. Bienvenue à bord !\"],\"email.welcome.button\":[\"Ouvrir \",[\"site\"]],\"email.welcome.digests\":[\"Chaque semaine, vous recevrez un résumé de ce qui s'est passé ici, et chaque mois un récap des recos les plus validées. Vous pouvez désactiver l'un ou l'autre dans vos paramètres.\"],\"email.welcome.preheader\":[\"Votre compte est prêt.\"],\"email.welcome.reason\":[\"Vous recevez cet e-mail car un compte \",[\"site\"],\" a été créé avec cette adresse.\"],\"email.welcome.subject\":[\"Bienvenue sur \",[\"site\"]],\"en9o7K\":[\"Impossible de publier le commentaire\"],\"etFQQS\":[\"Pourquoi on en voudrait ?\"],\"f3cEDO\":[\"Changer l'image\"],\"fC6mXb\":[\"Valider\"],\"fI-mNw\":[\"Collections\"],\"fgLNSM\":[\"S'inscrire\"],\"fn1ZQX\":[\"Se connecter à \",[\"name\"],\" ?\"],\"gANddk\":[\"Envoi…\"],\"gGx5tM\":[\"Modification\"],\"gIQQwD\":[\"Chargement échoué\"],\"gIpLuy\":[\"Il faut un lien\"],\"gL9KKi\":[\"Qui peut la voir\"],\"gLfZlz\":[\"Ajouter à la collection\"],\"gjJ-sb\":[\"Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion.\"],\"goWEhR\":[\"Piste précédente\"],\"hBuUKa\":[\"Changer le mot de passe…\"],\"hD7w09\":[\"Vous avez tout lu, tout vu, tout bu.\"],\"hJSliC\":[\"<0>\",[\"0\"],\" a publié <1>\",[\"1\"],\"\"],\"hLAe-z\":[\"Retour à la page de connexion\"],\"hT8hgO\":[\"Rétablir le titre suggéré\"],\"hYgDIe\":[\"Créer\"],\"he3ygx\":[\"Copier\"],\"hpCc_Z\":[\"Fermer quand même\"],\"i3uTFh\":[\"Catégories de recos\"],\"i7K_Te\":[\"Qui suis-je ?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Accueil\"],\"ipYn7W\":[\"Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu.\"],\"isRobC\":[\"Nouveau\"],\"jTGs8A\":[\"Ce site n'envoie pas d'e-mails, aucun résumé ne part donc.\"],\"jZNg18\":[\"Compte sur <0>\",[\"host\"],\"\"],\"jbernk\":[\"Chargement du profil…\"],\"jnrIbz\":[\"Validé (\",[\"0\"],\")\"],\"joEmfT\":[\"Serveur inaccessible\"],\"jrZTZl\":[\"Pas encore de recos. Soyez le premier !\"],\"k112DD\":[\"Charger les messages plus anciens\"],\"k36iJ-\":[\"Échec de l’enregistrement de la catégorie\"],\"k4b5_X\":[\"modifié\"],\"kLttbL\":[\"Inscription échouée\"],\"lUanmi\":[\"Vous serez notifié lorsque quelqu'un suit vos collections, valide vos recos ou publie du nouveau contenu.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"lcfvr_\":[\"Supprimer ce commentaire ?\"],\"lpIMne\":[\"Les mots de passe ne correspondent pas\"],\"m9Uf3I\":[\"Les utilisateurs d'une instance approuvée se connectent ici avec leur compte de là-bas. Ceux d'une instance bloquée ne le peuvent pas. Ceux de toute autre instance ont besoin d'une invitation.\"],\"mDJo8W\":[\"Recos (\",[\"0\"],\")\"],\"mbJgSu\":[\"<0>\",[\"0\"],\" a rejoint le site, sur invitation de <1>\",[\"1\"],\"\"],\"mb_Wju\":[\"Déposez un fichier ici, ou collez-en un\"],\"mt6O6E\":[\"C'est un mirage.\"],\"nbm5sI\":[\"Aucune reco ne correspond à « \",[\"q\"],\" ».\"],\"nrjqON\":[\"Vérification de l'invitation…\"],\"nucyoV\":[\"<0>\",[\"0\"],\" a mentionné tout le monde dans <1>\",[\"where\"],\"\"],\"nwtY4N\":[\"Une erreur est survenue\"],\"nx4kaN\":[\"Sauvegarde impossible\"],\"oSrMRT\":[\"Les miennes\"],\"oTYvvN\":[[\"0\",\"plural\",{\"one\":[\"et \",\"#\",\" reco plus ancienne\"],\"other\":[\"et \",\"#\",\" recos plus anciennes\"]}]],\"oW3yYY\":[\"Ce mois-ci\"],\"ogtYkT\":[\"Mot de passe mis à jour\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" vous a mentionné dans <1>\",[\"where\"],\"\"],\"pSheLH\":[\"Aucun invité pour le moment.\"],\"pd3FCq\":[\"Ce fichier dépasse \",[\"limit\"],\".\"],\"pvnfJD\":[\"Sombre\"],\"q018gJ\":[\"Impossible d'ajouter cette instance\"],\"qIMfNQ\":[\"Supprimer la collection\"],\"qItrqH\":[\"Vous ne recevrez plus le récap mensuel. Vous pouvez le réactiver dans vos paramètres.\"],\"qVdjD5\":[\"Approuvée\"],\"qbDAcy\":[\"Recommander\"],\"qgx_78\":[\"Suivez des collections publiques pour voir leurs recos ici.\"],\"qnUJBI\":[\"Le nouvel ordre n'a pas pu être enregistré. Réessayez.\"],\"qsqt0w\":[\"Lien d'invitation\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Reco\"],\"rCbqPX\":[\"Ce lien d'invitation est manquant, expiré ou déjà utilisé.\"],\"rDMzDU\":[\"Écrivez un message…\"],\"rg9pXu\":[\"Recherche échouée\"],\"rjgjpq\":[\"<0>\",[\"0\"],\" a rejoint le site\"],\"rn6SBY\":[\"Rétablir le son\"],\"sEXJPi\":[\"Lien ou fichier\"],\"sGeXL3\":[\"Miniature\"],\"sQia9P\":[\"Se connecter\"],\"sTiqbm\":[\"invité par\"],\"sdP5Aa\":[\"[supprimé]\"],\"sgX7B-\":[\"Usage unique, valable 7 jours.\"],\"shHs8T\":[\"Saisissez une recherche.\"],\"smeBfS\":[\"Invitation invalide\"],\"tOHgQw\":[\"Continuer en tant que @\",[\"username\"]],\"tXNIqW\":[\"Cette connexion a été commencée ailleurs. Recommencez depuis la page de connexion.\"],\"teaneZ\":[\"image\"],\"tfDRzk\":[\"Enregistrer\"],\"tvmuQ0\":[\"Thème de couleur\"],\"tyiiHe\":[\"Rien de neuf ce mois-ci.\"],\"u1lDX2\":[\"Récupération de l'aperçu…\"],\"u8pttd\":[\"Le changer sur \",[\"home\"]],\"uACFmj\":[\"Se connecter avec \",[\"0\"]],\"uD0qXQ\":[\"Déposez un fichier ici\"],\"uISnsY\":[\"Suppression impossible\"],\"uMGUnV\":[\"Pas encore de collections.\"],\"ub1EEL\":[\"modifié \",[\"0\"]],\"uomLas\":[\"Ajouter une catégorie\"],\"v-Wp--\":[\"Ouvrir la source\"],\"vJBF1r\":[\"Publication…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" non lues)\"],\"vQMkHu\":[\"Retirer le vote\"],\"vbo92d\":[\"<0>En ligne : \",[\"names\"]],\"vuosjb\":[\"Menu utilisateur\"],\"w61W3L\":[\"Retirer \",[\"0\"]],\"wckWOP\":[\"Administration\"],\"wixIgH\":[\"Vous avez déjà un compte ? <0>Se connecter\"],\"wrFDaD\":[\"Les utilisateurs d'ici ne peuvent pas se connecter à une instance bloquée.\"],\"wtxFp4\":[\"Fichier trop volumineux (max \",[\"max\"],\").\"],\"x6tjuK\":[\"Onglet par défaut\"],\"xEWkgZ\":[\"← Retour à toutes les recos\"],\"xGVfLh\":[\"Continuer\"],\"xPHtx0\":[\"Lancer la recherche\"],\"xVuNgt\":[\"+ Nouvelle collection\"],\"xc9O_u\":[\"Supprimer la reco\"],\"y1G_DI\":[\"Renommage impossible\"],\"y6sq5j\":[\"Abonné\"],\"yA_6BX\":[\"Tout voir →\"],\"yBBtRm\":[\"Suivez des utilisateurs pour voir leurs recos ici.\"],\"yByRxz\":[\"Cette semaine\"],\"yQ2kGp\":[\"Charger plus\"],\"y_0uwd\":[\"Hier\"],\"yxlcv5\":[\"Ne plus recevoir le récap mensuel ?\"],\"yz7wBu\":[\"Fermer\"],\"yzF66j\":[\"Lien\"],\"z0ROB3\":[\"Retirer le j'aime\"],\"z1uNN0\":[\"Aucun emoji trouvé.\"],\"zVuxvN\":[\"Actualisation…\"],\"zwBp5t\":[\"Privé\"]}"); \ No newline at end of file diff --git a/locales/fr.po b/locales/fr.po index 0ac11108..98b539af 100644 --- a/locales/fr.po +++ b/locales/fr.po @@ -206,14 +206,14 @@ msgstr "# Bienvenue sur {site} !\n" "Votre compte a bien été créé. Bienvenue à bord !" #: src/components/dump/create/DumpCreateModal.tsx:736 -#: src/pages/PlaylistDetail.tsx:98 +#: src/pages/PlaylistDetail.tsx:99 #: src/pages/UserPublicProfile.tsx:129 msgid "← Back" msgstr "← Retour" -#: src/pages/Dump.tsx:375 -#: src/pages/Dump.tsx:400 -#: src/pages/Dump.tsx:627 +#: src/pages/Dump.tsx:374 +#: src/pages/Dump.tsx:399 +#: src/pages/Dump.tsx:560 #: src/pages/DumpEdit.tsx:234 msgid "← Back to all dumps" msgstr "← Retour à toutes les recos" @@ -226,7 +226,7 @@ msgstr "+ Inviter quelqu'un" msgid "+ New playlist" msgstr "+ Nouvelle collection" -#: src/pages/Dump.tsx:474 +#: src/pages/Dump.tsx:451 msgid "+ Playlist" msgstr "+ Collection" @@ -252,7 +252,7 @@ msgid "<0>{0} followed your playlist <1>{1}" msgstr "<0>{0} a suivi votre collection <1>{1}" #. placeholder {0}: event.username -#: src/components/social/CommunityFeed.tsx:144 +#: src/components/social/CommunityFeed.tsx:145 msgid "<0>{0} joined" msgstr "<0>{0} a rejoint le site" @@ -311,16 +311,16 @@ msgstr "<0>{0} a été ajouté à <1>{1}" msgid "<0>{name} ({host}) asks to sign you in with your account here, <1>@{username}. It will receive your username, your email address and your avatar." msgstr "<0>{name} ({host}) demande à vous connecter avec votre compte d'ici, <1>@{username}. Il recevra votre nom d'utilisateur, votre adresse e-mail et votre avatar." -#: src/components/social/CommunityFeed.tsx:155 +#: src/components/social/CommunityFeed.tsx:159 msgid "<0>Online now: {names}" msgstr "<0>En ligne : {names}" -#: src/components/social/CommunityFeed.tsx:162 +#: src/components/social/CommunityFeed.tsx:166 msgid "<0>This week: <1>{figures}" msgstr "<0>Cette semaine : <1>{figures}" #. placeholder {0}: top.title -#: src/components/social/CommunityFeed.tsx:170 +#: src/components/social/CommunityFeed.tsx:174 msgid "<0>Top this week: <1>{0}" msgstr "<0>En tête cette semaine : <1>{0}" @@ -344,7 +344,7 @@ msgstr "une publication" msgid "Account" msgstr "Compte" -#: src/components/profile/ProfileHeader.tsx:144 +#: src/components/profile/ProfileHeader.tsx:151 msgid "Account on <0>{host}" msgstr "Compte sur <0>{host}" @@ -352,12 +352,17 @@ msgstr "Compte sur <0>{host}" msgid "Add a comment…" msgstr "Ajouter un commentaire…" +#: src/components/playlist/PlaylistHeader.tsx:117 +#: src/components/playlist/PlaylistHeader.tsx:128 +msgid "Add a description…" +msgstr "Ajouter une description…" + #: src/components/category/CategoryManager.tsx:115 #: src/components/category/CategoryManager.tsx:116 msgid "Add category" msgstr "Ajouter une catégorie" -#: src/components/profile/ProfileHeader.tsx:173 +#: src/components/profile/ProfileHeader.tsx:168 msgid "Add email…" msgstr "Ajouter un e-mail…" @@ -470,12 +475,10 @@ msgstr "Connexion impossible" #: src/components/comment/CommentForm.tsx:127 #: src/components/dump/create/DumpCreateModal.tsx:746 #: src/components/form/FormActions.tsx:42 +#: src/components/form/InlineEdit.tsx:277 #: src/components/playlist/PlaylistEditForm.tsx:222 -#: src/components/profile/DescriptionEditor.tsx:97 -#: src/components/profile/EmailEditor.tsx:83 #: src/components/shared/ConfirmModal.tsx:63 #: src/pages/Authorize.tsx:158 -#: src/pages/Dump.tsx:516 #: src/pages/DumpEdit.tsx:561 msgid "Cancel" msgstr "Annuler" @@ -501,7 +504,7 @@ msgstr "Catégories" msgid "Category" msgstr "Catégorie" -#: src/components/profile/ProfileHeader.tsx:102 +#: src/components/profile/ProfileHeader.tsx:109 msgid "Change avatar" msgstr "Changer l'avatar" @@ -721,7 +724,7 @@ msgstr "Échec de la suppression ({0})" #: src/components/playlist/PlaylistCard.tsx:172 #: src/components/playlist/PlaylistEditForm.tsx:213 #: src/components/profile/ProfilePlaylistsTab.tsx:224 -#: src/pages/PlaylistDetail.tsx:172 +#: src/pages/PlaylistDetail.tsx:174 msgid "Delete playlist" msgstr "Supprimer la collection" @@ -738,7 +741,7 @@ msgid "Delete this message?" msgstr "Supprimer ce message ?" #: src/components/profile/ProfilePlaylistsTab.tsx:223 -#: src/pages/PlaylistDetail.tsx:171 +#: src/pages/PlaylistDetail.tsx:173 msgid "Delete this playlist? This cannot be undone." msgstr "Supprimer cette collection ? Cette action est irréversible." @@ -827,8 +830,8 @@ msgstr "Plus tôt" #: src/components/chat/ChatMessageItem.tsx:108 #: src/components/chat/PendingItem.tsx:93 #: src/components/comment/CommentNode.tsx:256 -#: src/components/playlist/PlaylistHeader.tsx:64 -#: src/pages/Dump.tsx:623 +#: src/components/playlist/PlaylistHeader.tsx:104 +#: src/pages/Dump.tsx:556 msgid "Edit" msgstr "Modifier" @@ -837,16 +840,22 @@ msgstr "Modifier" msgid "Edit {0}" msgstr "Modifier {0}" -#: src/components/profile/ProfileDescription.tsx:58 +#: src/components/playlist/PlaylistHeader.tsx:113 +#: src/components/profile/ProfileDescription.tsx:45 msgid "Edit description" msgstr "Modifier la description" -#: src/components/profile/ProfileHeader.tsx:171 +#: src/components/profile/ProfileHeader.tsx:161 msgid "Edit email" msgstr "Modifier l'e-mail" +#: src/pages/Dump.tsx:510 +msgid "Edit note" +msgstr "Modifier la note" + #: src/components/dump/create/TitleField.tsx:69 -#: src/pages/Dump.tsx:533 +#: src/components/playlist/PlaylistHeader.tsx:84 +#: src/pages/Dump.tsx:459 msgid "Edit title" msgstr "Modifier le titre" @@ -859,8 +868,8 @@ msgstr "modifié" #. placeholder {0}: relativeTime(message.updatedAt) #: src/components/chat/ChatMessageItem.tsx:132 #: src/components/comment/CommentNode.tsx:205 -#: src/components/playlist/PlaylistHeader.tsx:99 -#: src/pages/Dump.tsx:576 +#: src/components/playlist/PlaylistHeader.tsx:158 +#: src/pages/Dump.tsx:492 msgid "edited {0}" msgstr "modifié {0}" @@ -869,8 +878,8 @@ msgstr "modifié {0}" #. placeholder {0}: message.updatedAt.toLocaleString() #: src/components/chat/ChatMessageItem.tsx:128 #: src/components/comment/CommentNode.tsx:203 -#: src/components/playlist/PlaylistHeader.tsx:97 -#: src/pages/Dump.tsx:574 +#: src/components/playlist/PlaylistHeader.tsx:156 +#: src/pages/Dump.tsx:490 msgid "Edited {0}" msgstr "Modifié le {0}" @@ -936,8 +945,6 @@ msgstr "Impossible de publier la réponse" #: src/components/federation/GrantList.tsx:98 #: src/components/federation/PeerManager.tsx:158 #: src/components/playlist/PlaylistEditForm.tsx:207 -#: src/components/profile/DescriptionEditor.tsx:99 -#: src/components/profile/EmailEditor.tsx:86 msgid "Failed to save" msgstr "Enregistrement échoué" @@ -949,7 +956,7 @@ msgstr "Échec de l’enregistrement de la catégorie" msgid "Failed to save edit" msgstr "Impossible d'enregistrer la modification" -#: src/components/profile/ProfileHeader.tsx:182 +#: src/components/profile/ProfileHeader.tsx:174 msgid "Failed to update avatar" msgstr "Impossible de mettre à jour l'avatar" @@ -1064,7 +1071,7 @@ msgstr "Si vous n'êtes pas à l'origine de cette demande, ignorez cet e-mail. V msgid "image" msgstr "image" -#: src/pages/Dump.tsx:643 +#: src/pages/Dump.tsx:576 msgid "In collections" msgstr "Dans les collections" @@ -1095,7 +1102,7 @@ msgstr "Lien invalide" msgid "Invite link" msgstr "Lien d'invitation" -#: src/components/profile/ProfileHeader.tsx:126 +#: src/components/profile/ProfileHeader.tsx:133 msgid "invited by" msgstr "invité par" @@ -1165,7 +1172,7 @@ msgstr "Charger plus" msgid "Load older messages" msgstr "Charger les messages plus anciens" -#: src/pages/Dump.tsx:359 +#: src/pages/Dump.tsx:358 #: src/pages/DumpEdit.tsx:210 msgid "Loading dump…" msgstr "Chargement de la reco…" @@ -1178,7 +1185,7 @@ msgstr "Chargement des messages…" msgid "Loading more…" msgstr "Chargement…" -#: src/pages/PlaylistDetail.tsx:82 +#: src/pages/PlaylistDetail.tsx:83 msgid "Loading playlist…" msgstr "Chargement de la collection…" @@ -1548,9 +1555,9 @@ msgstr "Piste précédente" #: src/components/dump/DumpCard.tsx:144 #: src/components/dump/JournalCard.tsx:146 #: src/components/playlist/PlaylistCard.tsx:127 -#: src/components/playlist/PlaylistHeader.tsx:81 +#: src/components/playlist/PlaylistHeader.tsx:140 #: src/components/playlist/PlaylistMembershipPanel.tsx:81 -#: src/pages/Dump.tsx:582 +#: src/pages/Dump.tsx:498 msgid "private" msgstr "privé" @@ -1559,7 +1566,7 @@ msgid "Private" msgstr "Privé" #: src/components/playlist/PlaylistCard.tsx:126 -#: src/components/playlist/PlaylistHeader.tsx:81 +#: src/components/playlist/PlaylistHeader.tsx:140 msgid "public" msgstr "public" @@ -1593,7 +1600,7 @@ msgstr "Inscription…" msgid "Registration failed" msgstr "Inscription échouée" -#: src/pages/Dump.tsx:634 +#: src/pages/Dump.tsx:567 msgid "Related" msgstr "Connexe" @@ -1680,7 +1687,7 @@ msgid "email.reset.title" msgstr "Réinitialiser votre mot de passe" #: src/components/chat/PendingItem.tsx:86 -#: src/pages/Dump.tsx:393 +#: src/pages/Dump.tsx:392 #: src/pages/DumpEdit.tsx:227 msgid "Retry" msgstr "Réessayer" @@ -1692,20 +1699,16 @@ msgstr "Rôle" #: src/components/category/CategoryManager.tsx:232 #: src/components/chat/ChatEditBox.tsx:85 #: src/components/comment/CommentNode.tsx:216 +#: src/components/form/InlineEdit.tsx:269 #: src/components/playlist/PlaylistEditForm.tsx:229 -#: src/components/profile/DescriptionEditor.tsx:89 -#: src/components/profile/EmailEditor.tsx:75 -#: src/pages/Dump.tsx:508 #: src/pages/DumpEdit.tsx:564 msgid "Save" msgstr "Enregistrer" #: src/components/comment/CommentNode.tsx:217 +#: src/components/form/InlineEdit.tsx:266 #: src/components/playlist/PlaylistEditForm.tsx:226 #: src/components/profile/ChangePasswordModal.tsx:119 -#: src/components/profile/DescriptionEditor.tsx:86 -#: src/components/profile/EmailEditor.tsx:72 -#: src/pages/Dump.tsx:507 #: src/pages/ResetPassword.tsx:191 msgid "Saving…" msgstr "Enregistrement…" @@ -1815,7 +1818,7 @@ msgstr "Quelqu'un a demandé à réinitialiser le mot de passe de votre compte { #: src/components/form/FormError.tsx:31 #: src/components/layout/PageError.tsx:29 #: src/contexts/ws/WSProvider.tsx:738 -#: src/pages/PlaylistDetail.tsx:155 +#: src/pages/PlaylistDetail.tsx:157 msgid "Something went wrong" msgstr "Une erreur est survenue" @@ -1879,7 +1882,7 @@ msgstr "Le nouvel ordre n'a pas pu être enregistré. Réessayez." msgid "This comment couldn't be deleted. Try again." msgstr "Ce commentaire n'a pas pu être supprimé. Réessayez." -#: src/pages/Dump.tsx:368 +#: src/pages/Dump.tsx:367 msgid "This dump was deleted, or made private." msgstr "Cette reco a été supprimée, ou rendue privée." @@ -2033,7 +2036,7 @@ msgstr "mise à jour {ago}" msgid "email.digest.upload" msgstr "fichier" -#: src/components/profile/ProfileHeader.tsx:79 +#: src/components/profile/ProfileHeader.tsx:80 msgid "Upload failed" msgstr "Envoi échoué" @@ -2136,12 +2139,14 @@ msgid "email.welcome.subject" msgstr "Bienvenue sur {site}" #: src/components/dump/create/DumpCreateModal.tsx:705 +#: src/pages/Dump.tsx:514 +#: src/pages/Dump.tsx:521 #: src/pages/DumpEdit.tsx:539 msgid "What makes it worth it?" msgstr "Pourquoi on en voudrait ?" -#: src/components/profile/DescriptionEditor.tsx:78 -#: src/components/profile/ProfileDescription.tsx:79 +#: src/components/profile/ProfileDescription.tsx:62 +#: src/components/profile/ProfileDescription.tsx:72 msgid "Who am I?" msgstr "Qui suis-je ?" diff --git a/src/App.css b/src/App.css index 57d3bf5d..05764720 100644 --- a/src/App.css +++ b/src/App.css @@ -223,67 +223,151 @@ word-break: break-word; } -.dump-title-view { +/* ── Inline edit ── */ +/* Text edited where it stands (InlineEdit.tsx). Hovered, the text sits on a + faint plate with a ✎ after it; opened, the field takes the plate's place + and the text's type, so that nothing jumps but the buttons that come under + it. The plate spills past the text, drawn under it by a pseudo-element: the + text's own box keeps its size, which a row measuring it (a playlist's title + beside its buttons) depends on. */ +.inline-edit { + /* Tinted from the text, as it sits on a card or on the page alike. */ + --inline-edit-plate: color-mix(in srgb, var(--color-text) 7%, transparent); + --inline-edit-pad-y: 0.1rem; + --inline-edit-pad-x: 0.45rem; + min-width: 0; + max-width: 100%; +} + +.inline-edit--block { + --inline-edit-pad-y: 0.45rem; + --inline-edit-pad-x: 0.6rem; +} + + +.inline-edit-view { position: relative; + isolation: isolate; + max-width: 100%; + cursor: text; +} + +.inline-edit-view::before { + content: ""; + position: absolute; + inset: calc(-1 * var(--inline-edit-pad-y)) + calc(-1 * var(--inline-edit-pad-x)); + z-index: -1; + border-radius: var(--radius-md); + transition: background-color var(--dur-fast); +} + +.inline-edit--line .inline-edit-view { display: inline-flex; align-items: center; - gap: 0.4rem; - border-radius: var(--radius-md); - max-width: 100%; - /* Padding gives the hover box breathing room (incl. right of the pencil); - the matching negative margin keeps the title's visual position unchanged. */ - padding: 0.1rem 0.45rem; - margin: -0.1rem -0.45rem; + gap: 0.4em; } -.dump-title-view--editable { - cursor: pointer; +/* Room on the right for the ✎, which sits in the corner. */ +.inline-edit--block .inline-edit-view { + display: block; + padding-right: 1.5rem; } -.dump-title-view--editable:hover { - background: var(--color-surface); +.inline-edit-view:hover::before { + background: var(--inline-edit-plate); } -.dump-title-edit-btn { - font-size: var(--text-sm); - line-height: 1; +.inline-edit-mark { flex-shrink: 0; + padding: 0.15em; + border: none; + border-radius: var(--radius-sm); + background: none; + font-family: inherit; + font-size: var(--text-sm); + font-weight: 400; + line-height: 1; color: var(--color-text-muted); + cursor: pointer; opacity: 0; transition: opacity var(--dur-fast); - pointer-events: none; } -.dump-title-view--editable:hover .dump-title-edit-btn, -.dump-title-view--editable:focus-visible .dump-title-edit-btn { +.inline-edit--block .inline-edit-mark { + position: absolute; + top: 0; + right: 0; +} + +.inline-edit-view:hover .inline-edit-mark, +.inline-edit-mark:focus-visible { opacity: 1; } -.dump-title-editor { +/* No pointer to hover with: the mark stays, faintly, to say it can be + tapped. */ +@media (hover: none) { + .inline-edit-mark { + opacity: 0.45; + } +} + +/* What an empty text says to the one who may fill it. */ +.inline-edit-empty { + margin: 0; + color: var(--color-text-muted); + font-style: italic; +} + +.inline-edit-form { + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +/* Typed as the text it replaces: the type is inherited at no specificity, so + that the class of that text (`fieldClassName`) and a style's rules for it + both set it over this. */ +:where(.inline-edit-input) { + font: inherit; + letter-spacing: inherit; + color: inherit; +} + +.inline-edit-input { + box-sizing: border-box; + width: calc(100% + 2 * var(--inline-edit-pad-x)); + min-width: 0; + padding: calc(var(--inline-edit-pad-y) - var(--border-width)) + calc(var(--inline-edit-pad-x) - var(--border-width)); + margin: calc(-1 * var(--inline-edit-pad-y)) + calc(-1 * var(--inline-edit-pad-x)); + border: var(--border-width) solid transparent; + border-radius: var(--radius-md); + background: var(--inline-edit-plate); + transition: border-color var(--dur-base), box-shadow var(--dur-base); +} + +/* As the Markdown editor's box takes the focus. */ +.inline-edit-input:focus { + outline: none; + border-color: var(--color-accent); + box-shadow: 0 0 0 3px + color-mix(in srgb, var(--color-accent) 20%, transparent); +} + +.inline-edit-actions { display: flex; flex-wrap: wrap; align-items: center; - gap: 0.5rem; + gap: 0.4rem; } -/* Shared chrome for inline-edit inputs (the editors that live outside `.form`). - Each rule below adds only its own size / weight / width deltas. */ -.dump-title-input, -.profile-email-input, -.username-field-input { - border: var(--border-width) solid var(--color-border); - border-radius: var(--radius-lg); - background: var(--color-surface-sunken); - color: var(--color-text); - font-family: inherit; - outline: none; - transition: border-color var(--dur-slow); -} - -.dump-title-input:focus, -.profile-email-input:focus, -.username-field-input:focus { - border-color: var(--color-accent); +.inline-edit-error { + margin: 0; + font-size: var(--text-sm); + color: var(--color-danger); } /* A name and its "Rename" button, the hint and errors below. */ @@ -304,28 +388,18 @@ flex: 1 1 10rem; min-width: 0; padding: 0.4rem 0.6rem; + border: var(--border-width) solid var(--color-border); + border-radius: var(--radius-lg); + background: var(--color-surface-sunken); + color: var(--color-text); + font-family: inherit; font-size: var(--text-base); + outline: none; + transition: border-color var(--dur-slow); } -.dump-title-input { - flex: 1; - min-width: 12rem; - padding: 0.4rem 0.6rem; - font-size: var(--text-xl); - font-weight: 700; -} - -/* Save and Cancel wrap together, under the input on a phone. */ -.dump-title-actions { - display: flex; - gap: 0.5rem; -} - -.dump-title-error { - width: 100%; - margin: 0; - font-size: var(--text-sm); - color: var(--color-danger); +.username-field-input:focus { + border-color: var(--color-accent); } /* On the narrowest viewports the fixed-width action column squeezes the title @@ -2357,23 +2431,9 @@ body.has-player .chat-fab { /* No margins of its own: the page's gap already sets it apart. */ .profile-description { width: 100%; -} - -.profile-description-view { - position: relative; - padding: 0.5rem 0.6rem; - border-radius: var(--radius-md); font-size: var(--text-md); } -.profile-description-view--editable { - cursor: pointer; -} - -.profile-description-view--editable:hover { - background: var(--color-surface); -} - .profile-description-text { white-space: pre-wrap; overflow-wrap: break-word; @@ -2381,80 +2441,16 @@ body.has-player .chat-fab { line-height: 1.6; } -.profile-description-empty { - color: var(--color-muted); - font-style: italic; -} - -.profile-description-edit-btn { - position: absolute; - top: 0.55rem; - right: 0.5rem; - font-size: var(--text-sm); - color: var(--color-muted); - opacity: 0; - transition: opacity var(--dur-fast); - pointer-events: none; -} - -.profile-description-view--editable:hover .profile-description-edit-btn, -.profile-description-view--editable:focus-visible .profile-description-edit-btn { - opacity: 1; -} - -.profile-description-editor { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.profile-email-display { - display: inline-flex; - max-width: 100%; - align-items: center; - gap: 0.35rem; +.profile-email { + margin: 0.1rem 0 0.4rem; font-size: var(--text-xs); color: var(--color-text-muted); - margin: 0.1rem 0 0.4rem; - cursor: pointer; - border-radius: var(--radius-sm); - padding: 0.1rem 0.2rem; - margin-left: -0.2rem; } -.profile-email-display:hover { - background: var(--color-surface); -} - -.profile-email-display:hover .profile-description-edit-btn, -.profile-email-display:focus-visible .profile-description-edit-btn { - opacity: 1; -} - -.profile-email-editor { - margin: 0.2rem 0 0.4rem; - display: flex; - flex-direction: column; - gap: 0.4rem; -} - -.profile-email-input { - width: min(260px, 100%); - padding: 0.35rem 0.6rem; - border-radius: var(--radius-md); +/* An address is typed a size up from the line it shows on. */ +.profile-email .inline-edit-input { + max-width: 22rem; font-size: var(--text-sm); - line-height: 1.5; -} - -.profile-email-actions { - display: flex; - gap: 0.4rem; -} - -.profile-description-actions { - display: flex; - align-items: center; - gap: 0.5rem; } .profile-invited-by { @@ -5125,7 +5121,10 @@ body.has-player .chat-fab { gap: 0.4rem; } +/* The input rests on one line, as tall as the Send button beside it: both + take --chat-composer-height, which any style's line of text fits in. */ .chat-composer { + --chat-composer-height: 2.75rem; display: flex; gap: 0.5rem; align-items: flex-end; @@ -5154,6 +5153,10 @@ body.has-player .chat-fab { above the input, in the gap over the composer: inside the textarea, the text ran under them, and in a row below they would knock the Send button out of alignment. */ +.chat-composer .chat-input { + min-height: var(--chat-composer-height); +} + .chat-composer .text-editor-footer { position: absolute; left: 0.25rem; @@ -5168,9 +5171,9 @@ body.has-player .chat-fab { .chat-send-btn { flex-shrink: 0; - /* Match the input's resting height so the two line up exactly (both edges), - while the input is free to grow taller as you type. */ - height: 2.4rem; + /* The input's resting height, so that the two line up at both edges, while + the input is free to grow taller as you type. */ + height: var(--chat-composer-height); box-sizing: border-box; } @@ -5518,6 +5521,23 @@ body.has-player .chat-fab { flex-wrap: wrap; } +/* The open title field takes the room its buttons leave beside it, which + line up with its top rather than with its Save and Cancel. */ +.playlist-detail-title-row:has(> .inline-edit--editing) { + align-items: flex-start; +} + +.playlist-detail-title-row > .inline-edit--editing { + flex: 1 1 16rem; + /* Clear of the buttons by the gap, past the field's spill. */ + margin-right: var(--inline-edit-pad-x); +} + +/* The title on the left, Follow and Edit together at the row's end. */ +.playlist-detail-title-row > :first-child:not(.inline-edit--editing) { + margin-right: auto; +} + .playlist-detail-title { margin: 0; font-size: var(--text-2xl); diff --git a/src/components/chat/ChatModal.tsx b/src/components/chat/ChatModal.tsx index 1f9b63e5..b8ebfe00 100644 --- a/src/components/chat/ChatModal.tsx +++ b/src/components/chat/ChatModal.tsx @@ -850,7 +850,7 @@ export function ChatModal( value={draft} onChange={handleDraftChange} placeholder={t`Type a message…`} - rows={2} + rows={1} autoResize maxLength={VALIDATION.CHAT_MESSAGE_MAX} countFrom={COUNT_FROM} diff --git a/src/components/form/InlineEdit.tsx b/src/components/form/InlineEdit.tsx new file mode 100644 index 00000000..c645f3ce --- /dev/null +++ b/src/components/form/InlineEdit.tsx @@ -0,0 +1,299 @@ +/** + * Renders a piece of text that whoever may change it edits where it stands, + * with no page or form of its own: a dump's title and note on its page, a + * playlist's title and description on its page, a profile's description and + * email on one's own profile. + * + * @module + */ + +import { + type ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import { Trans } from "@lingui/react/macro"; +import { Controller, FormProvider, useFormState } from "react-hook-form"; +import { useApiForm } from "../../hooks/form/useApiForm.ts"; +import { useHeightTransition } from "../../hooks/shared/useHeightTransition.ts"; +import { TextEditor, type TextEditorHandle } from "../editor/TextEditor.tsx"; +import { SubmitButton } from "./SubmitButton.tsx"; + +/** + * Passed to {@link InlineEdit} by the component showing the text, which + * renders it as `children` and saves it in `onSave`. + */ +export interface InlineEditProps { + /** The text as saved, which the field opens on. */ + value: string; + /** Without it, `children` show alone. */ + editable: boolean; + /** Names the action, such as "Edit title": the text's tooltip. */ + label: string; + /** + * Saves the trimmed text, and shows it: the field closes once it resolves. + * A thrown error, such as `expectOk`'s, stays open under the field. A text + * left as it was closes the field without calling it. + */ + onSave: (value: string) => Promise; + /** + * A one-line field (`text`, `email`), where Enter saves, or the Markdown + * editor (`markdown`), where Ctrl/Cmd+Enter does. Escape cancels in both. + */ + kind?: "text" | "email" | "markdown"; + /** Refuses to save an empty text. */ + required?: boolean; + maxLength?: number; + placeholder?: string; + /** Added to the wrapper, which the text and the field both sit in. */ + className?: string; + /** + * Added to a one-line field: the class of the text it edits, such as + * `dump-title`, so that the field takes on its type in every style. + */ + fieldClassName?: string; + children: ReactNode; +} + +/** + * Renders `children`, which a click on them, or the ✎ button after them, + * swaps for a field holding `value`, with Save and Cancel under it. The ✎ + * shows on hover and on keyboard focus. A click on a link in the text, or one + * that ends a selection, follows the link or keeps the selection instead. + * Closing the field gives the focus back to the ✎ when the field had it. + */ +export function InlineEdit( + { + value, + editable, + label, + onSave, + kind = "text", + required = false, + maxLength, + placeholder, + className, + fieldClassName, + children, + }: InlineEditProps, +): React.JSX.Element { + const [editing, setEditing] = useState(false); + const heightRef = useHeightTransition(editing); + const rootRef = useRef(null); + const setRoot = useCallback((el: HTMLDivElement | null) => { + rootRef.current = el; + return heightRef(el); + }, [heightRef]); + const markRef = useRef(null); + const refocusRef = useRef(false); + + useEffect(() => { + if (editing || !refocusRef.current) return; + refocusRef.current = false; + markRef.current?.focus({ preventScroll: true }); + }, [editing]); + + if (!editable) return <>{children}; + + const close = () => { + // A field disabled while it saved has dropped the focus to the body. + refocusRef.current = !!rootRef.current?.contains(document.activeElement) || + document.activeElement === document.body; + setEditing(false); + }; + + return ( +
+ {editing + ? ( + + ) + : ( +
{ + if ((e.target as Element).closest("a")) return; + if (getSelection()?.type === "Range") return; + setEditing(true); + }} + > + {children} + +
+ )} +
+ ); +} + +type InlineEditFormProps = + & Omit + & { onClose: () => void }; + +/** The field, mounted afresh on each opening so it starts from `value`. */ +function InlineEditForm( + { + value, + onSave, + onClose, + kind = "text", + required, + maxLength, + placeholder, + fieldClassName, + label, + }: InlineEditFormProps, +): React.JSX.Element { + const form = useApiForm<{ text: string }>({ + defaultValues: { text: value }, + }); + const text = form.watch("text"); + const submitting = form.formState.isSubmitting; + const editorRef = useRef(null); + const inputRef = useRef(null); + + // The caret goes after the text, where an edit most often begins. + useEffect(() => { + editorRef.current?.focus({ atEnd: true, preventScroll: true }); + const input = inputRef.current; + if (!input) return; + input.focus({ preventScroll: true }); + // An email field has no selection to set. + if (input.type === "text") { + input.setSelectionRange(input.value.length, input.value.length); + } + }, []); + + const tooLong = maxLength !== undefined && text.length > maxLength; + const blank = required && !text.trim(); + + const onSubmit = form.submit(async ({ text }) => { + const trimmed = text.trim(); + if (trimmed !== value.trim()) await onSave(trimmed); + onClose(); + }); + const cancel = () => { + if (!submitting) onClose(); + }; + const { ref: registerRef, ...registration } = form.register("text"); + + return ( + +
+ {kind === "markdown" + ? ( + ( + { + if (!tooLong && !blank) void onSubmit(); + }} + onCancel={cancel} + placeholder={placeholder} + autoResize + maxLength={maxLength} + disabled={submitting} + /> + )} + /> + ) + : ( + { + registerRef(el); + inputRef.current = el; + }} + type={kind} + className={`inline-edit-input${ + fieldClassName ? ` ${fieldClassName}` : "" + }`} + aria-label={label} + placeholder={placeholder} + maxLength={maxLength} + disabled={submitting} + onKeyDown={(e) => { + if (e.key === "Escape") { + e.preventDefault(); + cancel(); + } + }} + /> + )} +
+ Saving…} + disabled={tooLong || blank} + > + Save + + + +
+ +
+ ); +} + +/** + * Renders why the save failed, as one line beside the buttons: an + * `ErrorCard` would outweigh the field it is about. + */ +function InlineEditError(): React.JSX.Element | null { + const { errors } = useFormState(); + const message = errors.root?.message; + if (!message) return null; + return ( +

+ {message} +

+ ); +} diff --git a/src/components/layout/Marquee.tsx b/src/components/layout/Marquee.tsx index 8146b184..9e543a4c 100644 --- a/src/components/layout/Marquee.tsx +++ b/src/components/layout/Marquee.tsx @@ -7,7 +7,13 @@ * @module */ -import { useLayoutEffect, useRef, useState, useSyncExternalStore } from "react"; +import { + useEffect, + useLayoutEffect, + useRef, + useState, + useSyncExternalStore, +} from "react"; import type { CommunityFeed as CommunityFeedData } from "../../../shared/community.ts"; import type { OnlineUser } from "../../../shared/ws.ts"; import { useAuth } from "../../hooks/auth/useAuth.ts"; @@ -24,11 +30,16 @@ function onMotionChange(callback: () => void): () => void { return () => query.removeEventListener("change", callback); } -// When the crawl began its current loop. It is kept out of the component, -// which every page mounts anew with its header: the next page's marquee -// carries on from where the last one's had got to, rather than from the -// start. -let loopStart: number | undefined; +// Where the crawl had got to, and what its loop showed, when the marquee was +// last on screen. They are kept out of the component, which every page mounts +// anew with its header: the next page's marquee carries on from that point of +// that loop, rather than from the start. A crawl that was running has gone on +// since; one paused under the pointer, which has just clicked a link in it, +// hasn't. +let lastPosition: + | { elapsed: number; at: number; running: boolean } + | undefined; +let lastLoop: Loop | null = null; /** What one loop of the crawl shows, from its start to its end. */ interface Loop { @@ -61,8 +72,12 @@ function FeedMarquee(): React.JSX.Element { onMotionChange, () => matchMedia(REDUCED_MOTION).matches, ); - const [loop, setLoop] = useState(null); + // A marquee with no feed yet is a first one, or follows a sign-out. + const [loop, setLoop] = useState(() => feed && lastLoop); if (!loop && feed) setLoop({ feed, online, turn: 0 }); + useEffect(() => { + lastLoop = loop; + }, [loop]); const shown = still ? feed && { feed, online } : loop; const trackRef = useRef(null); @@ -71,16 +86,32 @@ function FeedMarquee(): React.JSX.Element { const length = track?.firstElementChild?.getBoundingClientRect().width; if (!track || !length) return; track.style.setProperty("--marquee-length", String(Math.round(length))); - loopStart ??= performance.now(); + const last = lastPosition; + lastPosition = undefined; + const elapsed = !last + ? 0 + : last.elapsed + (last.running ? performance.now() - last.at : 0); // A negative delay starts the animation that far in. - track.style.animationDelay = `${(loopStart - performance.now()) / 1000}s`; + track.style.animationDelay = `${-elapsed / 1000}s`; + return () => { + // The track's own animation: the focus can have stopped it. + const animation = track.getAnimations()[0]; + const timing = animation?.effect?.getComputedTiming(); + if (!timing?.progress || typeof timing.duration !== "number") return; + lastPosition = { + elapsed: timing.progress * timing.duration, + at: performance.now(), + running: animation.playState === "running", + }; + }; }, [loop?.turn, still]); return (
{ if (!e.currentTarget.contains(e.relatedTarget)) { e.currentTarget.scrollLeft = 0; @@ -95,7 +126,6 @@ function FeedMarquee(): React.JSX.Element { onAnimationIteration={(e) => { // Not a child's animation ending a round of its own. if (e.target !== e.currentTarget || !feed || !loop) return; - loopStart = performance.now(); setLoop({ feed, online, turn: loop.turn + 1 }); }} > diff --git a/src/components/playlist/PlaylistHeader.tsx b/src/components/playlist/PlaylistHeader.tsx index 2f04ac03..f5067776 100644 --- a/src/components/playlist/PlaylistHeader.tsx +++ b/src/components/playlist/PlaylistHeader.tsx @@ -8,12 +8,22 @@ import { Link } from "react-router"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; -import type { PlaylistWithDumps } from "../../../shared/playlist.ts"; +import { API_URL } from "../../config/api.ts"; +import { VALIDATION } from "../../../shared/validation.ts"; +import type { Wire } from "../../../shared/api.ts"; +import type { + Playlist, + PlaylistWithDumps, + UpdatePlaylistRequest, +} from "../../../shared/playlist.ts"; +import { expectOk } from "../../utils/shared/api.ts"; +import { deserializePlaylist } from "../../utils/shared/deserialize.ts"; import { imageProps, playlistImageUrl } from "../../utils/shared/urls.ts"; import { IMAGE_SIZES } from "../../config/imageSizes.ts"; import { relativeTime } from "../../utils/shared/relativeTime.ts"; import { useAuth } from "../../hooks/auth/useAuth.ts"; import { Markdown } from "../editor/Markdown.tsx"; +import { InlineEdit } from "../form/InlineEdit.tsx"; import { FollowPlaylistButton } from "../social/FollowButton.tsx"; import { Tooltip } from "../shared/Tooltip.tsx"; import { CategoryChips } from "../category/CategoryChips.tsx"; @@ -22,18 +32,38 @@ import { userPath } from "../../../shared/paths.ts"; /** Passed to {@link PlaylistHeader} by `PlaylistDetail`. */ export interface PlaylistHeaderProps { playlist: PlaylistWithDumps; - /** Given to those who may edit it, for whom it adds an "Edit" button. */ + /** + * Given to those who may edit it, with `onSaved`: it adds an "Edit" button, + * which opens `PlaylistEditForm`. + */ onEdit?: () => void; + /** + * Called with the playlist as the server has it once its title or + * description is edited in place, which it lets its editors do. + */ + onSaved?: (playlist: Playlist) => void; } /** * Renders the playlist's details, with a follow button for anyone but its - * owner. `PlaylistEditForm` takes its place while it is edited. + * owner. Its editors change the title and the description in place, saved to + * `PUT /api/playlists/:playlistId`, and the rest in `PlaylistEditForm`, which + * takes the header's place while it is open. */ export function PlaylistHeader( - { playlist, onEdit }: PlaylistHeaderProps, + { playlist, onEdit, onSaved }: PlaylistHeaderProps, ): React.JSX.Element { - const { user, mediaToken } = useAuth(); + const { user, mediaToken, authFetch } = useAuth(); + const save = async (changes: UpdatePlaylistRequest) => { + const updated = await expectOk>( + await authFetch(`${API_URL}/api/playlists/${playlist.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(changes), + }), + ); + onSaved?.(deserializePlaylist(updated)); + }; return ( <> {playlist.imageMime && ( @@ -48,7 +78,17 @@ export function PlaylistHeader( )}
-

{playlist.title}

+ save({ title })} + required + maxLength={VALIDATION.PLAYLIST_TITLE_MAX} + fieldClassName="playlist-detail-title" + > +

{playlist.title}

+
{user?.id !== playlist.userId && ( - {playlist.description && ( - - {playlist.description} - + {(playlist.description || onSaved) && ( + save({ description: description || null })} + kind="markdown" + maxLength={VALIDATION.PLAYLIST_DESCRIPTION_MAX} + placeholder={t`Add a description…`} + + > + {playlist.description + ? ( + + {playlist.description} + + ) + : ( +

+ Add a description… +

+ )} +
)}
diff --git a/src/components/profile/DescriptionEditor.tsx b/src/components/profile/DescriptionEditor.tsx deleted file mode 100644 index 3be8a967..00000000 --- a/src/components/profile/DescriptionEditor.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Renders the inline editor of the signed-in user's description, under their - * profile's header. - * - * @module - */ - -import { t } from "@lingui/core/macro"; -import { Trans } from "@lingui/react/macro"; -import { Controller, FormProvider } from "react-hook-form"; -import { API_URL } from "../../config/api.ts"; -import { VALIDATION } from "../../../shared/validation.ts"; -import type { Wire } from "../../../shared/api.ts"; -import type { PublicUser, UpdateUserRequest } from "../../../shared/user.ts"; -import { expectOk } from "../../utils/shared/api.ts"; -import { useAuth } from "../../hooks/auth/useAuth.ts"; -import { useApiForm } from "../../hooks/form/useApiForm.ts"; -import { TextEditor } from "../editor/TextEditor.tsx"; -import { FormError } from "../form/FormError.tsx"; -import { SubmitButton } from "../form/SubmitButton.tsx"; - -/** - * Passed to {@link DescriptionEditor} by `ProfileDescription`, which shows it - * in place of the description while editing. - */ -export interface DescriptionEditorProps { - initialDescription: string; - onCancel: () => void; - onSaved: (description: string | undefined) => void; -} - -/** - * Renders the description's Markdown editor, saved to `PATCH /api/me`. - * Other browsers see the change through the socket's `user_updated` event. - */ -export function DescriptionEditor( - { initialDescription, onCancel, onSaved }: DescriptionEditorProps, -): React.JSX.Element { - const { authFetch } = useAuth(); - const form = useApiForm<{ description: string }>({ - defaultValues: { description: initialDescription }, - }); - const description = form.watch("description"); - const submitting = form.formState.isSubmitting; - - const onSubmit = form.submit(async ({ description }) => { - const trimmed = description.trim(); - // Sent even when empty, since the server keeps the old one when it is left - // out. - await expectOk>( - await authFetch(`${API_URL}/api/me`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify( - { description: trimmed } satisfies UpdateUserRequest, - ), - }), - ); - onSaved(trimmed || undefined); - }); - - return ( - -
- ( - void onSubmit()} - onCancel={submitting ? undefined : onCancel} - placeholder={t`Who am I?`} - autoResize - maxLength={VALIDATION.USER_DESCRIPTION_MAX} - /> - )} - /> -
- Saving…} - disabled={description.length > VALIDATION.USER_DESCRIPTION_MAX} - > - Save - - - -
- -
- ); -} diff --git a/src/components/profile/EmailEditor.tsx b/src/components/profile/EmailEditor.tsx deleted file mode 100644 index ffbf089f..00000000 --- a/src/components/profile/EmailEditor.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Renders the inline editor of the signed-in user's email, in their profile's - * header. - * - * @module - */ - -import { t } from "@lingui/core/macro"; -import { Trans } from "@lingui/react/macro"; -import { FormProvider } from "react-hook-form"; -import { API_URL } from "../../config/api.ts"; -import type { Wire } from "../../../shared/api.ts"; -import type { PublicUser, UpdateUserRequest } from "../../../shared/user.ts"; -import { expectOk } from "../../utils/shared/api.ts"; -import { useAuth } from "../../hooks/auth/useAuth.ts"; -import { useApiForm } from "../../hooks/form/useApiForm.ts"; -import { FormError } from "../form/FormError.tsx"; -import { SubmitButton } from "../form/SubmitButton.tsx"; - -/** - * Passed to {@link EmailEditor} by `ProfileHeader`, which shows it in place of - * the email while editing. - */ -export interface EmailEditorProps { - initialEmail: string; - onCancel: () => void; - onSaved: (email: string) => void; -} - -/** - * Renders the email field, saved to `PATCH /api/me`. Escape cancels. - */ -export function EmailEditor( - { initialEmail, onCancel, onSaved }: EmailEditorProps, -): React.JSX.Element { - const { authFetch } = useAuth(); - const form = useApiForm<{ email: string }>({ - defaultValues: { email: initialEmail }, - }); - const email = form.watch("email"); - const submitting = form.formState.isSubmitting; - - const onSubmit = form.submit(async ({ email }) => { - await expectOk>( - await authFetch(`${API_URL}/api/me`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify( - { email: email.trim() } satisfies UpdateUserRequest, - ), - }), - ); - onSaved(email.trim()); - }); - - return ( - -
- { - if (e.key === "Escape") onCancel(); - }} - disabled={submitting} - autoFocus - /> -
- Saving…} - disabled={!email.trim()} - > - Save - - -
- - -
- ); -} diff --git a/src/components/profile/ProfileDescription.tsx b/src/components/profile/ProfileDescription.tsx index a2628550..9fe4f5db 100644 --- a/src/components/profile/ProfileDescription.tsx +++ b/src/components/profile/ProfileDescription.tsx @@ -5,12 +5,16 @@ * @module */ -import { useState } from "react"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; -import { useHeightTransition } from "../../hooks/shared/useHeightTransition.ts"; +import { API_URL } from "../../config/api.ts"; +import { VALIDATION } from "../../../shared/validation.ts"; +import type { Wire } from "../../../shared/api.ts"; +import type { PublicUser, UpdateUserRequest } from "../../../shared/user.ts"; +import { expectOk } from "../../utils/shared/api.ts"; +import { useAuth } from "../../hooks/auth/useAuth.ts"; +import { InlineEdit } from "../form/InlineEdit.tsx"; import { Markdown } from "../editor/Markdown.tsx"; -import { DescriptionEditor } from "./DescriptionEditor.tsx"; /** Passed to {@link ProfileDescription} by `UserPublicProfile`. */ export interface ProfileDescriptionProps { @@ -22,70 +26,53 @@ export interface ProfileDescriptionProps { /** * Renders the description in Markdown, or nothing when there is none. On - * their own profile, users get a "Who am I?" placeholder instead, and a click - * or Enter swaps in {@link DescriptionEditor}. + * their own profile, users get a "Who am I?" placeholder instead, and edit it + * in place, saved to `PATCH /api/me`. Other browsers see the change through + * the socket's `user_updated` event. */ export function ProfileDescription( { description, isOwnProfile, onSaved }: ProfileDescriptionProps, ): React.JSX.Element | null { - const [editing, setEditing] = useState(false); - const ref = useHeightTransition(editing); + const { authFetch } = useAuth(); if (!description && !isOwnProfile) return null; return ( -
- {editing - ? ( - setEditing(false)} - onSaved={(saved) => { - onSaved(saved); - setEditing(false); - }} - /> - ) - : ( -
setEditing(true) : undefined} - // Focusable rather than a button, so the Markdown inside keeps its - // own structure and links. - tabIndex={isOwnProfile ? 0 : undefined} - title={isOwnProfile ? t`Edit description` : undefined} - onKeyDown={isOwnProfile - ? (e) => { - if ( - (e.key === "Enter" || e.key === " ") && - e.target === e.currentTarget - ) { - e.preventDefault(); - setEditing(true); - } - } - : undefined} - > - {description - ? ( - - {description} - - ) - : ( -
- Who am I? -
- )} - {isOwnProfile && ( - - ✎ - - )} -
- )} +
+ { + // Sent even when empty, since the server keeps the old one when it + // is left out. + await expectOk>( + await authFetch(`${API_URL}/api/me`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify( + { description: saved } satisfies UpdateUserRequest, + ), + }), + ); + onSaved(saved || undefined); + }} + kind="markdown" + maxLength={VALIDATION.USER_DESCRIPTION_MAX} + placeholder={t`Who am I?`} + > + {description + ? ( + + {description} + + ) + : ( +

+ Who am I? +

+ )} +
); } diff --git a/src/components/profile/ProfileHeader.tsx b/src/components/profile/ProfileHeader.tsx index b8cb0462..7f0a3bdd 100644 --- a/src/components/profile/ProfileHeader.tsx +++ b/src/components/profile/ProfileHeader.tsx @@ -11,15 +11,18 @@ import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { API_URL } from "../../config/api.ts"; import type { Wire } from "../../../shared/api.ts"; -import type { PublicUser, SelfUser } from "../../../shared/user.ts"; -import { parseAPIResponse } from "../../utils/shared/api.ts"; +import type { + PublicUser, + SelfUser, + UpdateUserRequest, +} from "../../../shared/user.ts"; +import { expectOk, parseAPIResponse } from "../../utils/shared/api.ts"; import { deserializeSelfUser } from "../../utils/shared/deserialize.ts"; import { useAuth } from "../../hooks/auth/useAuth.ts"; -import { useHeightTransition } from "../../hooks/shared/useHeightTransition.ts"; import { Avatar } from "../shared/Avatar.tsx"; import { ErrorCard } from "../shared/ErrorCard.tsx"; import { FollowUserButton } from "../social/FollowButton.tsx"; -import { EmailEditor } from "./EmailEditor.tsx"; +import { InlineEdit } from "../form/InlineEdit.tsx"; import { ProfileOwnActions } from "./ProfileOwnActions.tsx"; import { RoleChip } from "./RoleSwitcher.tsx"; import { userPath } from "../../../shared/paths.ts"; @@ -38,7 +41,7 @@ export interface ProfileHeaderProps { * Renders the avatar, username, role and inviter, and for a user from another * instance, a link to their profile there. Visitors get a follow button. On * their own profile, users change their avatar (`PUT /api/me/avatar`) and - * email, unless their account comes from another instance, and invite + * email (`PATCH /api/me`), unless their account comes from another instance, and invite * someone; their settings, the site's tools and logging out are in the * header's avatar menu. */ @@ -49,8 +52,6 @@ export function ProfileHeader( const [uploading, setUploading] = useState(false); const [avatarError, setAvatarError] = useState(null); const fileInputRef = useRef(null); - const [emailEditing, setEmailEditing] = useState(false); - const infoRef = useHeightTransition(emailEditing); const { name, host } = splitHandle(user.username); // A user from another instance changes their avatar and email there. const editable = isOwnProfile && !host; @@ -83,9 +84,15 @@ export function ProfileHeader( } }; - const handleEmailSaved = (email: string) => { + const handleEmailSave = async (email: string) => { + await expectOk>( + await authFetch(`${API_URL}/api/me`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email } satisfies UpdateUserRequest), + }), + ); if (me) updateSession({ user: { ...me, email } }); - setEmailEditing(false); }; return ( @@ -112,7 +119,7 @@ export function ProfileHeader( )}
-
+

{name} @@ -148,34 +155,19 @@ export function ProfileHeader(

)} {editable && ( - emailEditing - ? ( - setEmailEditing(false)} - onSaved={handleEmailSaved} - /> - ) - : ( -

setEmailEditing(true)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - setEmailEditing(true); - } - }} - tabIndex={0} - role="button" - title={t`Edit email`} - > - {me?.email ?? t`Add email…`} - - ✎ - -

- ) + + + {me?.email ?? t`Add email…`} + + )} {avatarError && ( ([]); const [collections, setCollections] = useState([]); - const [titleEditing, setTitleEditing] = useState(false); - const headerRightRef = useHeightTransition(titleEditing); - const [titleDraft, setTitleDraft] = useState(""); - const [titleSaving, setTitleSaving] = useState(false); - const [titleError, setTitleError] = useState(null); - const { user, mediaToken, authFetch } = useAuth(); const { voteCounts, @@ -409,45 +408,23 @@ export function Dump(): React.JSX.Element { const canEdit = !!user && (dump.userId === user.id || can(user, "dump:moderate")); - const startTitleEdit = () => { - setTitleDraft(dump.title); - setTitleError(null); - setTitleEditing(true); - }; - - const handleTitleSave = async () => { - const trimmed = titleDraft.trim(); - if (!trimmed || trimmed.length > VALIDATION.DUMP_TITLE_MAX) return; - - setTitleSaving(true); - setTitleError(null); - try { - const res = await authFetch(`${API_URL}/api/dumps/${dump.id}`, { - method: "PUT", - body: JSON.stringify( - { title: trimmed } satisfies UpdateDumpRequest, - ), + // Saves what the title or the note was changed to in place. The page stays + // mounted across a slug change, and would keep showing the dump it had. + const saveDump = async (changes: UpdateDumpRequest) => { + const updatedDump = deserializeDump( + await expectOk>( + await authFetch(`${API_URL}/api/dumps/${dump.id}`, { + method: "PUT", + body: JSON.stringify(changes), + }), + ), + ); + setDumpState({ status: "loaded", dump: updatedDump }); + if (updatedDump.slug !== dump.slug) { + navigate(dumpUrl(updatedDump), { + replace: true, + state: { dump: updatedDump }, }); - const apiResponse = parseAPIResponse>(await res.json()); - if (!apiResponse.success) { - setTitleError(apiResponse.error.message); - return; - } - const updatedDump = deserializeDump(apiResponse.data); - setTitleEditing(false); - // The page stays mounted across the slug change below, and would keep - // the old title. - setDumpState({ status: "loaded", dump: updatedDump }); - if (updatedDump.slug !== dump.slug) { - navigate(dumpUrl(updatedDump), { - replace: true, - state: { dump: updatedDump }, - }); - } - } catch (err) { - setTitleError(friendlyFetchError(err)); - } finally { - setTitleSaving(false); } }; @@ -475,79 +452,18 @@ export function Dump(): React.JSX.Element { )}

-
- {titleEditing - ? ( -
- setTitleDraft(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - handleTitleSave(); - } else if (e.key === "Escape") { - setTitleEditing(false); - } - }} - maxLength={VALIDATION.DUMP_TITLE_MAX} - disabled={titleSaving} - autoFocus - /> -
- - -
- {titleError && ( -

{titleError}

- )} -
- ) - : ( -
{ - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - startTitleEdit(); - } - } - : undefined} - > -

{dump.title}

- {canEdit && ( - - ✎ - - )} -
- )} +
+ saveDump({ title })} + required + maxLength={VALIDATION.DUMP_TITLE_MAX} + fieldClassName="dump-title" + > +

{dump.title}

+
- {dump.comment && ( - {dump.comment} + {(dump.comment || canEdit) && ( + saveDump({ comment: comment || null })} + kind="markdown" + maxLength={VALIDATION.DUMP_COMMENT_MAX} + placeholder={t`What makes it worth it?`} + + > + {dump.comment + ? {dump.comment} + : ( +

+ What makes it worth it? +

+ )} +
)}
diff --git a/src/pages/PlaylistDetail.tsx b/src/pages/PlaylistDetail.tsx index 313f5fd8..f4cb49ac 100644 --- a/src/pages/PlaylistDetail.tsx +++ b/src/pages/PlaylistDetail.tsx @@ -36,7 +36,8 @@ export interface PlaylistDetailProps { * a private one only to its owner. `usePlaylist` loads it, by a former name or * slug too, and keeps it live. Its owner, or a user with `playlist:moderate`, * can drag its dumps into a new order and remove one, with a short window to - * undo, and edit or delete it in the editor that opens over its header at + * undo, edit its title and description in place, and edit the rest or delete + * it in the editor that opens over its header at * `/@:username/playlists/:slug/edit`. Both paths render the page at the same * place, so opening the editor keeps the playlist loaded. Anyone else there * gets the page, or is asked to sign in first. @@ -143,9 +144,10 @@ export function PlaylistDetail(
navigate(playlistUrl(playlist, "edit")) - : undefined} + {...(canEdit && { + onEdit: () => navigate(playlistUrl(playlist, "edit")), + onSaved: applyDetails, + })} />
)} diff --git a/src/test/components/form/InlineEdit.test.tsx b/src/test/components/form/InlineEdit.test.tsx new file mode 100644 index 00000000..ebda4692 --- /dev/null +++ b/src/test/components/form/InlineEdit.test.tsx @@ -0,0 +1,88 @@ +import { describe, expect, test, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { userEvent } from "@testing-library/user-event"; +import { I18nProvider } from "@lingui/react"; +import { i18n } from "@lingui/core"; +import { InlineEdit } from "../../../components/form/InlineEdit.tsx"; + +function renderEdit( + props: Partial> = {}, +) { + const onSave = vi.fn(async (_: string) => {}); + const user = userEvent.setup(); + render( + + +

+ Old link +

+
+
, + ); + return { user, onSave }; +} + +describe("InlineEdit", () => { + test("without the right to edit, only the text shows", () => { + renderEdit({ editable: false }); + expect(screen.getByRole("heading")).toBeInTheDocument(); + expect(screen.queryByRole("button")).toBeNull(); + }); + + test("a click on the text opens the field; Enter saves it trimmed", async () => { + const { user, onSave } = renderEdit(); + await user.click(screen.getByRole("heading")); + const input = screen.getByRole("textbox", { name: "Edit name" }); + expect(input).toHaveFocus(); + await user.type(input, "er {Enter}"); + expect(onSave).toHaveBeenCalledWith("Older"); + expect(screen.getByRole("heading")).toBeInTheDocument(); + }); + + test("a link in the text is followed, not edited", async () => { + const { user } = renderEdit(); + await user.click(screen.getByRole("link", { name: "link" })); + expect(screen.queryByRole("textbox")).toBeNull(); + }); + + test("left as it was, nothing is sent", async () => { + const { user, onSave } = renderEdit(); + await user.click(screen.getByRole("button", { name: "Edit name" })); + await user.click(screen.getByRole("button", { name: "Save" })); + expect(onSave).not.toHaveBeenCalled(); + expect(screen.queryByRole("textbox")).toBeNull(); + }); + + test("an empty required text can't be saved", async () => { + const { user } = renderEdit(); + await user.click(screen.getByRole("button", { name: "Edit name" })); + await user.clear(screen.getByRole("textbox")); + expect(screen.getByRole("button", { name: "Save" })).toBeDisabled(); + }); + + test("Escape backs out, and the focus goes back to the ✎", async () => { + const { user, onSave } = renderEdit(); + screen.getByRole("button", { name: "Edit name" }).focus(); + await user.keyboard("{Enter}"); + await user.type(screen.getByRole("textbox"), "x{Escape}"); + expect(onSave).not.toHaveBeenCalled(); + expect(screen.getByRole("button", { name: "Edit name" })).toHaveFocus(); + }); + + test("a failed save stays open and says why", async () => { + const { user } = renderEdit({ + onSave: () => Promise.reject(new Error("Taken")), + }); + await user.click(screen.getByRole("heading")); + await user.type(screen.getByRole("textbox"), "!{Enter}"); + expect(await screen.findByRole("alert")).toHaveTextContent("Taken"); + expect(screen.getByRole("textbox")).toHaveValue("Old!"); + }); +}); diff --git a/src/test/pages/Dump.test.tsx b/src/test/pages/Dump.test.tsx index 4c2345fd..d15c7604 100644 --- a/src/test/pages/Dump.test.tsx +++ b/src/test/pages/Dump.test.tsx @@ -135,6 +135,35 @@ describe("Dump page", () => { expect(screen.getByRole("heading", { name: "A find" })).toBeInTheDocument(); }); + test("the owner writes the note in place; emptied, it goes", async () => { + dumpRoutes(); + route( + "PUT /api/dumps/:id", + () => + wire(makeDump({ + id: "d1", + slug: "a-find", + title: "A find", + userId: "owner", + comment: undefined, + })), + ); + const { user } = openDump(owner); + await user.click(await screen.findByText("reasons")); + await user.clear(screen.getByDisplayValue("Because **reasons**")); + await user.click(screen.getByRole("button", { name: "Save" })); + expect(await screen.findByText("What makes it worth it?")) + .toBeInTheDocument(); + expect(calls("PUT /api/dumps/:id")[0].body).toEqual({ comment: null }); + }); + + test("a visitor sees no note where there is none", async () => { + dumpRoutes(makeDump({ slug: "a-find", title: "A find", userId: "owner" })); + openDump(other); + await screen.findByRole("heading", { name: "A find" }); + expect(screen.queryByText("What makes it worth it?")).toBeNull(); + }); + test("others can't rename or edit it", async () => { dumpRoutes(); const { user } = openDump(other); @@ -157,7 +186,7 @@ describe("Dump page", () => { dumpRoutes(); const { user } = openDump(owner); await screen.findByRole("heading", { name: "A find" }); - screen.getByTitle("Edit title").focus(); + screen.getByRole("button", { name: "Edit title" }).focus(); await user.keyboard("{Enter}"); expect(screen.getByDisplayValue("A find")).toBeInTheDocument(); }); diff --git a/src/test/pages/PlaylistDetail.test.tsx b/src/test/pages/PlaylistDetail.test.tsx index 0af2954c..2aa95241 100644 --- a/src/test/pages/PlaylistDetail.test.tsx +++ b/src/test/pages/PlaylistDetail.test.tsx @@ -100,6 +100,7 @@ describe("PlaylistDetail", () => { .toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Remove from playlist" })).not .toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Edit title" })).toBeNull(); }); test("the cover is versioned on the last edit", async () => { @@ -140,6 +141,29 @@ describe("PlaylistDetail", () => { }); }); + test("its owner retitles it in place, which keeps its link", async () => { + route( + "PUT /api/playlists/:id", + () => + wire(makePlaylist({ + id: "p1", + slug: "mix", + title: "Remix", + userId: "owner", + description: "Good **tunes**", + })), + ); + const { user } = open(); + await user.click(await screen.findByRole("heading", { name: "Mix" })); + const input = screen.getByDisplayValue("Mix"); + await user.clear(input); + await user.type(input, "Remix{Enter}"); + expect(await screen.findByRole("heading", { name: "Remix" })) + .toBeInTheDocument(); + expect(currentPath()).toBe("/@owner/playlists/mix"); + expect(calls("PUT /api/playlists/:id")[0].body).toEqual({ title: "Remix" }); + }); + test("the editor is a page: /edit opens it, closing goes back to the page", async () => { const { user } = open({ url: "/@owner/playlists/mix/edit" }); expect(await screen.findByRole("textbox", { name: "Description" })) diff --git a/src/test/pages/UserPublicProfile.test.tsx b/src/test/pages/UserPublicProfile.test.tsx index 43e7f286..ce14ce97 100644 --- a/src/test/pages/UserPublicProfile.test.tsx +++ b/src/test/pages/UserPublicProfile.test.tsx @@ -66,7 +66,8 @@ describe("UserPublicProfile", () => { `${API_URL}/api/users/@alice%40home.example/home`, ); expect(screen.queryByTitle("Change avatar")).not.toBeInTheDocument(); - expect(screen.queryByTitle("Edit email")).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Edit email" })).not + .toBeInTheDocument(); expect(screen.queryByText("O.G.")).not.toBeInTheDocument(); expect(calls("GET /api/users/:u")[0].path).toBe( "/api/users/@alice%40home.example", @@ -451,10 +452,8 @@ describe("UserPublicProfile", () => { (req) => wire({ ...ann, ...(req.body as object) }), ); const { user } = open({ session: annSession }); - const view = (await screen.findByText("dump")).closest( - "[title='Edit description']", - ) as HTMLElement; - view.focus(); + await screen.findByText("dump"); + screen.getByRole("button", { name: "Edit description" }).focus(); await user.keyboard("{Enter}"); const box = screen.getByPlaceholderText("Who am I?"); await user.clear(box); @@ -469,14 +468,11 @@ describe("UserPublicProfile", () => { (req) => wire({ ...ann, ...(req.body as object) }), ); const { user } = open({ session: annSession }); - await user.click( - await screen.findByRole("button", { name: /ann@x\.test/ }), - ); + await user.click(await screen.findByText("ann@x.test")); const input = screen.getByDisplayValue("ann@x.test"); await user.clear(input); await user.type(input, "new@x.test{Enter}"); - expect(await screen.findByRole("button", { name: /new@x\.test/ })) - .toBeInTheDocument(); + expect(await screen.findByText("new@x.test")).toBeInTheDocument(); expect(JSON.parse(localStorage.getItem(AUTH_STORAGE_KEY)!).user.email) .toBe("new@x.test"); }); @@ -484,9 +480,7 @@ describe("UserPublicProfile", () => { test("a refused email says why", async () => { route("PATCH /api/me", () => fail("Email already in use", 409)); const { user } = open({ session: annSession }); - await user.click( - await screen.findByRole("button", { name: /ann@x\.test/ }), - ); + await user.click(await screen.findByText("ann@x.test")); await user.type(screen.getByDisplayValue("ann@x.test"), "x{Enter}"); expect(await screen.findByRole("alert")).toHaveTextContent( "Email already in use", diff --git a/src/themes/brutalist.css b/src/themes/brutalist.css index 551443b3..72c2211b 100644 --- a/src/themes/brutalist.css +++ b/src/themes/brutalist.css @@ -172,7 +172,8 @@ [data-style="brutalist"] .user-menu-trigger, [data-style="brutalist"] .user-menu-item, [data-style="brutalist"] .form-cancel, -[data-style="brutalist"] .form-label-action { +[data-style="brutalist"] .form-label-action, +[data-style="brutalist"] .inline-edit-mark { border: none !important; box-shadow: none !important; } @@ -184,7 +185,9 @@ [data-style="brutalist"] .form-cancel:hover:not(:disabled), [data-style="brutalist"] .form-cancel:active, [data-style="brutalist"] .form-label-action:hover:not(:disabled), -[data-style="brutalist"] .form-label-action:active { +[data-style="brutalist"] .form-label-action:active, +[data-style="brutalist"] .inline-edit-mark:hover:not(:disabled), +[data-style="brutalist"] .inline-edit-mark:active { transform: none !important; box-shadow: none !important; } diff --git a/src/themes/geocities.css b/src/themes/geocities.css index 8ec5426c..29f39aa1 100644 --- a/src/themes/geocities.css +++ b/src/themes/geocities.css @@ -308,7 +308,7 @@ /* Pressed: the bevel turns inside out and the label moves down and right a pixel with the face. */ -[data-style="geocities"] button:not(.dump-fab, .chat-fab, .btn--ghost, .user-menu-trigger, .user-menu-item):active:not(:disabled, [aria-disabled="true"]), +[data-style="geocities"] button:not(.dump-fab, .chat-fab, .btn--ghost, .user-menu-trigger, .user-menu-item, .inline-edit-mark):active:not(:disabled, [aria-disabled="true"]), [data-style="geocities"] .btn-primary:active:not(:disabled, [aria-disabled="true"]), [data-style="geocities"] .btn-secondary:active:not(:disabled, [aria-disabled="true"]), [data-style="geocities"] .btn-border:active:not(:disabled, [aria-disabled="true"]), @@ -653,13 +653,16 @@ opacity: 0.75; } -/* The crawl stops under the pointer, and for a link that has the focus: the - line then stands at its start, and the browser scrolls it to that link. */ +/* The crawl stops under the pointer, and for a link the keyboard has + focused: the line then stands at its start, and the browser scrolls it to + that link. A link the pointer presses takes the focus too, but is left + where it is, paused under the pointer: brought back to the start, it would + slip away before the button came up, and the click with it. */ [data-style="geocities"] .site-marquee:hover .site-marquee-track { animation-play-state: paused; } -[data-style="geocities"] .site-marquee:focus-within .site-marquee-track { +[data-style="geocities"] .site-marquee:has(:focus-visible) .site-marquee-track { animation: none; } diff --git a/src/themes/nyt.css b/src/themes/nyt.css index 249fb447..d42cc783 100644 --- a/src/themes/nyt.css +++ b/src/themes/nyt.css @@ -126,7 +126,7 @@ .form-field, .profile-role-chip, .profile-invited-by, - .profile-email-display, + .profile-email, .profile-section-header, .profile-appearance-row, .playlist-badge,