From ed7695663e935abf4301f5833ac6197397d4c45b Mon Sep 17 00:00:00 2001 From: khannurien Date: Wed, 8 Apr 2026 20:12:30 +0000 Subject: [PATCH] v3: correctly using SITE_NAME across the app, added notifications on comments --- .env.example | 4 +-- Dockerfile | 4 +-- README.md | 5 +++- api/lib/static.ts | 27 +++++++++++++---- api/model/interfaces.ts | 14 +++++++-- api/services/comment-service.ts | 6 +++- api/services/notification-service.ts | 30 +++++++++++++++++++ index.html | 3 +- src/components/AppHeader.tsx | 3 +- src/locales/en.js | 2 +- src/locales/en.po | 43 ++++++++++++++++------------ src/locales/fr.js | 2 +- src/locales/fr.po | 43 ++++++++++++++++------------ src/model.ts | 14 +++++++-- src/pages/Notifications.tsx | 26 ++++++++++++++++- vite.config.ts | 18 ++++++++++-- 16 files changed, 185 insertions(+), 59 deletions(-) diff --git a/.env.example b/.env.example index a4bf622..e26148a 100644 --- a/.env.example +++ b/.env.example @@ -6,10 +6,10 @@ # Defaults to http://localhost:GERBEUR_PORT for local development. # GERBEUR_PUBLIC_URL=https://example.com -# Site name used in OG meta tags +# Site name shown in the browser tab, app header, OG meta tags, and emails. GERBEUR_SITE_NAME=gerbeur -# Port the API server listens on (the container's internal port) +# Port the API server listens on (the container's internal port). GERBEUR_PORT=8000 # Network interface Oak binds to. Default: 0.0.0.0 (all interfaces, required for Docker). diff --git a/Dockerfile b/Dockerfile index 8cc9120..16490f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,9 +12,7 @@ COPY public/ ./public/ COPY scripts/ ./scripts/ COPY src/ ./src/ -# In same-origin deployments (API serves the frontend), no build args are needed -# — the frontend uses relative URLs at runtime. Only set VITE_API_HOSTNAME if -# the API lives on a different host than the frontend (e.g. a separate API server). +# VITE_API_* are only needed when the API lives on a different host than the frontend. ARG VITE_API_PROTOCOL ARG VITE_API_HOSTNAME ARG VITE_API_PORT diff --git a/README.md b/README.md index c92f86f..fbb6c3f 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ See [`.env.example`](.env.example) for the full list with descriptions. Key vari | `GERBEUR_LISTEN_HOST` | Network interface Oak binds to; use `127.0.0.1` to restrict to loopback | `0.0.0.0` | | `GERBEUR_FRONTEND_URL` | Frontend base URL for email links and CORS; auto-added to allowed origins — the only variable needed when the frontend runs on a separate host | `GERBEUR_PUBLIC_URL` | | `GERBEUR_ALLOWED_ORIGINS` | Comma-separated extra origins for CORS/WebSocket; `PUBLIC_URL` and `FRONTEND_URL` are always included — typically only needed in dev for the Vite server | `""` (empty) | -| `GERBEUR_SITE_NAME` | Site name used in OG meta tags and emails | `gerbeur` | +| `GERBEUR_SITE_NAME` | Site name used in the browser tab, app header, OG meta tags, and emails | `gerbeur` | | `GERBEUR_SMTPS_URL` | SMTPS connection URL for outgoing email (`smtps://user:pass@host:465`) | unset | | `GERBEUR_FROM_EMAIL` | Sender address for outgoing emails — required when `GERBEUR_SMTPS_URL` is set | unset | | `GERBEUR_WELCOME_EMAIL_BODY` | Markdown body for the account-creation welcome email; supports `{{username}}` and `{{site_name}}` | built-in template | @@ -63,12 +63,15 @@ The standard deployment runs API and frontend in a single container. The API ser ```sh docker build -t gerbeur . +docker build -t gerbeur . + docker run -d \ -p 8000:8000 \ -v gerbeur-db:/app/api/sql \ -v gerbeur-uploads:/app/api/uploads \ -e GERBEUR_JWT_SECRET=$(openssl rand -hex 32) \ -e GERBEUR_PUBLIC_URL=https://example.com \ + -e GERBEUR_SITE_NAME=mysite \ --name gerbeur \ gerbeur ``` diff --git a/api/lib/static.ts b/api/lib/static.ts index 6a3dab0..14c7e3d 100644 --- a/api/lib/static.ts +++ b/api/lib/static.ts @@ -1,13 +1,30 @@ import { Context, Next, send } from "@oak/oak"; +import { OG_SITE_NAME } from "../config.ts"; + +async function serveIndexHtml( + context: Context>, + root: string, +) { + const filePath = `${root}/index.html`; + const raw = await Deno.readTextFile(filePath); + const html = raw.replaceAll("__SITE_NAME__", OG_SITE_NAME); + context.response.type = "text/html"; + context.response.body = html; +} export function routeStaticFilesFrom(staticPaths: string[]) { return async (context: Context>, next: Next) => { + const pathname = context.request.url.pathname; + + // Serve index.html with runtime placeholder replacement + if (pathname === "/" || pathname === "/index.html") { + await serveIndexHtml(context, staticPaths[0]); + return; + } + for (const path of staticPaths) { try { - await send(context, context.request.url.pathname, { - root: path, - index: "index.html", - }); + await send(context, pathname, { root: path }); return; } catch { continue; @@ -15,7 +32,7 @@ export function routeStaticFilesFrom(staticPaths: string[]) { } // SPA fallback: serve index.html so client-side routes work on direct navigation - await send(context, "/index.html", { root: staticPaths[0] }); + await serveIndexHtml(context, staticPaths[0]); await next(); }; } diff --git a/api/model/interfaces.ts b/api/model/interfaces.ts index 43b1f9d..1afca17 100644 --- a/api/model/interfaces.ts +++ b/api/model/interfaces.ts @@ -617,7 +617,8 @@ export type NotificationType = | "user_dump_posted" | "playlist_dump_added" | "dump_upvoted" - | "user_mentioned"; + | "user_mentioned" + | "dump_commented"; export interface PlaylistFollowedData { followerId: string; @@ -661,13 +662,22 @@ export interface UserMentionedData { dumpId?: string; } +export interface DumpCommentedData { + commenterId: string; + commenterUsername: string; + commentId: string; + dumpId: string; + dumpTitle: string; +} + export type NotificationData = | PlaylistFollowedData | UserFollowedData | UserDumpPostedData | PlaylistDumpAddedData | DumpUpvotedData - | UserMentionedData; + | UserMentionedData + | DumpCommentedData; export interface Notification { id: string; diff --git a/api/services/comment-service.ts b/api/services/comment-service.ts index d76d565..fe4cc7f 100644 --- a/api/services/comment-service.ts +++ b/api/services/comment-service.ts @@ -5,7 +5,10 @@ import { } from "../model/interfaces.ts"; import { type SQLOutputValue } from "node:sqlite"; import { commentRowToApi, db, isCommentRow } from "../model/db.ts"; -import { notifyMentions } from "./notification-service.ts"; +import { + notifyDumpOwnerNewComment, + notifyMentions, +} from "./notification-service.ts"; import { linkAttachments } from "./attachment-service.ts"; const SELECT_COLS = @@ -66,6 +69,7 @@ export function createComment( const dumpRow = db.prepare(`SELECT title FROM dumps WHERE id = ?;`).get( dumpId, ) as { title: string } | undefined; + notifyDumpOwnerNewComment(userId, id, dumpId); notifyMentions(userId, body, "comment", id, dumpRow?.title ?? "", dumpId); linkAttachments(body, id); return comment; diff --git a/api/services/notification-service.ts b/api/services/notification-service.ts index 6f3e21c..17597b9 100644 --- a/api/services/notification-service.ts +++ b/api/services/notification-service.ts @@ -276,6 +276,36 @@ export function notifyMentions( } } +export function notifyDumpOwnerNewComment( + commenterId: string, + commentId: string, + dumpId: string, +): void { + const commenterRow = db.prepare( + `SELECT username FROM users WHERE id = ?;`, + ).get(commenterId) as { username: string } | undefined; + + const dumpRow = db.prepare( + `SELECT title, user_id FROM dumps WHERE id = ?;`, + ).get(dumpId) as { title: string; user_id: string } | undefined; + + if (!commenterRow || !dumpRow) return; + if (commenterId === dumpRow.user_id) return; // no self-notification + + createNotification( + dumpRow.user_id, + "dump_commented", + { + commenterId, + commenterUsername: commenterRow.username, + commentId, + dumpId, + dumpTitle: dumpRow.title, + }, + `comment:${commentId}`, + ); +} + export function notifyPlaylistFollowersNewDump( playlistId: string, playlistTitle: string, diff --git a/index.html b/index.html index 7e7c091..5148878 100644 --- a/index.html +++ b/index.html @@ -8,9 +8,10 @@ + - Dumps + __SITE_NAME__
diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 4d0fcd2..c1766ad 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -29,7 +29,8 @@ export function AppHeader( className={`app-header${centerSlot ? " app-header--has-center" : ""}`} > - 🚚 gerbeur + 🚚 {document.querySelector('meta[name="site-name"]') + ?.content ?? "gerbeur"} {centerSlot &&
{centerSlot}
} diff --git a/src/locales/en.js b/src/locales/en.js index ae86e37..e68a9d1 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -1,5 +1,5 @@ /*eslint-disable*/ module.exports = { messages: JSON.parse( - '{"-K9EZb":["Add email…"],"-OxI15":["Followed playlists"],"-Ya-b9":["Failed to save"],"-siMqD":["Journal"],"0kWhlg":["File too large (max 5 MB)"],"1CalO6":["Style"],"1HfJWf":["Search dumps, users, playlists…"],"1cbYY_":["Upvoted (",["0"],["1"],")"],"1njn7W":["Light"],"1utXA6":["Dumps"],"26iNma":["Post comment"],"29VNqC":["Unknown error"],"2Hlmdt":["Write a reply…"],"2ygf_L":["← Back"],"3KKSM4":["private"],"3T1cI4":["Unexpected server error"],"3yfh3D":["<0>",["0"]," followed your playlist <1>",["1"],""],"49voTZ":[["label"]," (",["count"],")"],"4B6w_o":["Dumped!"],"4GKuCs":["Login failed"],"4HH9iB":["Not following anyone yet."],"4RtQ1k":["Unfollow ",["targetUsername"]],"4c-qBx":["Your password has been changed. You can now log in."],"4yj9xV":["Live updates are temporarily disconnected. Trying to reconnect…"],"5cC8f2":["Edited ",["0"]],"5oD9f_":["Earlier"],"6Qly-0":["a comment"],"6gRgw8":["Retry"],"7JBW66":["Forbidden"],"7PHCIN":["Cancel removal"],"7d1a0d":["Public"],"7sNhEz":["Username"],"8ZsakT":["Password"],"8pxhI8":["Please select a file."],"9l4qcT":["Drop a replacement here"],"9uI_rE":["Undo"],"A0y396":["+ Invite someone"],"A1taO8":["Search"],"AQbgNR":["Follow ",["targetUsername"]],"ATGYL1":["Email address"],"AZctoV":[["0","plural",{"one":["#"," comment"],"other":["#"," comments"]}]],"Ade-6d":["Live updates unavailable."],"AeXO77":["Account"],"CI50ct":["Upvoted"],"Cj24wt":["Registering…"],"DPfwMq":["Done"],"DdeHXH":["Delete this dump? This cannot be undone."],"Dp1JhP":["<0>",["0"]," upvoted <1>",["1"],""],"ECiS12":["View dump →"],"ExR0Fr":["URL is required."],"F1O9Ep":["Could not connect to server"],"F5Js1v":["Unfollow playlist"],"FgAxTj":["Log out"],"Fxf4jq":["Description (optional)"],"GNSsCc":["Failed to create playlist"],"GbqhrN":[["0"],"–",["1"]," characters: letters, numbers, or underscores"],"GptGxg":["Change password"],"GsRMX3":["Playlist not found"],"H8pzW-":["Failed to update avatar"],"HTLDA4":["Loading more…"],"I-x669":["Invitees"],"IZX7TO":["Failed to generate invite"],"IagCbF":["URL"],"ImOQa9":["Reply"],"J2eKUI":["File"],"JJ-Bhk":["Your email address"],"JRQitQ":["Confirm new password"],"Jd58Fo":["Hot"],"Jf0PuK":["Go to login"],"K1JdNl":["Username already exists"],"KDGWg5":["Remove from playlist"],"K_F6pa":["Saving…"],"LLyMkV":["Followed (",["0"],["1"],")"],"LPAv9E":[["days"],"d ago"],"Lld1jm":["Tell people about yourself…"],"MHrjPM":["Title"],"MKEPCY":["Follow"],"Mq2B8E":[["mins"],"m ago"],"NR0xa9":["Tell the community what makes this worth their time..."],"Nn4kr3":["+ New dump"],"Oprv1v":["Password (min. ",["0"]," characters)"],"Oz0N9s":["new"],"PiH3UR":["Copied!"],"Pn2B7_":["Current password"],"Pwqkdw":["Loading…"],"Q6n4F4":["Refresh metadata"],"QKsaQr":["or <0>browse files"],"QLtPBd":["No dumps in this playlist yet."],"R9Khdg":["Auto"],"RCcPrX":["Delete this playlist? This cannot be undone."],"RTksSy":["<0>",["0"]," started following you"],"RaKjrM":["Failed to save edit"],"RcUHRT":["Followed"],"SBTElJ":["Searching…"],"Sad2tK":["Sending…"],"Sxm8rQ":["Users"],"T9bjWt":["<0>",["0"]," was added to <1>",["1"],""],"TM1ZbA":["Playlists (",["0"],["1"],")"],"TN382O":["Invalid link"],"Tv9vbB":["Follow playlist"],"Tz0i8g":["Settings"],"U7u3q-":["+ New"],"UNMVei":["Forgot password?"],"UOZith":["Failed to post"],"UTiUFs":["Fetching…"],"VCoEm-":["Back to login"],"V_e7nf":["Set new password"],"VnNJbN":["From playlists"],"VyTYmS":["Change avatar"],"WhimMi":["Reset failed"],"WpXcBJ":["Nothing here yet."],"WtkMN8":["All ",["0","plural",{"one":["#"," upvoted dump"],"other":["#"," upvoted dumps"]}]," loaded."],"XILg0L":["Invalid email address"],"XJy2oN":["Logging in…"],"Xan6QP":["New dump"],"XgRtUf":["Could not change password"],"Xi0Mn4":["← Back to profile"],"XnL-Eu":["No users match \\"",["q"],"\\"."],"Xs2Lez":["This reset link is missing or malformed."],"YK1Dhc":["a post"],"YaSA2K":["Comment not found"],"YpkCca":["No followed playlists yet."],"ZCpU0u":["No playlists match \\"",["q"],"\\"."],"ZmD2o6":["Create & Add"],"_3O5R_":["Request failed"],"_84wxb":["All ",["0","plural",{"one":["#"," dump"],"other":["#"," dumps"]}]," loaded."],"_DwR-n":["Creating…"],"_R_sGB":["Password changed successfully."],"_aept4":["Post reply"],"_nT6AE":["New password"],"_t4W-i":["From people"],"aAIQg2":["Appearance"],"aDvLhk":["Add a comment…"],"alBtu4":["Invalid or expired invite"],"b3Thhd":["Upload failed"],"b8XMJ8":[["visibleCount","plural",{"one":["#"," comment"],"other":["#"," comments"]}]],"bQhwn-":["Loading playlist…"],"cILfnJ":["Remove file"],"cYP9Sb":["+ Playlist"],"cbeBbZ":["At least ",["0"]," characters"],"cnGeoo":["Delete"],"d8DZWS":["Open search"],"dAs22m":["Replace file"],"dEgA5A":["Cancel"],"dMizp8":["New playlist"],"dSKHAa":["Invalid username or password"],"dTU6Wi":["Password must be at most 128 characters"],"dbc28f":["Why are you dumping this?"],"eFSqvc":["Failed to post reply"],"ePK91l":["Edit"],"eaUTwS":["Send reset link"],"ecUA8p":["Today"],"ef9nPf":["Loading dump…"],"en9o7K":["Failed to post comment"],"fGxPOv":["Add a bio…"],"fI-mNw":["Playlists"],"f_akpP":["Max 50 MB"],"fgLNSM":["Register"],"gANddk":["Uploading…"],"gGx5tM":["Editing"],"gIQQwD":["Failed to load"],"gLfZlz":["Add to playlist"],"gjJ-sb":["Can\'t connect to the live updates server. Upvotes and notifications may not sync until it reconnects."],"hBuUKa":["Change password…"],"hD7w09":["You\'ve reached the end."],"hJSliC":["<0>",["0"]," posted <1>",["1"],""],"hYgDIe":["Create"],"he3ygx":["Copy"],"iDNBZe":["Notifications"],"ijVyoK":["Could not reach the server. Please try again."],"ipYn7W":["If that address is registered you\'ll receive a reset link shortly."],"isRobC":["New"],"jGrTH0":["Not authenticated"],"jbernk":["Loading profile…"],"joEmfT":["Server unreachable"],"jrZTZl":["No dumps yet. Be the first!"],"kLttbL":["Registration failed"],"kYYCil":["File too large (max 50 MB)"],"klOeIX":["Failed to change password"],"l3JaOO":["Cannot edit a deleted comment"],"lUDifl":["Created (",["0"],["1"],")"],"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"],"mt6O6E":["This is a mirage."],"nbm5sI":["No dumps match \\"",["q"],"\\"."],"nrjqON":["Checking invite…"],"nwtY4N":["Something went wrong"],"ogtYkT":["Password updated"],"pCpd9p":["<0>",["0"]," mentioned you in <1>",["where"],""],"pSheLH":["No invitees yet."],"pvnfJD":["Dark"],"qIMfNQ":["Delete playlist"],"qbDAcy":["Dump it"],"qgx_78":["Follow some public playlists to see their dumps here."],"qvFa8r":["public"],"rCbqPX":["This invite link is missing, expired, or already used."],"rg9pXu":["Search failed"],"rtpJqV":["Dumps (",["0"],["1"],")"],"sBZMWb":["Invalid URL"],"sQia9P":["Log in"],"sTiqbm":["invited by"],"sdP5Aa":["[deleted]"],"shHs8T":["Enter a query to search."],"siMTjB":["File content is not a recognised image (JPEG, PNG, GIF, WebP)"],"smeBfS":["Invalid invite"],"tfDRzk":["Save"],"tqKwXl":["Username must be 1–32 characters and contain only letters, numbers, or underscores"],"tvmuQ0":["Color scheme"],"u1lDX2":["Fetching preview…"],"u4pkXs":["Invite already used"],"uD0qXQ":["Drop a file here"],"uMGUnV":["No playlists yet."],"ub1EEL":["edited ",["0"]],"vJBF1r":["Posting…"],"vLhLLO":["Notifications (",["unreadNotificationCount"]," unread)"],"vuosjb":["User menu"],"vwGkYB":["Password must be at least 8 characters"],"wXO4Tg":[["hrs"],"h ago"],"wbXKOv":["File too large (max 50 MB)."],"wdiqRH":["Admin access required"],"wixIgH":["Already have an account? <0>Log in"],"x4aBfU":["Dump not found"],"xEWkgZ":["← Back to all dumps"],"xOTzt5":["just now"],"xPHtx0":["Submit search"],"xVuNgt":["+ New playlist"],"xc9O_u":["Delete dump"],"y6sq5j":["Following"],"yA_6BX":["View all →"],"yBBtRm":["Follow some users to see their dumps here."],"yQ2kGp":["Load more"],"y_0uwd":["Yesterday"],"yz7wBu":["Close"],"z1uNN0":["No emoji found."],"zVuxvN":["Refreshing…"],"zwBp5t":["Private"]}', + '{"-K9EZb":["Add email…"],"-OxI15":["Followed playlists"],"-Ya-b9":["Failed to save"],"-siMqD":["Journal"],"0kWhlg":["File too large (max 5 MB)"],"1CalO6":["Style"],"1HfJWf":["Search dumps, users, playlists…"],"1cbYY_":["Upvoted (",["0"],["1"],")"],"1njn7W":["Light"],"1utXA6":["Dumps"],"26iNma":["Post comment"],"29VNqC":["Unknown error"],"2Hlmdt":["Write a reply…"],"2ygf_L":["← Back"],"3KKSM4":["private"],"3T1cI4":["Unexpected server error"],"3yfh3D":["<0>",["0"]," followed your playlist <1>",["1"],""],"49voTZ":[["label"]," (",["count"],")"],"4B6w_o":["Dumped!"],"4GKuCs":["Login failed"],"4HH9iB":["Not following anyone yet."],"4RtQ1k":["Unfollow ",["targetUsername"]],"4c-qBx":["Your password has been changed. You can now log in."],"4yj9xV":["Live updates are temporarily disconnected. Trying to reconnect…"],"5cC8f2":["Edited ",["0"]],"5oD9f_":["Earlier"],"6Qly-0":["a comment"],"6gRgw8":["Retry"],"7JBW66":["Forbidden"],"7PHCIN":["Cancel removal"],"7d1a0d":["Public"],"7sNhEz":["Username"],"8ZsakT":["Password"],"8pxhI8":["Please select a file."],"9l4qcT":["Drop a replacement here"],"9uI_rE":["Undo"],"A0y396":["+ Invite someone"],"A1taO8":["Search"],"AQbgNR":["Follow ",["targetUsername"]],"ATGYL1":["Email address"],"AZctoV":[["0","plural",{"one":["#"," comment"],"other":["#"," comments"]}]],"Ade-6d":["Live updates unavailable."],"AeXO77":["Account"],"CI50ct":["Upvoted"],"Cj24wt":["Registering…"],"DPfwMq":["Done"],"DdeHXH":["Delete this dump? This cannot be undone."],"Dp1JhP":["<0>",["0"]," upvoted <1>",["1"],""],"ECiS12":["View dump →"],"ExR0Fr":["URL is required."],"F1O9Ep":["Could not connect to server"],"F5Js1v":["Unfollow playlist"],"FgAxTj":["Log out"],"Fxf4jq":["Description (optional)"],"GNSsCc":["Failed to create playlist"],"GbqhrN":[["0"],"–",["1"]," characters: letters, numbers, or underscores"],"GptGxg":["Change password"],"GsRMX3":["Playlist not found"],"H8pzW-":["Failed to update avatar"],"HTLDA4":["Loading more…"],"I-x669":["Invitees"],"IZX7TO":["Failed to generate invite"],"IagCbF":["URL"],"ImOQa9":["Reply"],"J2eKUI":["File"],"JJ-Bhk":["Your email address"],"JRQitQ":["Confirm new password"],"JXr41k":["<0>",["0"]," commented on <1>",["1"],""],"Jd58Fo":["Hot"],"Jf0PuK":["Go to login"],"K1JdNl":["Username already exists"],"KDGWg5":["Remove from playlist"],"K_F6pa":["Saving…"],"LLyMkV":["Followed (",["0"],["1"],")"],"LPAv9E":[["days"],"d ago"],"Lld1jm":["Tell people about yourself…"],"MHrjPM":["Title"],"MKEPCY":["Follow"],"Mq2B8E":[["mins"],"m ago"],"NR0xa9":["Tell the community what makes this worth their time..."],"Nn4kr3":["+ New dump"],"Oprv1v":["Password (min. ",["0"]," characters)"],"Oz0N9s":["new"],"PiH3UR":["Copied!"],"Pn2B7_":["Current password"],"Pwqkdw":["Loading…"],"Q6n4F4":["Refresh metadata"],"QKsaQr":["or <0>browse files"],"QLtPBd":["No dumps in this playlist yet."],"R9Khdg":["Auto"],"RCcPrX":["Delete this playlist? This cannot be undone."],"RTksSy":["<0>",["0"]," started following you"],"RaKjrM":["Failed to save edit"],"RcUHRT":["Followed"],"SBTElJ":["Searching…"],"Sad2tK":["Sending…"],"Sxm8rQ":["Users"],"T9bjWt":["<0>",["0"]," was added to <1>",["1"],""],"TM1ZbA":["Playlists (",["0"],["1"],")"],"TN382O":["Invalid link"],"Tv9vbB":["Follow playlist"],"Tz0i8g":["Settings"],"U7u3q-":["+ New"],"UNMVei":["Forgot password?"],"UOZith":["Failed to post"],"UTiUFs":["Fetching…"],"VCoEm-":["Back to login"],"V_e7nf":["Set new password"],"VnNJbN":["From playlists"],"VyTYmS":["Change avatar"],"WhimMi":["Reset failed"],"WpXcBJ":["Nothing here yet."],"WtkMN8":["All ",["0","plural",{"one":["#"," upvoted dump"],"other":["#"," upvoted dumps"]}]," loaded."],"XILg0L":["Invalid email address"],"XJy2oN":["Logging in…"],"Xan6QP":["New dump"],"XgRtUf":["Could not change password"],"Xi0Mn4":["← Back to profile"],"XnL-Eu":["No users match \\"",["q"],"\\"."],"Xs2Lez":["This reset link is missing or malformed."],"YK1Dhc":["a post"],"YaSA2K":["Comment not found"],"YpkCca":["No followed playlists yet."],"ZCpU0u":["No playlists match \\"",["q"],"\\"."],"ZmD2o6":["Create & Add"],"_3O5R_":["Request failed"],"_84wxb":["All ",["0","plural",{"one":["#"," dump"],"other":["#"," dumps"]}]," loaded."],"_DwR-n":["Creating…"],"_R_sGB":["Password changed successfully."],"_aept4":["Post reply"],"_nT6AE":["New password"],"_t4W-i":["From people"],"aAIQg2":["Appearance"],"aDvLhk":["Add a comment…"],"alBtu4":["Invalid or expired invite"],"b3Thhd":["Upload failed"],"b8XMJ8":[["visibleCount","plural",{"one":["#"," comment"],"other":["#"," comments"]}]],"bQhwn-":["Loading playlist…"],"cILfnJ":["Remove file"],"cYP9Sb":["+ Playlist"],"cbeBbZ":["At least ",["0"]," characters"],"cnGeoo":["Delete"],"d8DZWS":["Open search"],"dAs22m":["Replace file"],"dEgA5A":["Cancel"],"dMizp8":["New playlist"],"dSKHAa":["Invalid username or password"],"dTU6Wi":["Password must be at most 128 characters"],"dbc28f":["Why are you dumping this?"],"eFSqvc":["Failed to post reply"],"ePK91l":["Edit"],"eaUTwS":["Send reset link"],"ecUA8p":["Today"],"ef9nPf":["Loading dump…"],"en9o7K":["Failed to post comment"],"fGxPOv":["Add a bio…"],"fI-mNw":["Playlists"],"f_akpP":["Max 50 MB"],"fgLNSM":["Register"],"gANddk":["Uploading…"],"gGx5tM":["Editing"],"gIQQwD":["Failed to load"],"gLfZlz":["Add to playlist"],"gjJ-sb":["Can\'t connect to the live updates server. Upvotes and notifications may not sync until it reconnects."],"hBuUKa":["Change password…"],"hD7w09":["You\'ve reached the end."],"hJSliC":["<0>",["0"]," posted <1>",["1"],""],"hYgDIe":["Create"],"he3ygx":["Copy"],"iDNBZe":["Notifications"],"ijVyoK":["Could not reach the server. Please try again."],"ipYn7W":["If that address is registered you\'ll receive a reset link shortly."],"isRobC":["New"],"jGrTH0":["Not authenticated"],"jbernk":["Loading profile…"],"joEmfT":["Server unreachable"],"jrZTZl":["No dumps yet. Be the first!"],"kLttbL":["Registration failed"],"kYYCil":["File too large (max 50 MB)"],"klOeIX":["Failed to change password"],"l3JaOO":["Cannot edit a deleted comment"],"lUDifl":["Created (",["0"],["1"],")"],"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"],"mt6O6E":["This is a mirage."],"nbm5sI":["No dumps match \\"",["q"],"\\"."],"nrjqON":["Checking invite…"],"nwtY4N":["Something went wrong"],"ogtYkT":["Password updated"],"pCpd9p":["<0>",["0"]," mentioned you in <1>",["where"],""],"pSheLH":["No invitees yet."],"pvnfJD":["Dark"],"qIMfNQ":["Delete playlist"],"qbDAcy":["Dump it"],"qgx_78":["Follow some public playlists to see their dumps here."],"qvFa8r":["public"],"rCbqPX":["This invite link is missing, expired, or already used."],"rg9pXu":["Search failed"],"rtpJqV":["Dumps (",["0"],["1"],")"],"sBZMWb":["Invalid URL"],"sQia9P":["Log in"],"sTiqbm":["invited by"],"sdP5Aa":["[deleted]"],"shHs8T":["Enter a query to search."],"siMTjB":["File content is not a recognised image (JPEG, PNG, GIF, WebP)"],"smeBfS":["Invalid invite"],"tfDRzk":["Save"],"tqKwXl":["Username must be 1–32 characters and contain only letters, numbers, or underscores"],"tvmuQ0":["Color scheme"],"u1lDX2":["Fetching preview…"],"u4pkXs":["Invite already used"],"uD0qXQ":["Drop a file here"],"uMGUnV":["No playlists yet."],"ub1EEL":["edited ",["0"]],"vJBF1r":["Posting…"],"vLhLLO":["Notifications (",["unreadNotificationCount"]," unread)"],"vuosjb":["User menu"],"vwGkYB":["Password must be at least 8 characters"],"wXO4Tg":[["hrs"],"h ago"],"wbXKOv":["File too large (max 50 MB)."],"wdiqRH":["Admin access required"],"wixIgH":["Already have an account? <0>Log in"],"x4aBfU":["Dump not found"],"xEWkgZ":["← Back to all dumps"],"xOTzt5":["just now"],"xPHtx0":["Submit search"],"xVuNgt":["+ New playlist"],"xc9O_u":["Delete dump"],"y6sq5j":["Following"],"yA_6BX":["View all →"],"yBBtRm":["Follow some users to see their dumps here."],"yQ2kGp":["Load more"],"y_0uwd":["Yesterday"],"yz7wBu":["Close"],"z1uNN0":["No emoji found."],"zVuxvN":["Refreshing…"],"zwBp5t":["Private"]}', ), }; diff --git a/src/locales/en.po b/src/locales/en.po index e656cb6..08f3f05 100644 --- a/src/locales/en.po +++ b/src/locales/en.po @@ -92,45 +92,51 @@ msgstr "+ New playlist" msgid "+ Playlist" msgstr "+ Playlist" +#. placeholder {0}: d.commenterUsername +#. placeholder {1}: d.dumpTitle +#: src/pages/Notifications.tsx:171 +msgid "<0>{0} commented on <1>{1}" +msgstr "<0>{0} commented on <1>{1}" + #. placeholder {0}: d.followerUsername #. placeholder {1}: d.playlistTitle -#: src/pages/Notifications.tsx:124 +#: src/pages/Notifications.tsx:131 msgid "<0>{0} followed your playlist <1>{1}" msgstr "<0>{0} followed your playlist <1>{1}" #. placeholder {0}: d.mentionerUsername -#: src/pages/Notifications.tsx:166 +#: src/pages/Notifications.tsx:183 msgid "<0>{0} mentioned you in <1>{where}" msgstr "<0>{0} mentioned you in <1>{where}" #. placeholder {0}: d.dumperUsername #. placeholder {1}: d.dumpTitle -#: src/pages/Notifications.tsx:134 +#: src/pages/Notifications.tsx:141 msgid "<0>{0} posted <1>{1}" msgstr "<0>{0} posted <1>{1}" #. placeholder {0}: d.followerUsername -#: src/pages/Notifications.tsx:115 +#: src/pages/Notifications.tsx:122 msgid "<0>{0} started following you" msgstr "<0>{0} started following you" #. placeholder {0}: d.voterUsername #. placeholder {1}: d.dumpTitle -#: src/pages/Notifications.tsx:154 +#: src/pages/Notifications.tsx:161 msgid "<0>{0} upvoted <1>{1}" msgstr "<0>{0} upvoted <1>{1}" #. placeholder {0}: d.dumpTitle #. placeholder {1}: d.playlistTitle -#: src/pages/Notifications.tsx:144 +#: src/pages/Notifications.tsx:151 msgid "<0>{0} was added to <1>{1}" msgstr "<0>{0} was added to <1>{1}" -#: src/pages/Notifications.tsx:164 +#: src/pages/Notifications.tsx:181 msgid "a comment" msgstr "a comment" -#: src/pages/Notifications.tsx:164 +#: src/pages/Notifications.tsx:181 msgid "a post" msgstr "a post" @@ -373,7 +379,7 @@ msgstr "Dumps" msgid "Dumps ({0}{1})" msgstr "Dumps ({0}{1})" -#: src/pages/Notifications.tsx:349 +#: src/pages/Notifications.tsx:360 msgid "Earlier" msgstr "Earlier" @@ -432,6 +438,7 @@ msgstr "Failed to generate invite" #: src/pages/index/HotFeed.tsx:36 #: src/pages/index/JournalFeed.tsx:48 #: src/pages/index/NewFeed.tsx:36 +#: src/pages/Notifications.tsx:334 #: src/pages/UserPublicProfile.tsx:1106 #: src/pages/UserPublicProfile.tsx:1148 #: src/pages/UserPublicProfile.tsx:1193 @@ -623,7 +630,7 @@ msgstr "Live updates are temporarily disconnected. Trying to reconnect…" msgid "Live updates unavailable." msgstr "Live updates unavailable." -#: src/pages/Notifications.tsx:390 +#: src/pages/Notifications.tsx:407 msgid "Load more" msgstr "Load more" @@ -658,8 +665,8 @@ msgstr "Loading profile…" #: src/pages/index/HotFeed.tsx:32 #: src/pages/index/JournalFeed.tsx:44 #: src/pages/index/NewFeed.tsx:32 -#: src/pages/Notifications.tsx:313 -#: src/pages/Notifications.tsx:389 +#: src/pages/Notifications.tsx:330 +#: src/pages/Notifications.tsx:406 #: src/pages/UserDumps.tsx:51 #: src/pages/UserPlaylists.tsx:342 #: src/pages/UserPublicProfile.tsx:1100 @@ -692,7 +699,7 @@ msgstr "Login failed" msgid "Max 50 MB" msgstr "Max 50 MB" -#: src/pages/Notifications.tsx:306 +#: src/pages/Notifications.tsx:323 msgid "new" msgstr "new" @@ -762,7 +769,7 @@ msgstr "No users match \"{q}\"." msgid "Not following anyone yet." msgstr "Not following anyone yet." -#: src/pages/Notifications.tsx:324 +#: src/pages/Notifications.tsx:341 #: src/pages/UserDumps.tsx:123 #: src/pages/UserPublicProfile.tsx:1340 #: src/pages/UserPublicProfile.tsx:1463 @@ -771,7 +778,7 @@ msgid "Nothing here yet." msgstr "Nothing here yet." #: src/components/NotificationBell.tsx:42 -#: src/pages/Notifications.tsx:302 +#: src/pages/Notifications.tsx:319 msgid "Notifications" msgstr "Notifications" @@ -1021,7 +1028,7 @@ msgstr "This reset link is missing or malformed." msgid "Title" msgstr "Title" -#: src/pages/Notifications.tsx:346 +#: src/pages/Notifications.tsx:357 msgid "Today" msgstr "Today" @@ -1115,11 +1122,11 @@ msgstr "Why are you dumping this?" msgid "Write a reply…" msgstr "Write a reply…" -#: src/pages/Notifications.tsx:348 +#: src/pages/Notifications.tsx:359 msgid "Yesterday" msgstr "Yesterday" -#: src/pages/Notifications.tsx:327 +#: src/pages/Notifications.tsx:344 msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." diff --git a/src/locales/fr.js b/src/locales/fr.js index bd608d2..b9054dd 100644 --- a/src/locales/fr.js +++ b/src/locales/fr.js @@ -1,5 +1,5 @@ /*eslint-disable*/ module.exports = { messages: JSON.parse( - '{"-K9EZb":["Ajouter un e-mail…"],"-OxI15":["Collections suivies"],"-Ya-b9":["Enregistrement échoué"],"-siMqD":["Journal"],"0kWhlg":["File too large (max 5 MB)"],"1CalO6":["Style"],"1HfJWf":["Rechercher des recos, utilisateurs, collections…"],"1cbYY_":["Votés (",["0"],["1"],")"],"1njn7W":["Clair"],"1utXA6":["Recos"],"26iNma":["Publier le commentaire"],"29VNqC":["Erreur inconnue"],"2Hlmdt":["Écrire une réponse…"],"2ygf_L":["← Retour"],"3KKSM4":["privé"],"3T1cI4":["Unexpected server error"],"3yfh3D":["<0>",["0"]," a suivi votre collection <1>",["1"],""],"49voTZ":[["label"]," (",["count"],")"],"4B6w_o":["Recommandé !"],"4GKuCs":["Connexion échouée"],"4HH9iB":["Aucun abonnement pour le moment."],"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…"],"5cC8f2":["Modifié le ",["0"]],"5oD9f_":["Plus tôt"],"6Qly-0":["un commentaire"],"6gRgw8":["Réessayer"],"7JBW66":["Forbidden"],"7PHCIN":["Annuler la suppression"],"7d1a0d":["Public"],"7sNhEz":["Nom d\'utilisateur"],"8ZsakT":["Mot de passe"],"8pxhI8":["Veuillez sélectionner un fichier."],"9l4qcT":["Déposez un fichier de remplacement ici"],"9uI_rE":["Annuler"],"A0y396":["+ Inviter quelqu\'un"],"A1taO8":["Rechercher"],"AQbgNR":["Suivre ",["targetUsername"]],"ATGYL1":["Adresse e-mail"],"AZctoV":[["0","plural",{"one":["#"," commentaire"],"other":["#"," commentaires"]}]],"Ade-6d":["Mises à jour en direct indisponibles."],"AeXO77":["Compte"],"CI50ct":["Voté"],"Cj24wt":["Inscription…"],"DPfwMq":["Terminé"],"DdeHXH":["Supprimer cette reco ? Cette action est irréversible."],"Dp1JhP":["<0>",["0"]," a voté pour <1>",["1"],""],"ECiS12":["Voir la reco →"],"ExR0Fr":["L\'URL est obligatoire."],"F1O9Ep":["Impossible de contacter le serveur"],"F5Js1v":["Ne plus suivre la collection"],"FgAxTj":["Se déconnecter"],"Fxf4jq":["Description (facultatif)"],"GNSsCc":["Impossible de créer la collection"],"GbqhrN":[["0"],"–",["1"]," caractères : lettres, chiffres ou tirets bas"],"GptGxg":["Changer le mot de passe"],"GsRMX3":["Playlist not found"],"H8pzW-":["Impossible de mettre à jour l\'avatar"],"HTLDA4":["Chargement…"],"I-x669":["Invités"],"IZX7TO":["Impossible de générer une invitation"],"IagCbF":["URL"],"ImOQa9":["Répondre"],"J2eKUI":["Fichier"],"JJ-Bhk":["Votre adresse e-mail"],"JRQitQ":["Confirmer le nouveau mot de passe"],"Jd58Fo":["Tendances"],"Jf0PuK":["Aller à la connexion"],"K1JdNl":["Username already exists"],"KDGWg5":["Retirer de la collection"],"K_F6pa":["Enregistrement…"],"LLyMkV":["Suivies (",["0"],["1"],")"],"LPAv9E":["il y a ",["days"],"j"],"Lld1jm":["Parlez de vous…"],"MHrjPM":["Titre"],"MKEPCY":["Suivre"],"Mq2B8E":["il y a ",["mins"],"min"],"NR0xa9":["Dites à la communauté pourquoi ça vaut le coup…"],"Nn4kr3":["+ Nouvelle reco"],"Oprv1v":["Mot de passe (min. ",["0"]," caractères)"],"Oz0N9s":["nouveau"],"PiH3UR":["Copié !"],"Pn2B7_":["Mot de passe actuel"],"Pwqkdw":["Chargement…"],"Q6n4F4":["Actualiser les métadonnées"],"QKsaQr":["ou <0>parcourir les fichiers"],"QLtPBd":["Aucune reco dans cette collection pour l\'instant."],"R9Khdg":["Auto"],"RCcPrX":["Supprimer cette collection ? Cette action est irréversible."],"RTksSy":["<0>",["0"]," a commencé à vous suivre"],"RaKjrM":["Impossible d\'enregistrer la modification"],"RcUHRT":["Suivi"],"SBTElJ":["Recherche…"],"Sad2tK":["Envoi…"],"Sxm8rQ":["Utilisateurs"],"T9bjWt":["<0>",["0"]," a été ajouté à <1>",["1"],""],"TM1ZbA":["Collections (",["0"],["1"],")"],"TN382O":["Lien invalide"],"Tv9vbB":["Suivre la collection"],"Tz0i8g":["Paramètres"],"U7u3q-":["+ Nouveau"],"UNMVei":["Mot de passe oublié ?"],"UOZith":["Publication échouée"],"UTiUFs":["Récupération…"],"VCoEm-":["Retour à la connexion"],"V_e7nf":["Définir un nouveau mot de passe"],"VnNJbN":["De collections"],"VyTYmS":["Changer l\'avatar"],"WhimMi":["Échec de la réinitialisation"],"WpXcBJ":["Rien ici pour l\'instant."],"WtkMN8":["Toutes les ",["0","plural",{"one":["#"," reco votée"],"other":["#"," recos votées"]}]," chargées."],"XILg0L":["Invalid email address"],"XJy2oN":["Connexion…"],"Xan6QP":["Nouvelle reco"],"XgRtUf":["Impossible de changer le mot de passe"],"Xi0Mn4":["← Retour au profil"],"XnL-Eu":["Aucun utilisateur ne correspond à « ",["q"]," »."],"Xs2Lez":["Ce lien de réinitialisation est absent ou malformé."],"YK1Dhc":["une publication"],"YaSA2K":["Comment not found"],"YpkCca":["Pas encore de collections suivies."],"ZCpU0u":["Aucune collection ne correspond à « ",["q"]," »."],"ZmD2o6":["Créer et ajouter"],"_3O5R_":["Échec de la demande"],"_84wxb":["Toutes les ",["0","plural",{"one":["#"," reco"],"other":["#"," recos"]}]," chargées."],"_DwR-n":["Création…"],"_R_sGB":["Mot de passe modifié avec succès."],"_aept4":["Publier la réponse"],"_nT6AE":["Nouveau mot de passe"],"_t4W-i":["De personnes"],"aAIQg2":["Apparence"],"aDvLhk":["Ajouter un commentaire…"],"alBtu4":["Invalid or expired invite"],"b3Thhd":["Envoi échoué"],"b8XMJ8":[["visibleCount","plural",{"one":["#"," commentaire"],"other":["#"," commentaires"]}]],"bQhwn-":["Chargement de la collection…"],"cILfnJ":["Supprimer le fichier"],"cYP9Sb":["+ Collection"],"cbeBbZ":["Au moins ",["0"]," caractères"],"cnGeoo":["Supprimer"],"d8DZWS":["Ouvrir la recherche"],"dAs22m":["Remplacer le fichier"],"dEgA5A":["Annuler"],"dMizp8":["Nouvelle collection"],"dSKHAa":["Invalid username or password"],"dTU6Wi":["Password must be at most 128 characters"],"dbc28f":["Pourquoi recommandez-vous ça ?"],"eFSqvc":["Impossible de publier la réponse"],"ePK91l":["Modifier"],"eaUTwS":["Envoyer le lien de réinitialisation"],"ecUA8p":["Aujourd\'hui"],"ef9nPf":["Chargement de la reco…"],"en9o7K":["Impossible de publier le commentaire"],"fGxPOv":["Ajouter une bio…"],"fI-mNw":["Collections"],"f_akpP":["Max 50 Mo"],"fgLNSM":["S\'inscrire"],"gANddk":["Envoi…"],"gGx5tM":["Modification"],"gIQQwD":["Chargement échoué"],"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."],"hBuUKa":["Changer le mot de passe…"],"hD7w09":["Vous avez tout lu, tout vu, tout bu."],"hJSliC":["<0>",["0"]," a publié <1>",["1"],""],"hYgDIe":["Créer"],"he3ygx":["Copier"],"iDNBZe":["Notifications"],"ijVyoK":["Impossible de contacter le serveur. Veuillez réessayer."],"ipYn7W":["Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu."],"isRobC":["Nouveau"],"jGrTH0":["Not authenticated"],"jbernk":["Chargement du profil…"],"joEmfT":["Serveur inaccessible"],"jrZTZl":["Pas encore de recos. Soyez le premier !"],"kLttbL":["Inscription échouée"],"kYYCil":["File too large (max 50 MB)"],"klOeIX":["Impossible de changer le mot de passe"],"l3JaOO":["Cannot edit a deleted comment"],"lUDifl":["Créées (",["0"],["1"],")"],"lUanmi":["Vous serez notifié lorsque quelqu\'un suit vos collections, vote pour 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"],"mt6O6E":["C\'est un mirage."],"nbm5sI":["Aucune reco ne correspond à « ",["q"]," »."],"nrjqON":["Vérification de l\'invitation…"],"nwtY4N":["Une erreur est survenue"],"ogtYkT":["Mot de passe mis à jour"],"pCpd9p":["<0>",["0"]," vous a mentionné dans <1>",["where"],""],"pSheLH":["Aucun invité pour le moment."],"pvnfJD":["Sombre"],"qIMfNQ":["Supprimer la collection"],"qbDAcy":["Recommander"],"qgx_78":["Suivez des collections publiques pour voir leurs recos ici."],"qvFa8r":["public"],"rCbqPX":["Ce lien d\'invitation est manquant, expiré ou déjà utilisé."],"rg9pXu":["Recherche échouée"],"rtpJqV":["Recos (",["0"],["1"],")"],"sBZMWb":["Invalid URL"],"sQia9P":["Se connecter"],"sTiqbm":["invité par"],"sdP5Aa":["[supprimé]"],"shHs8T":["Saisissez une recherche."],"siMTjB":["File content is not a recognised image (JPEG, PNG, GIF, WebP)"],"smeBfS":["Invitation invalide"],"tfDRzk":["Enregistrer"],"tqKwXl":["Username must be 1–32 characters and contain only letters, numbers, or underscores"],"tvmuQ0":["Thème de couleur"],"u1lDX2":["Récupération de l\'aperçu…"],"u4pkXs":["Invite already used"],"uD0qXQ":["Déposez un fichier ici"],"uMGUnV":["Pas encore de collections."],"ub1EEL":["modifié ",["0"]],"vJBF1r":["Publication…"],"vLhLLO":["Notifications (",["unreadNotificationCount"]," non lues)"],"vuosjb":["Menu utilisateur"],"vwGkYB":["Password must be at least 8 characters"],"wXO4Tg":["il y a ",["hrs"],"h"],"wbXKOv":["Fichier trop volumineux (max 50 Mo)."],"wdiqRH":["Admin access required"],"wixIgH":["Vous avez déjà un compte ? <0>Se connecter"],"x4aBfU":["Dump not found"],"xEWkgZ":["← Retour à toutes les recos"],"xOTzt5":["à l\'instant"],"xPHtx0":["Lancer la recherche"],"xVuNgt":["+ Nouvelle collection"],"xc9O_u":["Supprimer la reco"],"y6sq5j":["Abonné"],"yA_6BX":["Tout voir →"],"yBBtRm":["Suivez des utilisateurs pour voir leurs recos ici."],"yQ2kGp":["Charger plus"],"y_0uwd":["Hier"],"yz7wBu":["Fermer"],"z1uNN0":["Aucun emoji trouvé."],"zVuxvN":["Actualisation…"],"zwBp5t":["Privé"]}', + '{"-K9EZb":["Ajouter un e-mail…"],"-OxI15":["Collections suivies"],"-Ya-b9":["Enregistrement échoué"],"-siMqD":["Journal"],"0kWhlg":["File too large (max 5 MB)"],"1CalO6":["Style"],"1HfJWf":["Rechercher des recos, utilisateurs, collections…"],"1cbYY_":["Votés (",["0"],["1"],")"],"1njn7W":["Clair"],"1utXA6":["Recos"],"26iNma":["Publier le commentaire"],"29VNqC":["Erreur inconnue"],"2Hlmdt":["Écrire une réponse…"],"2ygf_L":["← Retour"],"3KKSM4":["privé"],"3T1cI4":["Unexpected server error"],"3yfh3D":["<0>",["0"]," a suivi votre collection <1>",["1"],""],"49voTZ":[["label"]," (",["count"],")"],"4B6w_o":["Recommandé !"],"4GKuCs":["Connexion échouée"],"4HH9iB":["Aucun abonnement pour le moment."],"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…"],"5cC8f2":["Modifié le ",["0"]],"5oD9f_":["Plus tôt"],"6Qly-0":["un commentaire"],"6gRgw8":["Réessayer"],"7JBW66":["Forbidden"],"7PHCIN":["Annuler la suppression"],"7d1a0d":["Public"],"7sNhEz":["Nom d\'utilisateur"],"8ZsakT":["Mot de passe"],"8pxhI8":["Veuillez sélectionner un fichier."],"9l4qcT":["Déposez un fichier de remplacement ici"],"9uI_rE":["Annuler"],"A0y396":["+ Inviter quelqu\'un"],"A1taO8":["Rechercher"],"AQbgNR":["Suivre ",["targetUsername"]],"ATGYL1":["Adresse e-mail"],"AZctoV":[["0","plural",{"one":["#"," commentaire"],"other":["#"," commentaires"]}]],"Ade-6d":["Mises à jour en direct indisponibles."],"AeXO77":["Compte"],"CI50ct":["Voté"],"Cj24wt":["Inscription…"],"DPfwMq":["Terminé"],"DdeHXH":["Supprimer cette reco ? Cette action est irréversible."],"Dp1JhP":["<0>",["0"]," a voté pour <1>",["1"],""],"ECiS12":["Voir la reco →"],"ExR0Fr":["L\'URL est obligatoire."],"F1O9Ep":["Impossible de contacter le serveur"],"F5Js1v":["Ne plus suivre la collection"],"FgAxTj":["Se déconnecter"],"Fxf4jq":["Description (facultatif)"],"GNSsCc":["Impossible de créer la collection"],"GbqhrN":[["0"],"–",["1"]," caractères : lettres, chiffres ou tirets bas"],"GptGxg":["Changer le mot de passe"],"GsRMX3":["Playlist not found"],"H8pzW-":["Impossible de mettre à jour l\'avatar"],"HTLDA4":["Chargement…"],"I-x669":["Invités"],"IZX7TO":["Impossible de générer une invitation"],"IagCbF":["URL"],"ImOQa9":["Répondre"],"J2eKUI":["Fichier"],"JJ-Bhk":["Votre adresse e-mail"],"JRQitQ":["Confirmer le nouveau mot de passe"],"JXr41k":["<0>",["0"]," commented on <1>",["1"],""],"Jd58Fo":["Tendances"],"Jf0PuK":["Aller à la connexion"],"K1JdNl":["Username already exists"],"KDGWg5":["Retirer de la collection"],"K_F6pa":["Enregistrement…"],"LLyMkV":["Suivies (",["0"],["1"],")"],"LPAv9E":["il y a ",["days"],"j"],"Lld1jm":["Parlez de vous…"],"MHrjPM":["Titre"],"MKEPCY":["Suivre"],"Mq2B8E":["il y a ",["mins"],"min"],"NR0xa9":["Dites à la communauté pourquoi ça vaut le coup…"],"Nn4kr3":["+ Nouvelle reco"],"Oprv1v":["Mot de passe (min. ",["0"]," caractères)"],"Oz0N9s":["nouveau"],"PiH3UR":["Copié !"],"Pn2B7_":["Mot de passe actuel"],"Pwqkdw":["Chargement…"],"Q6n4F4":["Actualiser les métadonnées"],"QKsaQr":["ou <0>parcourir les fichiers"],"QLtPBd":["Aucune reco dans cette collection pour l\'instant."],"R9Khdg":["Auto"],"RCcPrX":["Supprimer cette collection ? Cette action est irréversible."],"RTksSy":["<0>",["0"]," a commencé à vous suivre"],"RaKjrM":["Impossible d\'enregistrer la modification"],"RcUHRT":["Suivi"],"SBTElJ":["Recherche…"],"Sad2tK":["Envoi…"],"Sxm8rQ":["Utilisateurs"],"T9bjWt":["<0>",["0"]," a été ajouté à <1>",["1"],""],"TM1ZbA":["Collections (",["0"],["1"],")"],"TN382O":["Lien invalide"],"Tv9vbB":["Suivre la collection"],"Tz0i8g":["Paramètres"],"U7u3q-":["+ Nouveau"],"UNMVei":["Mot de passe oublié ?"],"UOZith":["Publication échouée"],"UTiUFs":["Récupération…"],"VCoEm-":["Retour à la connexion"],"V_e7nf":["Définir un nouveau mot de passe"],"VnNJbN":["De collections"],"VyTYmS":["Changer l\'avatar"],"WhimMi":["Échec de la réinitialisation"],"WpXcBJ":["Rien ici pour l\'instant."],"WtkMN8":["Toutes les ",["0","plural",{"one":["#"," reco votée"],"other":["#"," recos votées"]}]," chargées."],"XILg0L":["Invalid email address"],"XJy2oN":["Connexion…"],"Xan6QP":["Nouvelle reco"],"XgRtUf":["Impossible de changer le mot de passe"],"Xi0Mn4":["← Retour au profil"],"XnL-Eu":["Aucun utilisateur ne correspond à « ",["q"]," »."],"Xs2Lez":["Ce lien de réinitialisation est absent ou malformé."],"YK1Dhc":["une publication"],"YaSA2K":["Comment not found"],"YpkCca":["Pas encore de collections suivies."],"ZCpU0u":["Aucune collection ne correspond à « ",["q"]," »."],"ZmD2o6":["Créer et ajouter"],"_3O5R_":["Échec de la demande"],"_84wxb":["Toutes les ",["0","plural",{"one":["#"," reco"],"other":["#"," recos"]}]," chargées."],"_DwR-n":["Création…"],"_R_sGB":["Mot de passe modifié avec succès."],"_aept4":["Publier la réponse"],"_nT6AE":["Nouveau mot de passe"],"_t4W-i":["De personnes"],"aAIQg2":["Apparence"],"aDvLhk":["Ajouter un commentaire…"],"alBtu4":["Invalid or expired invite"],"b3Thhd":["Envoi échoué"],"b8XMJ8":[["visibleCount","plural",{"one":["#"," commentaire"],"other":["#"," commentaires"]}]],"bQhwn-":["Chargement de la collection…"],"cILfnJ":["Supprimer le fichier"],"cYP9Sb":["+ Collection"],"cbeBbZ":["Au moins ",["0"]," caractères"],"cnGeoo":["Supprimer"],"d8DZWS":["Ouvrir la recherche"],"dAs22m":["Remplacer le fichier"],"dEgA5A":["Annuler"],"dMizp8":["Nouvelle collection"],"dSKHAa":["Invalid username or password"],"dTU6Wi":["Password must be at most 128 characters"],"dbc28f":["Pourquoi recommandez-vous ça ?"],"eFSqvc":["Impossible de publier la réponse"],"ePK91l":["Modifier"],"eaUTwS":["Envoyer le lien de réinitialisation"],"ecUA8p":["Aujourd\'hui"],"ef9nPf":["Chargement de la reco…"],"en9o7K":["Impossible de publier le commentaire"],"fGxPOv":["Ajouter une bio…"],"fI-mNw":["Collections"],"f_akpP":["Max 50 Mo"],"fgLNSM":["S\'inscrire"],"gANddk":["Envoi…"],"gGx5tM":["Modification"],"gIQQwD":["Chargement échoué"],"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."],"hBuUKa":["Changer le mot de passe…"],"hD7w09":["Vous avez tout lu, tout vu, tout bu."],"hJSliC":["<0>",["0"]," a publié <1>",["1"],""],"hYgDIe":["Créer"],"he3ygx":["Copier"],"iDNBZe":["Notifications"],"ijVyoK":["Impossible de contacter le serveur. Veuillez réessayer."],"ipYn7W":["Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu."],"isRobC":["Nouveau"],"jGrTH0":["Not authenticated"],"jbernk":["Chargement du profil…"],"joEmfT":["Serveur inaccessible"],"jrZTZl":["Pas encore de recos. Soyez le premier !"],"kLttbL":["Inscription échouée"],"kYYCil":["File too large (max 50 MB)"],"klOeIX":["Impossible de changer le mot de passe"],"l3JaOO":["Cannot edit a deleted comment"],"lUDifl":["Créées (",["0"],["1"],")"],"lUanmi":["Vous serez notifié lorsque quelqu\'un suit vos collections, vote pour 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"],"mt6O6E":["C\'est un mirage."],"nbm5sI":["Aucune reco ne correspond à « ",["q"]," »."],"nrjqON":["Vérification de l\'invitation…"],"nwtY4N":["Une erreur est survenue"],"ogtYkT":["Mot de passe mis à jour"],"pCpd9p":["<0>",["0"]," vous a mentionné dans <1>",["where"],""],"pSheLH":["Aucun invité pour le moment."],"pvnfJD":["Sombre"],"qIMfNQ":["Supprimer la collection"],"qbDAcy":["Recommander"],"qgx_78":["Suivez des collections publiques pour voir leurs recos ici."],"qvFa8r":["public"],"rCbqPX":["Ce lien d\'invitation est manquant, expiré ou déjà utilisé."],"rg9pXu":["Recherche échouée"],"rtpJqV":["Recos (",["0"],["1"],")"],"sBZMWb":["Invalid URL"],"sQia9P":["Se connecter"],"sTiqbm":["invité par"],"sdP5Aa":["[supprimé]"],"shHs8T":["Saisissez une recherche."],"siMTjB":["File content is not a recognised image (JPEG, PNG, GIF, WebP)"],"smeBfS":["Invitation invalide"],"tfDRzk":["Enregistrer"],"tqKwXl":["Username must be 1–32 characters and contain only letters, numbers, or underscores"],"tvmuQ0":["Thème de couleur"],"u1lDX2":["Récupération de l\'aperçu…"],"u4pkXs":["Invite already used"],"uD0qXQ":["Déposez un fichier ici"],"uMGUnV":["Pas encore de collections."],"ub1EEL":["modifié ",["0"]],"vJBF1r":["Publication…"],"vLhLLO":["Notifications (",["unreadNotificationCount"]," non lues)"],"vuosjb":["Menu utilisateur"],"vwGkYB":["Password must be at least 8 characters"],"wXO4Tg":["il y a ",["hrs"],"h"],"wbXKOv":["Fichier trop volumineux (max 50 Mo)."],"wdiqRH":["Admin access required"],"wixIgH":["Vous avez déjà un compte ? <0>Se connecter"],"x4aBfU":["Dump not found"],"xEWkgZ":["← Retour à toutes les recos"],"xOTzt5":["à l\'instant"],"xPHtx0":["Lancer la recherche"],"xVuNgt":["+ Nouvelle collection"],"xc9O_u":["Supprimer la reco"],"y6sq5j":["Abonné"],"yA_6BX":["Tout voir →"],"yBBtRm":["Suivez des utilisateurs pour voir leurs recos ici."],"yQ2kGp":["Charger plus"],"y_0uwd":["Hier"],"yz7wBu":["Fermer"],"z1uNN0":["Aucun emoji trouvé."],"zVuxvN":["Actualisation…"],"zwBp5t":["Privé"]}', ), }; diff --git a/src/locales/fr.po b/src/locales/fr.po index 19952f2..9f32ac3 100644 --- a/src/locales/fr.po +++ b/src/locales/fr.po @@ -92,45 +92,51 @@ msgstr "+ Nouvelle collection" msgid "+ Playlist" msgstr "+ Collection" +#. placeholder {0}: d.commenterUsername +#. placeholder {1}: d.dumpTitle +#: src/pages/Notifications.tsx:171 +msgid "<0>{0} commented on <1>{1}" +msgstr "" + #. placeholder {0}: d.followerUsername #. placeholder {1}: d.playlistTitle -#: src/pages/Notifications.tsx:124 +#: src/pages/Notifications.tsx:131 msgid "<0>{0} followed your playlist <1>{1}" msgstr "<0>{0} a suivi votre collection <1>{1}" #. placeholder {0}: d.mentionerUsername -#: src/pages/Notifications.tsx:166 +#: src/pages/Notifications.tsx:183 msgid "<0>{0} mentioned you in <1>{where}" msgstr "<0>{0} vous a mentionné dans <1>{where}" #. placeholder {0}: d.dumperUsername #. placeholder {1}: d.dumpTitle -#: src/pages/Notifications.tsx:134 +#: src/pages/Notifications.tsx:141 msgid "<0>{0} posted <1>{1}" msgstr "<0>{0} a publié <1>{1}" #. placeholder {0}: d.followerUsername -#: src/pages/Notifications.tsx:115 +#: src/pages/Notifications.tsx:122 msgid "<0>{0} started following you" msgstr "<0>{0} a commencé à vous suivre" #. placeholder {0}: d.voterUsername #. placeholder {1}: d.dumpTitle -#: src/pages/Notifications.tsx:154 +#: src/pages/Notifications.tsx:161 msgid "<0>{0} upvoted <1>{1}" msgstr "<0>{0} a voté pour <1>{1}" #. placeholder {0}: d.dumpTitle #. placeholder {1}: d.playlistTitle -#: src/pages/Notifications.tsx:144 +#: src/pages/Notifications.tsx:151 msgid "<0>{0} was added to <1>{1}" msgstr "<0>{0} a été ajouté à <1>{1}" -#: src/pages/Notifications.tsx:164 +#: src/pages/Notifications.tsx:181 msgid "a comment" msgstr "un commentaire" -#: src/pages/Notifications.tsx:164 +#: src/pages/Notifications.tsx:181 msgid "a post" msgstr "une publication" @@ -357,7 +363,7 @@ msgstr "Recos" msgid "Dumps ({0}{1})" msgstr "Recos ({0}{1})" -#: src/pages/Notifications.tsx:349 +#: src/pages/Notifications.tsx:360 msgid "Earlier" msgstr "Plus tôt" @@ -416,6 +422,7 @@ msgstr "Impossible de générer une invitation" #: src/pages/index/HotFeed.tsx:36 #: src/pages/index/JournalFeed.tsx:48 #: src/pages/index/NewFeed.tsx:36 +#: src/pages/Notifications.tsx:334 #: src/pages/UserPublicProfile.tsx:1106 #: src/pages/UserPublicProfile.tsx:1148 #: src/pages/UserPublicProfile.tsx:1193 @@ -570,7 +577,7 @@ msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative msgid "Live updates unavailable." msgstr "Mises à jour en direct indisponibles." -#: src/pages/Notifications.tsx:390 +#: src/pages/Notifications.tsx:407 msgid "Load more" msgstr "Charger plus" @@ -605,8 +612,8 @@ msgstr "Chargement du profil…" #: src/pages/index/HotFeed.tsx:32 #: src/pages/index/JournalFeed.tsx:44 #: src/pages/index/NewFeed.tsx:32 -#: src/pages/Notifications.tsx:313 -#: src/pages/Notifications.tsx:389 +#: src/pages/Notifications.tsx:330 +#: src/pages/Notifications.tsx:406 #: src/pages/UserDumps.tsx:51 #: src/pages/UserPlaylists.tsx:342 #: src/pages/UserPublicProfile.tsx:1100 @@ -639,7 +646,7 @@ msgstr "Connexion échouée" msgid "Max 50 MB" msgstr "Max 50 Mo" -#: src/pages/Notifications.tsx:306 +#: src/pages/Notifications.tsx:323 msgid "new" msgstr "nouveau" @@ -705,7 +712,7 @@ msgstr "Aucun utilisateur ne correspond à « {q} »." msgid "Not following anyone yet." msgstr "Aucun abonnement pour le moment." -#: src/pages/Notifications.tsx:324 +#: src/pages/Notifications.tsx:341 #: src/pages/UserDumps.tsx:123 #: src/pages/UserPublicProfile.tsx:1340 #: src/pages/UserPublicProfile.tsx:1463 @@ -714,7 +721,7 @@ msgid "Nothing here yet." msgstr "Rien ici pour l'instant." #: src/components/NotificationBell.tsx:42 -#: src/pages/Notifications.tsx:302 +#: src/pages/Notifications.tsx:319 msgid "Notifications" msgstr "Notifications" @@ -952,7 +959,7 @@ msgstr "Ce lien de réinitialisation est absent ou malformé." msgid "Title" msgstr "Titre" -#: src/pages/Notifications.tsx:346 +#: src/pages/Notifications.tsx:357 msgid "Today" msgstr "Aujourd'hui" @@ -1034,11 +1041,11 @@ msgstr "Pourquoi recommandez-vous ça ?" msgid "Write a reply…" msgstr "Écrire une réponse…" -#: src/pages/Notifications.tsx:348 +#: src/pages/Notifications.tsx:359 msgid "Yesterday" msgstr "Hier" -#: src/pages/Notifications.tsx:327 +#: src/pages/Notifications.tsx:344 msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu." diff --git a/src/model.ts b/src/model.ts index b8d2d44..440d798 100644 --- a/src/model.ts +++ b/src/model.ts @@ -248,7 +248,8 @@ export type NotificationType = | "user_dump_posted" | "playlist_dump_added" | "dump_upvoted" - | "user_mentioned"; + | "user_mentioned" + | "dump_commented"; export interface PlaylistFollowedData { followerId: string; @@ -292,13 +293,22 @@ export interface UserMentionedData { dumpId?: string; } +export interface DumpCommentedData { + commenterId: string; + commenterUsername: string; + commentId: string; + dumpId: string; + dumpTitle: string; +} + export type NotificationData = | PlaylistFollowedData | UserFollowedData | UserDumpPostedData | PlaylistDumpAddedData | DumpUpvotedData - | UserMentionedData; + | UserMentionedData + | DumpCommentedData; export interface Notification { id: string; diff --git a/src/pages/Notifications.tsx b/src/pages/Notifications.tsx index 1d5d801..c873a2c 100644 --- a/src/pages/Notifications.tsx +++ b/src/pages/Notifications.tsx @@ -10,6 +10,7 @@ import { ErrorCard } from "../components/ErrorCard.tsx"; import { Tooltip } from "../components/Tooltip.tsx"; import { useWS } from "../hooks/useWS.ts"; import type { + DumpCommentedData, DumpUpvotedData, Notification, NotificationData, @@ -36,7 +37,13 @@ type State = loadingMore: boolean; }; -type NotifIconKind = "upvote" | "follow" | "dump" | "playlist" | "mention"; +type NotifIconKind = + | "upvote" + | "follow" + | "dump" + | "playlist" + | "mention" + | "comment"; function notifIconKind(type: Notification["type"]): NotifIconKind { switch (type) { @@ -52,6 +59,8 @@ function notifIconKind(type: Notification["type"]): NotifIconKind { return "playlist"; case "user_mentioned": return "mention"; + case "dump_commented": + return "comment"; } } @@ -75,6 +84,7 @@ function NotifIcon({ type }: { type: Notification["type"] }) { dump: "🚚", playlist: "📜", mention: "@", + comment: "💬", }; return ( @@ -96,6 +106,10 @@ function notificationLink(n: Notification): string { return `/dumps/${(data as PlaylistDumpAddedData).dumpId}`; case "dump_upvoted": return `/dumps/${(data as DumpUpvotedData).dumpId}`; + case "dump_commented": + return `/dumps/${(data as DumpCommentedData).dumpId}#comment-${ + (data as DumpCommentedData).commentId + }`; case "user_mentioned": { const d = data as UserMentionedData; if (d.contextType === "comment") { @@ -159,6 +173,16 @@ function notificationContent(n: Notification): React.ReactNode { ); } + case "dump_commented": { + const d = data as DumpCommentedData; + return ( + + {d.commenterUsername} + {" commented on "} + {d.dumpTitle} + + ); + } case "user_mentioned": { const d = data as UserMentionedData; const where = d.contextTitle || diff --git a/vite.config.ts b/vite.config.ts index f8c36d4..6eb3096 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,8 @@ import { lingui } from "@lingui/vite-plugin"; import fs from "node:fs"; import path from "node:path"; +const SITE_NAME = process.env.GERBEUR_SITE_NAME || "gerbeur"; + function manifestPlugin(): Plugin { const cssPath = path.resolve("src/index.css"); const outPath = path.resolve("public/manifest.webmanifest"); @@ -21,8 +23,8 @@ function manifestPlugin(): Plugin { const bgColor = cssVar(rootBlock, "--color-bg") ?? "#111827"; const manifest = { - name: "gerbeur", - short_name: "gerbeur", + name: SITE_NAME, + short_name: SITE_NAME, start_url: "/", display: "standalone", background_color: bgColor, @@ -73,6 +75,18 @@ export default defineConfig({ }, plugins: [ manifestPlugin(), + { + // In dev the API server isn't serving index.html, so replace the placeholder here. + // In production the Oak server does the replacement at runtime (see api/lib/static.ts). + name: "inject-site-name", + transformIndexHtml: { + order: "pre", + handler: (html, ctx) => + ctx.server + ? html.replaceAll("__SITE_NAME__", SITE_NAME) + : html, + }, + }, lingui(), react({ plugins: [["@lingui/swc-plugin", {}]],