diff --git a/.github/workflows/validate-app.yml b/.github/workflows/validate-app.yml
new file mode 100644
index 00000000..88177116
--- /dev/null
+++ b/.github/workflows/validate-app.yml
@@ -0,0 +1,32 @@
+name: Validate App
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run app checks
+ run: npm run check
+
+ - name: Build app
+ run: npm run build
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..b3cbd7e6
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,22 @@
+# AGENTS.md
+
+These instructions apply to the whole repository.
+
+## Language
+
+- Use Australian/British English over US English.
+
+## Frontend
+
+- Prefer Server Components by default. Only add `"use client"` when the component needs browser-only APIs, state/effects, event handlers, or client-only libraries.
+- When touching JS/JSX components, convert them to TS/TSX when it is reasonable and scoped to the change.
+- Keep shared presentational components server-safe where possible. For translated labels or suffixes, prefer passing translated text from the caller instead of adding `useTranslations` to a shared component.
+- In MDX prose, if an inline component inside a Markdown list item is formatted onto multiple lines and changes rendered spacing, use a targeted `{/* prettier-ignore */}` before that list rather than disabling formatting for the whole file.
+
+## Internationalisation
+
+- Put user-facing app UI copy in `next-intl` message files under `messages/`.
+- Treat `messages/en.json` as the source catalogue. Keep Spanish (`es`) and German (`de`) complete whenever adding or changing message keys.
+- Run `npm run i18n:check` after editing messages. Run `npm run check` before handing work back.
+- Do not put dynamic user-generated content, internal enum values, table names, route constants, CSS strings, logs, or analytics identifiers into message files.
+- For listing types and other enums, pass stable keys such as `business`, `community`, or `residential` and handle display grammar in translations, usually with ICU `select`.
diff --git a/messages/de.json b/messages/de.json
index b8202eb2..31f71712 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -6,12 +6,110 @@
"about": "Über"
},
"Actions": {
+ "add": "Hinzufügen",
+ "addListing": "Eintrag hinzufügen",
+ "addMorePhotos": "Weitere Fotos hinzufügen",
+ "addPhotos": "Fotos hinzufügen",
+ "backToPeels": "Zurück zu Peels",
+ "cancel": "Abbrechen",
+ "close": "Schließen",
+ "continue": "Weiter",
+ "delete": "Löschen",
+ "deleteListing": "Eintrag löschen",
+ "done": "Fertig",
+ "edit": "Bearbeiten",
+ "emailLink": "Link per E-Mail senden",
+ "exportData": "Daten exportieren",
+ "home": "Startseite",
+ "joinPeels": "Peels beitreten",
+ "noCancel": "Nein, abbrechen",
+ "openInAppleMaps": "In Apple Karten öffnen",
+ "openInGoogleMaps": "In Google Maps öffnen",
+ "replace": "Ersetzen",
+ "resetPassword": "Passwort zurücksetzen",
+ "saveChanges": "Änderungen speichern",
+ "seeNearbyListings": "Einträge in der Nähe ansehen",
+ "sendLink": "Link senden",
+ "signIn": "Anmelden",
+ "signInToContact": "Zum Kontaktieren anmelden",
+ "signOut": "Abmelden",
"signUp": "Registrieren",
- "signIn": "Anmelden"
+ "update": "Aktualisieren",
+ "viewFullListing": "Vollständigen Eintrag ansehen",
+ "viewListing": "Eintrag ansehen"
+ },
+ "Common": {
+ "account": "Konto",
+ "actions": "Aktionen",
+ "admin": "Admin",
+ "email": "E-Mail",
+ "hidden": "Ausgeblendet",
+ "links": "Links",
+ "listings": "Einträge",
+ "loading": "Lädt...",
+ "newsletter": "Newsletter",
+ "notSubscribed": "Nicht abonniert",
+ "optional": "optional",
+ "password": "Passwort",
+ "photos": "Fotos",
+ "source": "Quelle",
+ "stub": "Stub",
+ "subscribed": "Abonniert"
+ },
+ "Status": {
+ "adding": "Wird hinzugefügt...",
+ "copying": "Wird kopiert...",
+ "deleting": "Wird gelöscht...",
+ "emailing": "E-Mail wird gesendet...",
+ "resetting": "Wird zurückgesetzt...",
+ "saving": "Wird gespeichert...",
+ "sending": "Wird gesendet...",
+ "signingIn": "Anmeldung läuft...",
+ "signingOut": "Abmeldung läuft...",
+ "signingUp": "Registrierung läuft...",
+ "updating": "Wird aktualisiert...",
+ "uploading": "Wird hochgeladen...",
+ "verifying": "Wird verifiziert...",
+ "working": "Wird verarbeitet..."
+ },
+ "Errors": {
+ "alreadyYourEmail": "Das ist bereits deine E-Mail-Adresse.",
+ "emptyListingName": "{type, select, business {Der Name des Unternehmens darf nicht leer sein.} community {Der Name der Community darf nicht leer sein.} other {Der Name des Eintrags darf nicht leer sein.}}",
+ "emptyName": "Der Name darf nicht leer sein.",
+ "accountDeleted": "Dein Konto wurde gelöscht. Schade, dass du gehst.",
+ "accountExists": "Ein Konto mit dieser E-Mail existiert bereits. Bitte melde dich stattdessen an.",
+ "deleteAccountFailed": "Fehler beim Löschen des Kontos",
+ "duplicateListing": "Ein identischer Eintrag existiert bereits.",
+ "emailRequired": "E-Mail ist erforderlich",
+ "failedDeleteListing": "Der Eintrag konnte nicht gelöscht werden.",
+ "failedDeletePhoto": "Das Foto konnte nicht gelöscht werden. Bitte versuche es erneut.",
+ "generic": "Hmm, da stimmt etwas nicht. Versuchst du es noch einmal?",
+ "genericLater": "Etwas ist schiefgelaufen. Bitte versuche es später erneut.",
+ "missingLocation": "Bitte wähle einen Standort aus.",
+ "missingSignUpFields": "Vorname, E-Mail und Passwort sind erforderlich.",
+ "passwordMismatch": "Diese Passwörter stimmen nicht überein.",
+ "photoUploadFailed": "Beim Hochladen deiner Fotos ist ein Fehler aufgetreten. Bitte versuche es erneut.",
+ "avatarUploadFailed": "Beim Hochladen deines Fotos ist ein Fehler aufgetreten. Bitte versuche es erneut.",
+ "requiredPasswordFields": "Beide Felder sind erforderlich.",
+ "resetPasswordDenied": "Hmm, da stimmt etwas nicht. Vielleicht hast du keine Berechtigung, dieses Passwort zurückzusetzen, oder du hast versucht, ein kürzlich verwendetes Passwort erneut zu nutzen.",
+ "resetPasswordSuccess": "Dein Passwort wurde aktualisiert. Zurück zum Kompostieren!",
+ "savePhotosFailed": "Der Eintrag wurde erstellt, aber die Fotos konnten nicht gespeichert werden.",
+ "signUpFailed": "Registrierung fehlgeschlagen",
+ "tooManyListings": "Du hast die maximale Anzahl erlaubter Einträge erreicht. Lösche einen deiner aktuellen drei Einträge, um einen neuen zu erstellen.",
+ "tooManyMessages": "Du hast zu viele Nachrichten gesendet. Bitte versuche es später erneut.",
+ "updateEmailFailed": "Hmm, da stimmt etwas nicht. Prüfe deine E-Mail oder versuche es erneut.",
+ "updateFirstNameFailed": "Entschuldigung, wir konnten deinen Vornamen nicht aktualisieren.",
+ "updateNewsletterFailed": "Entschuldigung, wir konnten deine Newsletter-Einstellung nicht aktualisieren.",
+ "unexpected": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.",
+ "validationSummary": "Bitte behebe {count, plural, one {den obigen Fehler} other {die obigen Fehler}} und versuche es erneut.",
+ "verificationChallenge": "Bitte schließe die Verifizierung ab.",
+ "verificationFailed": "Die Verifizierung ist fehlgeschlagen. Bitte versuche es erneut.",
+ "forgotPasswordSuccess": "Prüfe deinen Posteingang (und Spam) für einen Link zum Zurücksetzen des Passworts, sofern diese E-Mail-Adresse mit einem Peels-Konto verknüpft ist."
},
"Index": {
"title": "Finde ein Zuhause für deine Küchenabfälle, wo auch immer du bist",
"subtitle": "Peels verbindet die Menschen mit Lebensmittelkratzern mit denen, die komponieren. Es ist ein kostenloses und nicht-kommerzielles Gemeinschaftsprojekt.",
+ "hostAvatarAlt": "Der Avatar eines Peels-Gastgebers",
"buttons": {
"browseMap": "Karte durchsuchen",
"signUp": "Bei Peels anmelden",
@@ -64,11 +162,270 @@
}
},
"Profile": {
- "addListing": "Angebot hinzufügen"
+ "addListing": "Eintrag hinzufügen",
+ "addAnotherListing": "Weiteren Eintrag hinzufügen",
+ "listingCardAlt": "{type, select, residential {Dein Avatar für diesen Wohneintrag} community {Dein Avatar für diesen Community-Eintrag} business {Dein Avatar für diesen Unternehmenseintrag} other {Dein Avatar für diesen Eintrag}}",
+ "listingCardType": "{type, select, residential {Wohneintrag} community {Community-Eintrag} business {Unternehmenseintrag} other {Eintrag}}",
+ "listingPrompt": "Setze dich, deinen Gemeinschaftsort oder dein Unternehmen auf die Karte",
+ "sections": {
+ "listings": "Einträge",
+ "account": "Konto",
+ "actions": "Aktionen"
+ },
+ "account": {
+ "firstName": "Vorname",
+ "firstNameHint": "Verwende deinen Vornamen oder einen Spitznamen.",
+ "emailSuccess": "Prüfe deine E-Mails für den Bestätigungslink.",
+ "emailHint": "Wir senden einen Bestätigungslink an diese E-Mail.",
+ "newsletterSubscribedHint": "Wir senden dir gelegentliche E-Mail-Updates über Peels.",
+ "newsletterNotSubscribedHint": "Wir senden dir nur notwendige E-Mails zu deinem Konto oder deinen Einträgen."
+ },
+ "actions": {
+ "signOutTitle": "Abmelden",
+ "signOutDescription": "Bis bald!",
+ "exportTitle": "Daten exportieren",
+ "exportDescription": "Erhalte eine Kopie deiner Peels-Daten",
+ "exportDialogTitle": "Demnächst",
+ "exportDialog": "Wir arbeiten noch an dieser Funktion. In der Zwischenzeit kannst du uns kontaktieren und uns bitten, deine Daten manuell zu exportieren.",
+ "deleteTitle": "Konto löschen",
+ "deleteDescription": "Lösche dein Konto{count, plural, =0 {} one {, deinen Eintrag,} other {, deine Einträge,}} und alle deine Daten",
+ "deleteConfirm": "{count, plural, =0 {Ja, mein Konto löschen} one {Ja, mein Konto und meinen Eintrag löschen} other {Ja, mein Konto und meine Einträge löschen}}",
+ "deleteDialog": "Möchtest du dein Konto wirklich löschen?{count, plural, =0 {} one { Dein Eintrag wird ebenfalls gelöscht.} other { Deine Einträge werden ebenfalls gelöscht.}}"
+ }
+ },
+ "Auth": {
+ "complete": {
+ "title": "Du wirst angemeldet...",
+ "body": "Wir bestätigen deinen Link sicher. Du wirst gleich weitergeleitet."
+ },
+ "forgotPassword": {
+ "title": "Passwort vergessen",
+ "sentTitle": "E-Mail gesendet",
+ "body": "Das passiert uns allen. Gib unten deine E-Mail ein, um einen Link zum Zurücksetzen des Passworts zu erhalten."
+ },
+ "resetPassword": {
+ "title": "Passwort zurücksetzen",
+ "successTitle": "Passwort aktualisiert",
+ "body": "Gib unten dein neues Passwort ein.",
+ "newPassword": "Neues Passwort",
+ "confirmPassword": "Passwort bestätigen"
+ },
+ "signIn": {
+ "title": "Bei Peels anmelden",
+ "firstTime": "Zum ersten Mal hier? Registrieren",
+ "forgotPassword": "Passwort vergessen?"
+ },
+ "signUp": {
+ "firstName": "Vorname",
+ "newPassword": "Dein neues Passwort",
+ "newsletterOptIn": "Sendet mir gelegentliche E-Mail-Updates über Peels",
+ "errorWithSupport": "{error} Wenn du denkst, dass das nicht stimmen kann, schreib uns eine E-Mail."
+ },
+ "turnstile": {
+ "expired": "Die Verifizierung ist abgelaufen. Bitte schließe sie erneut ab.",
+ "timeout": "Die Sicherheitsprüfung wurde nicht abgeschlossen. Deaktiviere Werbeblocker und versuche es erneut. Wenn es weiterhin fehlschlägt, probiere einen anderen Browser oder ein anderes Netzwerk.",
+ "unsupported": "Dieser Browser kann die Sicherheitsprüfung nicht abschließen. Bitte probiere einen anderen Browser oder ein anderes Netzwerk.",
+ "failed": "Die Sicherheitsverifizierung ist mit Fehler #{code} fehlgeschlagen. Bitte versuche es erneut oder nutze einen anderen Browser."
+ }
+ },
+ "Listings": {
+ "new": {
+ "listingTypeTitle": "Welche Art von Eintrag?",
+ "hostTypeTitle": "Wo nimmst du Essensreste an?",
+ "listingTypeLabel": "Eintragstyp",
+ "hostTypeLabel": "Gastgebertyp",
+ "options": {
+ "host": {
+ "title": "Ich nehme Essensreste an",
+ "description": "Andere können eine Abgabe von Essensresten bei dir zu Hause oder in deinem Gemeinschaftsgarten vereinbaren"
+ },
+ "business": {
+ "title": "Mein Unternehmen spendet Reste",
+ "description": "Andere können Kaffeesatz aus deinem Café, Hopfen aus deiner Brauerei oder Ähnliches abholen"
+ },
+ "residential": {
+ "title": "Bei mir zu Hause",
+ "description": "Ich nehme Reste an einer Wohnadresse an"
+ },
+ "community": {
+ "title": "An einem Gemeinschaftsort",
+ "description": "Ich betreue einen Gemeinschaftsgarten oder etwas Ähnliches"
+ }
+ }
+ },
+ "edit": {
+ "title": "Eintrag bearbeiten",
+ "notFound": "Eintrag nicht gefunden"
+ },
+ "form": {
+ "basics": "Grundlagen",
+ "placeName": "Name des Ortes",
+ "placeNamePlaceholder": "{type, select, business {Name deines Unternehmens} community {Name deiner Community} other {Name des Ortes}}",
+ "yourFirstName": "Dein Vorname",
+ "location": "Standort",
+ "selectCountry": "Land auswählen",
+ "locationPlaceholder": "Deine Straße oder ein Ort in der Nähe",
+ "customLocation": "Benutzerdefinierter Standort",
+ "locationHint": "{type, select, residential {Beginne zu tippen und wähle dann eine der vorgeschlagenen Optionen aus dem Menü.} other {Beginne zu tippen und wähle dann eine der vorgeschlagenen Adressen aus dem Menü.}}",
+ "dragPinHint": "Ziehe die Markierung, um deinen Standort zu verfeinern{obscure, select, true { oder zu verschleiern} other {}}.",
+ "businessAddress": "Adresse deines Unternehmens",
+ "communityAddress": "Adresse deiner Gemeinschaft",
+ "residentialAddress": "Deine Straße oder Nachbarschaft",
+ "donationDetails": "Details zur Spende",
+ "donationDetailsPlaceholder": "Details zu deiner Spende",
+ "donationDetailsHint": "Welche Reste du abgeben möchtest und wie die Abholung funktioniert. Links gehören in den eigenen Abschnitt weiter unten.",
+ "descriptionLabel": "Kurze Beschreibung oder Hinweise",
+ "descriptionPlaceholder": "{type, select, residential {Über deinen Eintrag} community {Über deine Community} business {Über dein Unternehmen} other {Über deinen Eintrag}}",
+ "communityDescriptionHint": "Öffnungszeiten und Kompostiermöglichkeiten. Hebe die akzeptierten Reste und Links für die eigenen Abschnitte weiter unten auf.",
+ "residentialDescriptionHint": "Dein Kompost-Setup und deine allgemeine Verfügbarkeit. Hebe die akzeptierten Reste für den eigenen Abschnitt weiter unten auf.",
+ "compostingDetails": "Kompostierungsdetails",
+ "compostingDetailsHint": "Sei konkret, damit andere genau wissen, was vermieden werden sollte. Gib Elemente einzeln ein, damit sie leichter zu lesen sind.",
+ "acceptedLabel": "Welche Reste nimmst du an?",
+ "rejectedLabel": "Welche Reste nimmst du nicht an?",
+ "addItem": "Element hinzufügen",
+ "addAnotherItem": "Weiteres Element hinzufügen",
+ "acceptedPlaceholder": "Ein Element, das du annimmst (z. B. ‘Obstschalen’)",
+ "acceptedSecondaryPlaceholder": "Ein weiteres Element, das du annimmst",
+ "rejectedPlaceholder": "Ein Element, das du nicht annimmst (z. B. ‘Fleisch’)",
+ "rejectedSecondaryPlaceholder": "Ein weiteres Element, das du nicht annimmst",
+ "media": "Medien",
+ "mediaHint": "Zeige Peels-Mitgliedern optional {subject}.",
+ "mediaResidential": "etwas mehr über deinen Eintrag",
+ "mediaCommunity": "etwas mehr über dein Gemeinschaftsprojekt",
+ "mediaBusiness": "dein Unternehmen",
+ "externalLinks": "Externe Links",
+ "addLink": "Link hinzufügen",
+ "linkPlaceholder": "Deine Website oder Social Media",
+ "visibility": "Sichtbarkeit",
+ "visibilityHint": "Brauchst du eine Pause von Peels? Blende diesen Eintrag vorübergehend von der Karte aus.",
+ "mapVisibility": "Sichtbarkeit auf der Karte",
+ "showOnMap": "Diesen Eintrag auf der Karte anzeigen",
+ "hideFromMap": "Diesen Eintrag von der Karte ausblenden",
+ "adminHint": "Nur für Admins sichtbare Steuerelemente für diesen Eintrag.",
+ "stubSettings": "Stub-Einstellungen",
+ "regularListing": "Dies ist ein regulärer Eintrag, der dir gehört",
+ "stubListing": "Dieser Eintrag ist ein Stub, den andere beanspruchen können",
+ "stubActiveHint": "Dieser Eintrag enthält keine deiner Kontaktinformationen. Andere können ihn als eigenen Eintrag beanspruchen und übernehmen.",
+ "stubInactiveHint": "Dieser Eintrag wird wie jeder andere angezeigt."
+ },
+ "read": {
+ "contact": "{name} kontaktieren",
+ "about": "Über",
+ "donationDetails": "Details zur Spende",
+ "accepted": "Was angenommen wird",
+ "rejected": "Was nicht angenommen wird",
+ "location": "Standort",
+ "residentialLocation": "{name} wohnt in {area}. Frage nach dem genauen Standort, wenn ihr die Abgabe von Essensresten vereinbart.",
+ "nonResidentialLocation": "{name} ist {type} in {area}.",
+ "thisArea": "dieser Gegend",
+ "businessType": "ein Unternehmen",
+ "communityType": "ein Gemeinschaftsort",
+ "signInForPhotos": "Melde dich an, um die Fotos dieses Gastgebers zu sehen.",
+ "ownerNote": "Dies ist dein eigener Eintrag{stub, select, true {, als Stub markiert} other {}}. {visibility, select, true {Sieht gut aus!} other {Du hast ihn von der Karte ausgeblendet, daher kannst im Moment nur du ihn sehen.}}",
+ "stubNote": "Dies ist ein Stub, der vom Peels-Team erstellt wurde. Prüfe die Angaben im Eintrag, bevor du vorbeischaust.",
+ "stubClaim": "Bist du die Eigentümerin oder der Eigentümer? Kontaktiere uns, um diesen Eintrag zu beanspruchen oder Änderungen anzufordern.",
+ "firstTime": "Zum ersten Mal hier? Registrieren",
+ "residentOf": "Bewohner von {area}",
+ "localResident": "Lokale Person",
+ "communityIn": "Gemeinschaft in {area}",
+ "localCommunity": "Lokale Gemeinschaft",
+ "businessIn": "Unternehmen in {area}",
+ "localBusiness": "Lokales Unternehmen",
+ "avatarAlt": "Der Avatar für diesen Eintrag"
+ },
+ "delete": {
+ "confirm": "Ja, Eintrag löschen",
+ "dialog": "Möchtest du deinen Eintrag wirklich löschen? Das kann nicht rückgängig gemacht werden.",
+ "success": "Dein Eintrag wurde gelöscht."
+ },
+ "photos": {
+ "alt": "Foto {number}",
+ "dropHere": "Fotos hier ablegen",
+ "tooMany": "Du kannst höchstens {max} Fotos hochladen",
+ "tooLargeOne": "Dein Foto ist zu groß. Die maximale Dateigröße beträgt {max} MB.",
+ "tooLargeMany": "Ein oder mehrere Fotos sind zu groß. Die maximale Dateigröße beträgt {max} MB pro Foto."
+ }
+ },
+ "Upload": {
+ "avatarAlt": "Dein Avatar",
+ "avatarHint": "Lade ein Foto hoch, damit Mitglieder wissen, wem sie schreiben."
+ },
+ "Legal": {
+ "agreement": "Ich habe die Peels-Nutzungsbedingungen und die Datenschutzerklärung gelesen und stimme ihnen zu",
+ "privacy": "Datenschutz",
+ "terms": "Bedingungen"
+ },
+ "Chat": {
+ "send": "Senden",
+ "placeholder": "Nachricht{name, select, empty {} other { an {name}}} senden...",
+ "empty": "Noch keine Nachrichten",
+ "threadsTitle": "Chats",
+ "noChats": "Noch keine Chats",
+ "youReachedOut": "Du hast {name} kontaktiert",
+ "personReachedOut": "{name} hat dich kontaktiert",
+ "personReachedOutAbout": "{name} hat dich wegen {listing} kontaktiert",
+ "drawerTitle": "Chat-Schublade",
+ "drawerDescription": "Unterhaltung zu diesem Eintrag.",
+ "report": "Melden oder blockieren",
+ "reportTitle": "Wir kümmern uns darum",
+ "reportBody": "Es tut uns leid, dass du Probleme mit {name} hast. Bitte kontaktiere uns, um das Problem zu melden oder diese Person daran zu hindern, dich weiter zu kontaktieren."
+ },
+ "Map": {
+ "drawerTitle": "Eintragsdetails",
+ "drawerDescription": "Details zum ausgewählten Eintrag.",
+ "emptyTitle": "Nichts gefunden",
+ "emptyBody": "Der Eintrag, den du suchst, existiert nicht oder wurde entfernt. Tut uns leid.",
+ "searchPlaceholder": "Suchen",
+ "searchError": "Etwas ist schiefgelaufen. Erneut versuchen?",
+ "searchNoResults": "Keine Ergebnisse. Tippe weiter oder verfeinere deine Suche",
+ "didYouKnow": "Wusstest du schon?",
+ "steps": {
+ "find": {
+ "title": "Gastgeber finden",
+ "description": "Wähle eine Markierung auf der Karte aus."
+ },
+ "contact": {
+ "title": "Kontakt aufnehmen",
+ "description": "Vereinbare eine Abgabe per Chat."
+ },
+ "dropOff": {
+ "title": "Abgeben",
+ "description": "Lerne deine Nachbarn kennen!"
+ }
+ }
+ },
+ "Newsletter": {
+ "title": "Newsletter",
+ "inboxTitle": "Direkt in dein Postfach",
+ "inboxDescription": "Aktiviere den Empfang künftiger Newsletter-Ausgaben per E-Mail.",
+ "rss": "Oder abonniere den RSS-Feed.",
+ "latestIssue": "Neueste Ausgabe",
+ "pastIssues": "Frühere Ausgaben",
+ "issueSubtitle": "Ausgabe #{number} · Veröffentlicht am {date}",
+ "parent": "Newsletter",
+ "stampAlt": "Eine Briefmarke",
+ "aside": {
+ "title": "Über diesen Newsletter",
+ "bodyGuest": "Dies ist die Webversion des E-Mail-Newsletters für Abonnenten. Tritt Peels bei, um künftige Ausgaben zu erhalten.",
+ "bodySubscribed": "Dies ist die Webversion des E-Mail-Newsletters für Abonnenten. Teile ihn gerne weiter.",
+ "bodyMember": "Dies ist die Webversion des E-Mail-Newsletters für Abonnenten. Bearbeite deine Einstellungen, um künftige Ausgaben zu erhalten."
+ },
+ "callout": {
+ "guestTitle": "Tritt Peels bei, um den Newsletter zu erhalten",
+ "guestBody": "Du musst Mitglied bei Peels sein, um den Newsletter per E-Mail zu erhalten. Die Registrierung ist kostenlos und dauert nur ein paar Sekunden.",
+ "alreadySubscribedTitle": "Du bist bereits abonniert",
+ "alreadySubscribedBody": "Die nächste Ausgabe sollte in deinem E-Mail-Postfach erscheinen. Teile diese Seite in der Zwischenzeit gerne mit einer Freundin oder einem Freund!",
+ "notSubscribedTitle": "Du bist nicht abonniert",
+ "notSubscribedBody": "Ändere deine Newsletter-Einstellung auf deiner Profilseite.",
+ "editPreference": "Newsletter-Einstellung bearbeiten"
+ }
+ },
+ "NotFound": {
+ "body": "Entschuldigung, wir konnten die gesuchte Seite nicht finden."
},
"Support": {
"title": "Support",
- "subtitle": "We periodically update this page with answers to common questions. Feel free to email us for help with anything else.",
+ "subtitle": "Wir aktualisieren diese Seite regelmäßig mit Antworten auf häufige Fragen. Für alles Weitere kannst du uns gerne kontaktieren.",
"peelsFaq": {
"title": "About Peels",
"whosBehind": {
diff --git a/messages/en.json b/messages/en.json
index f1bfb350..a3e8cfbc 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -6,12 +6,110 @@
"about": "About"
},
"Actions": {
+ "add": "Add",
+ "addListing": "Add listing",
+ "addMorePhotos": "Add more photos",
+ "addPhotos": "Add photos",
+ "backToPeels": "Back to Peels",
+ "cancel": "Cancel",
+ "close": "Close",
+ "continue": "Continue",
+ "delete": "Delete",
+ "deleteListing": "Delete listing",
+ "done": "Done",
+ "edit": "Edit",
+ "emailLink": "Email me the link",
+ "exportData": "Export data",
+ "home": "Home",
+ "joinPeels": "Join Peels",
+ "noCancel": "No, cancel",
+ "openInAppleMaps": "Open in Apple Maps",
+ "openInGoogleMaps": "Open in Google Maps",
+ "replace": "Replace",
+ "resetPassword": "Reset password",
+ "saveChanges": "Save changes",
+ "seeNearbyListings": "See nearby listings",
+ "sendLink": "Send the link",
+ "signIn": "Sign in",
+ "signInToContact": "Sign in to contact",
+ "signOut": "Sign out",
"signUp": "Sign up",
- "signIn": "Sign in"
+ "update": "Update",
+ "viewFullListing": "View full listing",
+ "viewListing": "View listing"
+ },
+ "Common": {
+ "account": "Account",
+ "actions": "Actions",
+ "admin": "Admin",
+ "email": "Email",
+ "hidden": "Hidden",
+ "links": "Links",
+ "listings": "Listings",
+ "loading": "Loading...",
+ "newsletter": "Newsletter",
+ "notSubscribed": "Not subscribed",
+ "optional": "optional",
+ "password": "Password",
+ "photos": "Photos",
+ "source": "Source",
+ "stub": "Stub",
+ "subscribed": "Subscribed"
+ },
+ "Status": {
+ "adding": "Adding...",
+ "copying": "Copying...",
+ "deleting": "Deleting...",
+ "emailing": "Emailing...",
+ "resetting": "Resetting...",
+ "saving": "Saving...",
+ "sending": "Sending...",
+ "signingIn": "Signing in...",
+ "signingOut": "Signing out...",
+ "signingUp": "Signing up...",
+ "updating": "Updating...",
+ "uploading": "Uploading...",
+ "verifying": "Verifying...",
+ "working": "Working..."
+ },
+ "Errors": {
+ "alreadyYourEmail": "This is already your email address.",
+ "emptyListingName": "{type, select, business {You can't have an empty business name.} community {You can't have an empty community name.} other {You can't have an empty listing name.}}",
+ "emptyName": "You can’t have an empty name.",
+ "accountDeleted": "Your account has been deleted. Sorry to see you go.",
+ "accountExists": "An account with this email already exists. Please sign in instead.",
+ "deleteAccountFailed": "Error whilst deleting account",
+ "duplicateListing": "An identical listing already exists.",
+ "emailRequired": "Email is required",
+ "failedDeleteListing": "Failed to delete listing.",
+ "failedDeletePhoto": "Failed to delete photo. Please try again.",
+ "generic": "Hmm, something’s not right. Mind trying again?",
+ "genericLater": "Something went wrong. Please try again later.",
+ "missingLocation": "Please select a location.",
+ "missingSignUpFields": "A first name, email, and password are required.",
+ "passwordMismatch": "Those passwords don’t match.",
+ "photoUploadFailed": "There was an error uploading your photos. Please try again.",
+ "avatarUploadFailed": "There was an error uploading your photo. Please try again.",
+ "requiredPasswordFields": "Both those fields are required.",
+ "resetPasswordDenied": "Hmm, something’s not right. You might not have permission to reset this password, or you tried reusing a recent password.",
+ "resetPasswordSuccess": "Your password has been updated. Let’s get back to composting!",
+ "savePhotosFailed": "Created listing but couldn’t save photos.",
+ "signUpFailed": "Sign up failed",
+ "tooManyListings": "You’ve reached the maximum number of listings allowed. Delete one of your current three to create a new one.",
+ "tooManyMessages": "You’ve sent too many messages. Please try again later.",
+ "updateEmailFailed": "Hmm, something’s not right. Check your email or try again.",
+ "updateFirstNameFailed": "Sorry, we couldn’t update your first name.",
+ "updateNewsletterFailed": "Sorry, we couldn’t update your newsletter preference.",
+ "unexpected": "An unexpected error occurred. Please try again later.",
+ "validationSummary": "Please fix the above error{count, plural, one {} other {s}} and then try again.",
+ "verificationChallenge": "Please complete the verification challenge.",
+ "verificationFailed": "Verification failed. Please try again.",
+ "forgotPasswordSuccess": "Check your inbox (and spam) for a password reset link, assuming that email address is linked to a Peels account."
},
"Index": {
"title": "Find a home for your food scraps, wherever you are",
"subtitle": "Peels connects folks with food scraps to those who compost. It’s a free, non-commercial, community project.",
+ "hostAvatarAlt": "The avatar for a Peels host",
"buttons": {
"browseMap": "Browse the map",
"signUp": "Sign up to Peels",
@@ -64,7 +162,266 @@
}
},
"Profile": {
- "addListing": "Add a listing"
+ "addListing": "Add a listing",
+ "addAnotherListing": "Add another listing",
+ "listingCardAlt": "{type, select, residential {Your avatar for this residential listing} community {Your avatar for this community listing} business {Your avatar for this business listing} other {Your avatar for this listing}}",
+ "listingCardType": "{type, select, residential {Residential listing} community {Community listing} business {Business listing} other {Listing}}",
+ "listingPrompt": "Put yourself, your community spot, or your business on the map",
+ "sections": {
+ "listings": "Listings",
+ "account": "Account",
+ "actions": "Actions"
+ },
+ "account": {
+ "firstName": "First name",
+ "firstNameHint": "Use your first name or a nickname.",
+ "emailSuccess": "Check your email for the verification link.",
+ "emailHint": "We’ll send a verification link to this email.",
+ "newsletterSubscribedHint": "We’ll send you occasional email updates about Peels.",
+ "newsletterNotSubscribedHint": "We’ll only send you necessary account or listing-related emails."
+ },
+ "actions": {
+ "signOutTitle": "Sign out",
+ "signOutDescription": "Goodbye for now!",
+ "exportTitle": "Export data",
+ "exportDescription": "Get a copy of your Peels data",
+ "exportDialogTitle": "Coming soon",
+ "exportDialog": "We’re still working on this feature. In the meantime, reach out and ask us to export your data manually.",
+ "deleteTitle": "Delete account",
+ "deleteDescription": "Delete your account{count, plural, =0 {} one {, listing,} other {, listings,}} and all your data",
+ "deleteConfirm": "{count, plural, =0 {Yes, delete my account} one {Yes, delete my account and listing} other {Yes, delete my account and listings}}",
+ "deleteDialog": "Are you sure you want to delete your account?{count, plural, =0 {} one { Your listing will also be deleted.} other { Your listings will also be deleted.}}"
+ }
+ },
+ "Auth": {
+ "complete": {
+ "title": "Signing you in...",
+ "body": "We’re securely confirming your link. You’ll be redirected in a moment."
+ },
+ "forgotPassword": {
+ "title": "Forgot password",
+ "sentTitle": "Email sent",
+ "body": "It happens to all of us. Enter your email below to receive a password reset link."
+ },
+ "resetPassword": {
+ "title": "Reset password",
+ "successTitle": "Password updated",
+ "body": "Please enter your new password below.",
+ "newPassword": "New password",
+ "confirmPassword": "Confirm password"
+ },
+ "signIn": {
+ "title": "Sign in to Peels",
+ "firstTime": "First time here? Sign up",
+ "forgotPassword": "Forgot password?"
+ },
+ "signUp": {
+ "firstName": "First name",
+ "newPassword": "Your new password",
+ "newsletterOptIn": "Send me occasional email updates about Peels",
+ "errorWithSupport": "{error} If you think this might be wrong, please email us."
+ },
+ "turnstile": {
+ "expired": "Verification expired. Please complete it again.",
+ "timeout": "Security check didn’t complete. Try disabling ad blockers and then try again. If it still fails, try a different browser or network.",
+ "unsupported": "This browser can’t complete the security check. Please try a different browser or network.",
+ "failed": "Security verification failed with error #{code}. Please try again or use a different browser."
+ }
+ },
+ "Listings": {
+ "new": {
+ "listingTypeTitle": "What kind of listing?",
+ "hostTypeTitle": "Where will you accept food scraps?",
+ "listingTypeLabel": "Listing type",
+ "hostTypeLabel": "Host type",
+ "options": {
+ "host": {
+ "title": "I accept food scraps",
+ "description": "Others can arrange food scraps drop-off to your home or your community garden"
+ },
+ "business": {
+ "title": "My business donates scraps",
+ "description": "Others can pick up spent coffee from your cafe, hops from your brewery, or similar"
+ },
+ "residential": {
+ "title": "At my home",
+ "description": "I accept scraps at a residential address"
+ },
+ "community": {
+ "title": "At a community place",
+ "description": "I manage a community garden or similar"
+ }
+ }
+ },
+ "edit": {
+ "title": "Edit listing",
+ "notFound": "Listing not found"
+ },
+ "form": {
+ "basics": "Basics",
+ "placeName": "Place name",
+ "placeNamePlaceholder": "{type, select, business {Your business’ name} community {Your community’s name} other {Your place name}}",
+ "yourFirstName": "Your first name",
+ "location": "Location",
+ "selectCountry": "Select a country",
+ "locationPlaceholder": "Your street name or nearby",
+ "customLocation": "Custom location",
+ "locationHint": "{type, select, residential {Start typing, then select one of the suggested options from the dropdown.} other {Start typing, then select one of the suggested addresses from the dropdown.}}",
+ "dragPinHint": "Drag the pin to refine{obscure, select, true { or obscure} other {}} your location.",
+ "businessAddress": "Your business’ address",
+ "communityAddress": "Your community’s address",
+ "residentialAddress": "Your street or neighbourhood",
+ "donationDetails": "Donation details",
+ "donationDetailsPlaceholder": "Your donation details",
+ "donationDetailsHint": "What kind of scraps you have to give away and the collection details. Save any links for the dedicated section, below.",
+ "descriptionLabel": "Short description or instructions",
+ "descriptionPlaceholder": "{type, select, residential {About your listing} community {About your community} business {About your business} other {About your listing}}",
+ "communityDescriptionHint": "Opening hours and composting facilities. Save the scraps you accept and any links for the dedicated sections, below.",
+ "residentialDescriptionHint": "Your composting set up and general availability. Save the scraps you accept for the dedicated section, below.",
+ "compostingDetails": "Composting details",
+ "compostingDetailsHint": "Be specific so people know exactly what should be avoided. Enter items separately so it’s easier to read.",
+ "acceptedLabel": "What scraps do you accept?",
+ "rejectedLabel": "What scraps do you not accept?",
+ "addItem": "Add an item",
+ "addAnotherItem": "Add another item",
+ "acceptedPlaceholder": "An item you accept (e.g. ‘fruit rinds’)",
+ "acceptedSecondaryPlaceholder": "Another item you accept",
+ "rejectedPlaceholder": "An item you don’t accept (e.g. ‘meat’)",
+ "rejectedSecondaryPlaceholder": "Another item you don’t accept",
+ "media": "Media",
+ "mediaHint": "Optionally show {subject} to Peels members.",
+ "mediaResidential": "a bit more about your listing",
+ "mediaCommunity": "a bit more about your community project",
+ "mediaBusiness": "off your business",
+ "externalLinks": "External links",
+ "addLink": "Add link",
+ "linkPlaceholder": "Your website or social media",
+ "visibility": "Visibility",
+ "visibilityHint": "Need a break from Peels? Temporarily hide this listing from the map.",
+ "mapVisibility": "Map visibility",
+ "showOnMap": "Show this listing on the map",
+ "hideFromMap": "Hide this listing from the map",
+ "adminHint": "Admin-only controls for this listing.",
+ "stubSettings": "Stub settings",
+ "regularListing": "This is a regular listing owned by you",
+ "stubListing": "This listing is a stub that others can claim",
+ "stubActiveHint": "This listing will not contain your contact information. Others can claim it as their own and take it over.",
+ "stubInactiveHint": "This listing will be presented just like any other."
+ },
+ "read": {
+ "contact": "Contact {name}",
+ "about": "About",
+ "donationDetails": "Donation details",
+ "accepted": "What’s accepted",
+ "rejected": "What’s not",
+ "location": "Location",
+ "residentialLocation": "{name} is a resident of {area}. Ask them for their exact location when you arrange a food scrap drop-off.",
+ "nonResidentialLocation": "{name} is {type} located in {area}.",
+ "thisArea": "this area",
+ "businessType": "a business",
+ "communityType": "a community spot",
+ "signInForPhotos": "Sign in to see this host’s photos.",
+ "ownerNote": "This is your own listing{stub, select, true {, marked as a stub} other {}}. {visibility, select, true {Lookin’ good!} other {You’ve hidden it from the map, so only you can see this right now.}}",
+ "stubNote": "This is a stub created by the Peels team. Double-check the listing information before visiting.",
+ "stubClaim": "Are you the owner? Reach out to claim this listing or to request changes.",
+ "firstTime": "First time here? Sign up",
+ "residentOf": "Resident of {area}",
+ "localResident": "Local resident",
+ "communityIn": "Community in {area}",
+ "localCommunity": "Local community",
+ "businessIn": "Business in {area}",
+ "localBusiness": "Local business",
+ "avatarAlt": "The avatar for this listing"
+ },
+ "delete": {
+ "confirm": "Yes, delete listing",
+ "dialog": "Are you sure you want to delete your listing? This is irreversible.",
+ "success": "Your listing has been deleted."
+ },
+ "photos": {
+ "alt": "Photo {number}",
+ "dropHere": "Drop photos here",
+ "tooMany": "You can only upload up to {max} photos",
+ "tooLargeOne": "Your photo is too large. The maximum file size is {max}MB.",
+ "tooLargeMany": "One or more of your photos are too large. The maximum file size is {max}MB per photo."
+ }
+ },
+ "Upload": {
+ "avatarAlt": "Your avatar",
+ "avatarHint": "Consider uploading a photo so members know who they’re messaging."
+ },
+ "Legal": {
+ "agreement": "I have read and agree to the Peels terms of use and privacy policy",
+ "privacy": "Privacy",
+ "terms": "Terms"
+ },
+ "Chat": {
+ "send": "Send",
+ "placeholder": "Send a message{name, select, empty {} other { to {name}}}...",
+ "empty": "No messages yet",
+ "threadsTitle": "Chats",
+ "noChats": "No chats yet",
+ "youReachedOut": "You reached out to {name}",
+ "personReachedOut": "{name} reached out to you",
+ "personReachedOutAbout": "{name} reached out to you about {listing}",
+ "drawerTitle": "Chat drawer",
+ "drawerDescription": "Conversation for this listing.",
+ "report": "Report or block",
+ "reportTitle": "Let’s get this sorted",
+ "reportBody": "Sorry to hear you’re having trouble with {name}. Please contact us to report the issue or to block them from contacting you any more."
+ },
+ "Map": {
+ "drawerTitle": "Listing details",
+ "drawerDescription": "Selected listing details.",
+ "emptyTitle": "Coming up empty",
+ "emptyBody": "The listing you’re looking for doesn’t exist or has been removed. Sorry to disappoint.",
+ "searchPlaceholder": "Search",
+ "searchError": "Something went wrong. Try again?",
+ "searchNoResults": "No results. Keep typing or refine your search",
+ "didYouKnow": "Did you know?",
+ "steps": {
+ "find": {
+ "title": "Find a host",
+ "description": "Select a marker on the map."
+ },
+ "contact": {
+ "title": "Contact",
+ "description": "Arrange a drop-off via chat."
+ },
+ "dropOff": {
+ "title": "Drop-off",
+ "description": "Meet your neighbours!"
+ }
+ }
+ },
+ "Newsletter": {
+ "title": "Newsletter",
+ "inboxTitle": "Get these in your inbox",
+ "inboxDescription": "Opt-in to receive future issues of the newsletter via email.",
+ "rss": "Or subscribe to the RSS feed.",
+ "latestIssue": "Latest issue",
+ "pastIssues": "Past issues",
+ "issueSubtitle": "Issue #{number} · Published {date}",
+ "parent": "Newsletter",
+ "stampAlt": "A postage stamp",
+ "aside": {
+ "title": "About this newsletter",
+ "bodyGuest": "This is the web version of the email newsletter sent out to subscribers. Join Peels to receive future issues.",
+ "bodySubscribed": "This is the web version of the email newsletter sent out to subscribers. Feel free to share it far and wide.",
+ "bodyMember": "This is the web version of the email newsletter sent out to subscribers. Edit your preferences to receive future issues."
+ },
+ "callout": {
+ "guestTitle": "Join Peels to get the newsletter",
+ "guestBody": "You need to be a member of Peels to get the newsletter via email. Signing up is free and only takes a few seconds.",
+ "alreadySubscribedTitle": "You’re already subscribed",
+ "alreadySubscribedBody": "You should see the next issue appear in your email inbox. Feel free to share this page with a friend in the meantime!",
+ "notSubscribedTitle": "You’re not subscribed",
+ "notSubscribedBody": "Change your newsletter preference on your Profile page.",
+ "editPreference": "Edit newsletter preference"
+ }
+ },
+ "NotFound": {
+ "body": "Sorry, we couldn’t find the page you were looking for."
},
"Support": {
"title": "Support",
diff --git a/messages/es.json b/messages/es.json
index 1b27ab1e..0efbfa31 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -6,12 +6,110 @@
"about": "Acerca"
},
"Actions": {
+ "add": "Agregar",
+ "addListing": "Agregar anuncio",
+ "addMorePhotos": "Agregar más fotos",
+ "addPhotos": "Agregar fotos",
+ "backToPeels": "Volver a Peels",
+ "cancel": "Cancelar",
+ "close": "Cerrar",
+ "continue": "Continuar",
+ "delete": "Eliminar",
+ "deleteListing": "Eliminar anuncio",
+ "done": "Listo",
+ "edit": "Editar",
+ "emailLink": "Enviarme el enlace",
+ "exportData": "Exportar datos",
+ "home": "Inicio",
+ "joinPeels": "Unirme a Peels",
+ "noCancel": "No, cancelar",
+ "openInAppleMaps": "Abrir en Apple Maps",
+ "openInGoogleMaps": "Abrir en Google Maps",
+ "replace": "Reemplazar",
+ "resetPassword": "Restablecer contraseña",
+ "saveChanges": "Guardar cambios",
+ "seeNearbyListings": "Ver anuncios cercanos",
+ "sendLink": "Enviar el enlace",
+ "signIn": "Iniciar sesión",
+ "signInToContact": "Inicia sesión para contactar",
+ "signOut": "Cerrar sesión",
"signUp": "Registrarse",
- "signIn": "Iniciar sesión"
+ "update": "Actualizar",
+ "viewFullListing": "Ver anuncio completo",
+ "viewListing": "Ver anuncio"
+ },
+ "Common": {
+ "account": "Cuenta",
+ "actions": "Acciones",
+ "admin": "Administración",
+ "email": "Correo electrónico",
+ "hidden": "Oculto",
+ "links": "Enlaces",
+ "listings": "Anuncios",
+ "loading": "Cargando...",
+ "newsletter": "Boletín",
+ "notSubscribed": "No suscrito",
+ "optional": "opcional",
+ "password": "Contraseña",
+ "photos": "Fotos",
+ "source": "Fuente",
+ "stub": "Borrador",
+ "subscribed": "Suscrito"
+ },
+ "Status": {
+ "adding": "Agregando...",
+ "copying": "Copiando...",
+ "deleting": "Eliminando...",
+ "emailing": "Enviando correo...",
+ "resetting": "Restableciendo...",
+ "saving": "Guardando...",
+ "sending": "Enviando...",
+ "signingIn": "Iniciando sesión...",
+ "signingOut": "Cerrando sesión...",
+ "signingUp": "Registrando...",
+ "updating": "Actualizando...",
+ "uploading": "Subiendo...",
+ "verifying": "Verificando...",
+ "working": "Procesando..."
+ },
+ "Errors": {
+ "alreadyYourEmail": "Esta ya es tu dirección de correo electrónico.",
+ "emptyListingName": "{type, select, business {No puedes dejar vacío el nombre del negocio.} community {No puedes dejar vacío el nombre de la comunidad.} other {No puedes dejar vacío el nombre del anuncio.}}",
+ "emptyName": "No puedes dejar el nombre vacío.",
+ "accountDeleted": "Tu cuenta ha sido eliminada. Sentimos verte partir.",
+ "accountExists": "Ya existe una cuenta con este correo. Inicia sesión en su lugar.",
+ "deleteAccountFailed": "Error al eliminar la cuenta",
+ "duplicateListing": "Ya existe un anuncio idéntico.",
+ "emailRequired": "El correo electrónico es obligatorio",
+ "failedDeleteListing": "No se pudo eliminar el anuncio.",
+ "failedDeletePhoto": "No se pudo eliminar la foto. Inténtalo de nuevo.",
+ "generic": "Hmm, algo no está bien. ¿Puedes intentarlo de nuevo?",
+ "genericLater": "Algo salió mal. Inténtalo de nuevo más tarde.",
+ "missingLocation": "Selecciona una ubicación.",
+ "missingSignUpFields": "El nombre, el correo electrónico y la contraseña son obligatorios.",
+ "passwordMismatch": "Las contraseñas no coinciden.",
+ "photoUploadFailed": "Hubo un error al subir tus fotos. Inténtalo de nuevo.",
+ "avatarUploadFailed": "Hubo un error al subir tu foto. Inténtalo de nuevo.",
+ "requiredPasswordFields": "Ambos campos son obligatorios.",
+ "resetPasswordDenied": "Hmm, algo no está bien. Puede que no tengas permiso para restablecer esta contraseña o que hayas intentado reutilizar una contraseña reciente.",
+ "resetPasswordSuccess": "Tu contraseña ha sido actualizada. ¡Volvamos al compostaje!",
+ "savePhotosFailed": "Se creó el anuncio, pero no se pudieron guardar las fotos.",
+ "signUpFailed": "No se pudo completar el registro",
+ "tooManyListings": "Has alcanzado el número máximo de anuncios permitidos. Elimina uno de tus tres anuncios actuales para crear uno nuevo.",
+ "tooManyMessages": "Has enviado demasiados mensajes. Inténtalo de nuevo más tarde.",
+ "updateEmailFailed": "Hmm, algo no está bien. Revisa tu correo o inténtalo de nuevo.",
+ "updateFirstNameFailed": "Lo sentimos, no pudimos actualizar tu nombre.",
+ "updateNewsletterFailed": "Lo sentimos, no pudimos actualizar tu preferencia del boletín.",
+ "unexpected": "Ocurrió un error inesperado. Inténtalo de nuevo más tarde.",
+ "validationSummary": "Corrige {count, plural, one {el error anterior} other {los errores anteriores}} y vuelve a intentarlo.",
+ "verificationChallenge": "Completa la verificación.",
+ "verificationFailed": "La verificación falló. Inténtalo de nuevo.",
+ "forgotPasswordSuccess": "Revisa tu bandeja de entrada (y spam) para ver el enlace de restablecimiento de contraseña, suponiendo que ese correo esté vinculado a una cuenta de Peels."
},
"Index": {
"title": "Encuentra un hogar para tus restos de comida, estés donde estés",
"subtitle": "Peels conecta a personas con restos de comida con quienes hacen compost. Es un proyecto comunitario, gratuito y sin fines de lucro.",
+ "hostAvatarAlt": "El avatar de un anfitrión de Peels",
"buttons": {
"browseMap": "Explorar el mapa",
"signUp": "Registrarse en Peels",
@@ -64,11 +162,270 @@
}
},
"Profile": {
- "addListing": "Agregar un anuncio"
+ "addListing": "Agregar un anuncio",
+ "addAnotherListing": "Agregar otro anuncio",
+ "listingCardAlt": "{type, select, residential {Tu avatar para este anuncio residencial} community {Tu avatar para este anuncio comunitario} business {Tu avatar para este anuncio de negocio} other {Tu avatar para este anuncio}}",
+ "listingCardType": "{type, select, residential {Anuncio residencial} community {Anuncio comunitario} business {Anuncio de negocio} other {Anuncio}}",
+ "listingPrompt": "Ponte a ti, a tu espacio comunitario o a tu negocio en el mapa",
+ "sections": {
+ "listings": "Anuncios",
+ "account": "Cuenta",
+ "actions": "Acciones"
+ },
+ "account": {
+ "firstName": "Nombre",
+ "firstNameHint": "Usa tu nombre o un apodo.",
+ "emailSuccess": "Revisa tu correo para ver el enlace de verificación.",
+ "emailHint": "Enviaremos un enlace de verificación a este correo.",
+ "newsletterSubscribedHint": "Te enviaremos actualizaciones ocasionales por correo sobre Peels.",
+ "newsletterNotSubscribedHint": "Solo te enviaremos correos necesarios relacionados con tu cuenta o tus anuncios."
+ },
+ "actions": {
+ "signOutTitle": "Cerrar sesión",
+ "signOutDescription": "¡Hasta pronto!",
+ "exportTitle": "Exportar datos",
+ "exportDescription": "Obtén una copia de tus datos de Peels",
+ "exportDialogTitle": "Próximamente",
+ "exportDialog": "Todavía estamos trabajando en esta función. Mientras tanto, contáctanos y pídenos que exportemos tus datos manualmente.",
+ "deleteTitle": "Eliminar cuenta",
+ "deleteDescription": "Elimina tu cuenta{count, plural, =0 {} one {, anuncio,} other {, anuncios,}} y todos tus datos",
+ "deleteConfirm": "{count, plural, =0 {Sí, eliminar mi cuenta} one {Sí, eliminar mi cuenta y anuncio} other {Sí, eliminar mi cuenta y anuncios}}",
+ "deleteDialog": "¿Seguro que quieres eliminar tu cuenta?{count, plural, =0 {} one { Tu anuncio también se eliminará.} other { Tus anuncios también se eliminarán.}}"
+ }
+ },
+ "Auth": {
+ "complete": {
+ "title": "Iniciando sesión...",
+ "body": "Estamos confirmando tu enlace de forma segura. Te redirigiremos en un momento."
+ },
+ "forgotPassword": {
+ "title": "Olvidé mi contraseña",
+ "sentTitle": "Correo enviado",
+ "body": "Nos pasa a todos. Ingresa tu correo para recibir un enlace de restablecimiento de contraseña."
+ },
+ "resetPassword": {
+ "title": "Restablecer contraseña",
+ "successTitle": "Contraseña actualizada",
+ "body": "Ingresa tu nueva contraseña abajo.",
+ "newPassword": "Nueva contraseña",
+ "confirmPassword": "Confirmar contraseña"
+ },
+ "signIn": {
+ "title": "Iniciar sesión en Peels",
+ "firstTime": "¿Primera vez por aquí? Regístrate",
+ "forgotPassword": "¿Olvidaste tu contraseña?"
+ },
+ "signUp": {
+ "firstName": "Nombre",
+ "newPassword": "Tu nueva contraseña",
+ "newsletterOptIn": "Envíenme actualizaciones ocasionales por correo sobre Peels",
+ "errorWithSupport": "{error} Si crees que puede ser un error, envíanos un correo."
+ },
+ "turnstile": {
+ "expired": "La verificación caducó. Complétala de nuevo.",
+ "timeout": "La comprobación de seguridad no se completó. Prueba desactivar bloqueadores de anuncios y vuelve a intentarlo. Si sigue fallando, prueba otro navegador o red.",
+ "unsupported": "Este navegador no puede completar la comprobación de seguridad. Prueba otro navegador o red.",
+ "failed": "La verificación de seguridad falló con el error #{code}. Inténtalo de nuevo o usa otro navegador."
+ }
+ },
+ "Listings": {
+ "new": {
+ "listingTypeTitle": "¿Qué tipo de anuncio?",
+ "hostTypeTitle": "¿Dónde aceptarás restos de comida?",
+ "listingTypeLabel": "Tipo de anuncio",
+ "hostTypeLabel": "Tipo de anfitrión",
+ "options": {
+ "host": {
+ "title": "Acepto restos de comida",
+ "description": "Otras personas pueden coordinar la entrega de restos de comida en tu casa o huerta comunitaria"
+ },
+ "business": {
+ "title": "Mi negocio dona restos",
+ "description": "Otras personas pueden recoger café usado de tu cafetería, lúpulo de tu cervecería o algo similar"
+ },
+ "residential": {
+ "title": "En mi casa",
+ "description": "Acepto restos en una dirección residencial"
+ },
+ "community": {
+ "title": "En un espacio comunitario",
+ "description": "Gestiono una huerta comunitaria o algo similar"
+ }
+ }
+ },
+ "edit": {
+ "title": "Editar anuncio",
+ "notFound": "Anuncio no encontrado"
+ },
+ "form": {
+ "basics": "Datos básicos",
+ "placeName": "Nombre del lugar",
+ "placeNamePlaceholder": "{type, select, business {Nombre de tu negocio} community {Nombre de tu comunidad} other {Nombre del lugar}}",
+ "yourFirstName": "Tu nombre",
+ "location": "Ubicación",
+ "selectCountry": "Selecciona un país",
+ "locationPlaceholder": "Tu calle o un lugar cercano",
+ "customLocation": "Ubicación personalizada",
+ "locationHint": "{type, select, residential {Empieza a escribir y selecciona una de las opciones sugeridas en el menú.} other {Empieza a escribir y selecciona una de las direcciones sugeridas en el menú.}}",
+ "dragPinHint": "Arrastra el pin para ajustar{obscure, select, true { u ocultar} other {}} tu ubicación.",
+ "businessAddress": "Dirección de tu negocio",
+ "communityAddress": "Dirección de tu comunidad",
+ "residentialAddress": "Tu calle o barrio",
+ "donationDetails": "Detalles de la donación",
+ "donationDetailsPlaceholder": "Detalles de tu donación",
+ "donationDetailsHint": "Qué tipo de restos tienes para donar y los detalles de recogida. Guarda los enlaces para la sección dedicada, más abajo.",
+ "descriptionLabel": "Descripción breve o instrucciones",
+ "descriptionPlaceholder": "{type, select, residential {Acerca de tu anuncio} community {Acerca de tu comunidad} business {Acerca de tu negocio} other {Acerca de tu anuncio}}",
+ "communityDescriptionHint": "Horarios de apertura e instalaciones de compostaje. Guarda los restos que aceptas y cualquier enlace para las secciones dedicadas, más abajo.",
+ "residentialDescriptionHint": "Tu sistema de compostaje y disponibilidad general. Guarda los restos que aceptas para la sección dedicada, más abajo.",
+ "compostingDetails": "Detalles de compostaje",
+ "compostingDetailsHint": "Sé específico para que la gente sepa exactamente qué evitar. Ingresa los elementos por separado para que sean más fáciles de leer.",
+ "acceptedLabel": "¿Qué restos aceptas?",
+ "rejectedLabel": "¿Qué restos no aceptas?",
+ "addItem": "Agregar un elemento",
+ "addAnotherItem": "Agregar otro elemento",
+ "acceptedPlaceholder": "Un elemento que aceptas (p. ej. ‘cáscaras de fruta’)",
+ "acceptedSecondaryPlaceholder": "Otro elemento que aceptas",
+ "rejectedPlaceholder": "Un elemento que no aceptas (p. ej. ‘carne’)",
+ "rejectedSecondaryPlaceholder": "Otro elemento que no aceptas",
+ "media": "Multimedia",
+ "mediaHint": "Opcionalmente muestra {subject} a miembros de Peels.",
+ "mediaResidential": "un poco más sobre tu anuncio",
+ "mediaCommunity": "un poco más sobre tu proyecto comunitario",
+ "mediaBusiness": "tu negocio",
+ "externalLinks": "Enlaces externos",
+ "addLink": "Agregar enlace",
+ "linkPlaceholder": "Tu sitio web o redes sociales",
+ "visibility": "Visibilidad",
+ "visibilityHint": "¿Necesitas un descanso de Peels? Oculta temporalmente este anuncio del mapa.",
+ "mapVisibility": "Visibilidad en el mapa",
+ "showOnMap": "Mostrar este anuncio en el mapa",
+ "hideFromMap": "Ocultar este anuncio del mapa",
+ "adminHint": "Controles solo para administradores de este anuncio.",
+ "stubSettings": "Configuración de borrador",
+ "regularListing": "Este es un anuncio normal de tu propiedad",
+ "stubListing": "Este anuncio es un borrador que otras personas pueden reclamar",
+ "stubActiveHint": "Este anuncio no contendrá tu información de contacto. Otras personas pueden reclamarlo como propio y hacerse cargo.",
+ "stubInactiveHint": "Este anuncio se presentará como cualquier otro."
+ },
+ "read": {
+ "contact": "Contactar con {name}",
+ "about": "Acerca de",
+ "donationDetails": "Detalles de la donación",
+ "accepted": "Qué se acepta",
+ "rejected": "Qué no",
+ "location": "Ubicación",
+ "residentialLocation": "{name} vive en {area}. Pídele su ubicación exacta cuando coordinen la entrega de restos de comida.",
+ "nonResidentialLocation": "{name} es {type} ubicada en {area}.",
+ "thisArea": "esta zona",
+ "businessType": "un negocio",
+ "communityType": "un espacio comunitario",
+ "signInForPhotos": "Inicia sesión para ver las fotos de este anfitrión.",
+ "ownerNote": "Este es tu propio anuncio{stub, select, true {, marcado como borrador} other {}}. {visibility, select, true {¡Se ve bien!} other {Lo has ocultado del mapa, así que solo tú puedes verlo ahora.}}",
+ "stubNote": "Este es un borrador creado por el equipo de Peels. Comprueba la información del anuncio antes de visitar.",
+ "stubClaim": "¿Eres el propietario? Contáctanos para reclamar este anuncio o pedir cambios.",
+ "firstTime": "¿Primera vez por aquí? Regístrate",
+ "residentOf": "Residente de {area}",
+ "localResident": "Residente local",
+ "communityIn": "Comunidad en {area}",
+ "localCommunity": "Comunidad local",
+ "businessIn": "Negocio en {area}",
+ "localBusiness": "Negocio local",
+ "avatarAlt": "El avatar de este anuncio"
+ },
+ "delete": {
+ "confirm": "Sí, eliminar anuncio",
+ "dialog": "¿Seguro que quieres eliminar tu anuncio? Esta acción no se puede deshacer.",
+ "success": "Tu anuncio ha sido eliminado."
+ },
+ "photos": {
+ "alt": "Foto {number}",
+ "dropHere": "Suelta las fotos aquí",
+ "tooMany": "Solo puedes subir hasta {max} fotos",
+ "tooLargeOne": "Tu foto es demasiado grande. El tamaño máximo de archivo es {max} MB.",
+ "tooLargeMany": "Una o más fotos son demasiado grandes. El tamaño máximo es {max} MB por foto."
+ }
+ },
+ "Upload": {
+ "avatarAlt": "Tu avatar",
+ "avatarHint": "Considera subir una foto para que los miembros sepan con quién están hablando."
+ },
+ "Legal": {
+ "agreement": "He leído y acepto los términos de uso y la política de privacidad de Peels",
+ "privacy": "Privacidad",
+ "terms": "Términos"
+ },
+ "Chat": {
+ "send": "Enviar",
+ "placeholder": "Enviar un mensaje{name, select, empty {} other { a {name}}}...",
+ "empty": "Aún no hay mensajes",
+ "threadsTitle": "Chats",
+ "noChats": "Aún no hay chats",
+ "youReachedOut": "Contactaste con {name}",
+ "personReachedOut": "{name} te contactó",
+ "personReachedOutAbout": "{name} te contactó sobre {listing}",
+ "drawerTitle": "Panel de chat",
+ "drawerDescription": "Conversación de este anuncio.",
+ "report": "Denunciar o bloquear",
+ "reportTitle": "Vamos a resolverlo",
+ "reportBody": "Sentimos que estés teniendo problemas con {name}. Contáctanos para denunciar el problema o bloquear que vuelva a contactarte."
+ },
+ "Map": {
+ "drawerTitle": "Detalles del anuncio",
+ "drawerDescription": "Detalles del anuncio seleccionado.",
+ "emptyTitle": "No encontramos nada",
+ "emptyBody": "El anuncio que buscas no existe o fue eliminado. Sentimos decepcionarte.",
+ "searchPlaceholder": "Buscar",
+ "searchError": "Algo salió mal. ¿Intentarlo de nuevo?",
+ "searchNoResults": "Sin resultados. Sigue escribiendo o ajusta tu búsqueda",
+ "didYouKnow": "¿Sabías que?",
+ "steps": {
+ "find": {
+ "title": "Busca un anfitrión",
+ "description": "Selecciona un marcador en el mapa."
+ },
+ "contact": {
+ "title": "Contacta",
+ "description": "Coordina una entrega por chat."
+ },
+ "dropOff": {
+ "title": "Entrega",
+ "description": "¡Conoce a tus vecinos!"
+ }
+ }
+ },
+ "Newsletter": {
+ "title": "Boletín",
+ "inboxTitle": "Recíbelo en tu correo",
+ "inboxDescription": "Activa la opción para recibir futuros números del boletín por correo.",
+ "rss": "O suscríbete al feed RSS.",
+ "latestIssue": "Último número",
+ "pastIssues": "Números anteriores",
+ "issueSubtitle": "Número #{number} · Publicado el {date}",
+ "parent": "Boletín",
+ "stampAlt": "Un sello postal",
+ "aside": {
+ "title": "Acerca de este boletín",
+ "bodyGuest": "Esta es la versión web del boletín enviado por correo a los suscriptores. Únete a Peels para recibir futuros números.",
+ "bodySubscribed": "Esta es la versión web del boletín enviado por correo a los suscriptores. Siéntete libre de compartirlo.",
+ "bodyMember": "Esta es la versión web del boletín enviado por correo a los suscriptores. Edita tus preferencias para recibir futuros números."
+ },
+ "callout": {
+ "guestTitle": "Únete a Peels para recibir el boletín",
+ "guestBody": "Necesitas ser miembro de Peels para recibir el boletín por correo. Registrarse es gratis y solo toma unos segundos.",
+ "alreadySubscribedTitle": "Ya estás suscrito",
+ "alreadySubscribedBody": "El próximo número debería aparecer en tu bandeja de entrada. Mientras tanto, comparte esta página con alguien.",
+ "notSubscribedTitle": "No estás suscrito",
+ "notSubscribedBody": "Cambia tu preferencia de boletín en tu página de Perfil.",
+ "editPreference": "Editar preferencia del boletín"
+ }
+ },
+ "NotFound": {
+ "body": "Lo sentimos, no pudimos encontrar la página que buscabas."
},
"Support": {
"title": "Soporte",
- "subtitle": "We periodically update this page with answers to common questions. Feel free to email us for help with anything else.",
+ "subtitle": "Actualizamos esta página periódicamente con respuestas a preguntas comunes. Si necesitas algo más, puedes contactarnos.",
"peelsFaq": {
"title": "Acerca de Peels",
"whosBehind": {
diff --git a/package.json b/package.json
index c810e015..ad544a2d 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,8 @@
"supabase:reset": "supabase db reset",
"seed:local-media": "node scripts/seed-local-media.mjs",
"supabase:diff": "supabase db diff",
+ "i18n:check": "node scripts/check-i18n-messages.mjs",
+ "check": "npm run i18n:check && npm run format:check",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
diff --git a/scripts/check-i18n-messages.mjs b/scripts/check-i18n-messages.mjs
new file mode 100644
index 00000000..5128175c
--- /dev/null
+++ b/scripts/check-i18n-messages.mjs
@@ -0,0 +1,266 @@
+import fs from "node:fs";
+import path from "node:path";
+import process from "node:process";
+
+const rootDir = process.cwd();
+const configPath = path.join(rootDir, "src/i18n/config.ts");
+const messagesDir = path.join(rootDir, "messages");
+
+function readConfig() {
+ const source = fs.readFileSync(configPath, "utf8");
+ const localesMatch = source.match(/locales\s*=\s*(\[[^\]]+\])\s*as const/);
+ const defaultLocaleMatch = source.match(
+ /defaultLocale:\s*Locale\s*=\s*["']([^"']+)["']/
+ );
+
+ if (!localesMatch) {
+ throw new Error(`Could not find the locales array in ${configPath}`);
+ }
+
+ const locales = JSON.parse(localesMatch[1].replaceAll("'", '"'));
+ const defaultLocale = defaultLocaleMatch?.[1] ?? locales[0];
+
+ if (!locales.includes(defaultLocale)) {
+ throw new Error(
+ `Default locale "${defaultLocale}" is not listed in ${configPath}`
+ );
+ }
+
+ return { locales, defaultLocale };
+}
+
+function readMessages(locale) {
+ const filePath = path.join(messagesDir, `${locale}.json`);
+
+ if (!fs.existsSync(filePath)) {
+ throw new Error(
+ `Missing message file: ${path.relative(rootDir, filePath)}`
+ );
+ }
+
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
+}
+
+function valueType(value) {
+ if (Array.isArray(value)) {
+ return "array";
+ }
+
+ if (value === null) {
+ return "null";
+ }
+
+ return typeof value;
+}
+
+function flattenMessages(value, pathParts = [], entries = new Map()) {
+ if (value && typeof value === "object" && !Array.isArray(value)) {
+ entries.set(pathParts.join("."), {
+ type: "object",
+ value,
+ });
+
+ for (const [key, childValue] of Object.entries(value)) {
+ flattenMessages(childValue, [...pathParts, key], entries);
+ }
+
+ return entries;
+ }
+
+ entries.set(pathParts.join("."), {
+ type: valueType(value),
+ value,
+ });
+
+ return entries;
+}
+
+function leafKeys(entries) {
+ return [...entries]
+ .filter(([, entry]) => entry.type !== "object")
+ .map(([key]) => key)
+ .sort();
+}
+
+function extractIcuArguments(message) {
+ const argumentsSet = new Set();
+
+ for (let index = 0; index < message.length; index += 1) {
+ if (message[index] !== "{") {
+ continue;
+ }
+
+ const match = message
+ .slice(index + 1)
+ .match(/^([A-Za-z_][\w]*)\s*(?:[,}])/);
+
+ if (match) {
+ argumentsSet.add(match[1]);
+ }
+
+ let depth = 1;
+ index += 1;
+
+ while (index < message.length && depth > 0) {
+ if (message[index] === "{") {
+ depth += 1;
+ } else if (message[index] === "}") {
+ depth -= 1;
+ }
+
+ index += 1;
+ }
+ }
+
+ return [...argumentsSet].sort();
+}
+
+function extractRichTextTags(message) {
+ const tags = new Set();
+ const tagPattern = /<\/?([A-Za-z][\w]*)\b[^>]*>/g;
+
+ for (const match of message.matchAll(tagPattern)) {
+ tags.add(match[1]);
+ }
+
+ return [...tags].sort();
+}
+
+function listDifference(left, right) {
+ const rightSet = new Set(right);
+ return left.filter((item) => !rightSet.has(item));
+}
+
+function sameList(left, right) {
+ return (
+ left.length === right.length &&
+ left.every((item, index) => item === right[index])
+ );
+}
+
+function formatList(items) {
+ return items.map((item) => `- ${item}`).join("\n");
+}
+
+function compareLocale(locale, baselineLocale, baselineEntries, localeEntries) {
+ const problems = [];
+ const baselineLeaves = leafKeys(baselineEntries);
+ const localeLeaves = leafKeys(localeEntries);
+ const missingKeys = listDifference(baselineLeaves, localeLeaves);
+ const extraKeys = listDifference(localeLeaves, baselineLeaves);
+
+ if (missingKeys.length > 0) {
+ problems.push(
+ `messages/${locale}.json is missing keys from messages/${baselineLocale}.json:\n${formatList(
+ missingKeys
+ )}`
+ );
+ }
+
+ if (extraKeys.length > 0) {
+ problems.push(
+ `messages/${locale}.json has extra keys not found in messages/${baselineLocale}.json:\n${formatList(
+ extraKeys
+ )}`
+ );
+ }
+
+ for (const [key, baselineEntry] of baselineEntries) {
+ const localeEntry = localeEntries.get(key);
+
+ if (!localeEntry) {
+ continue;
+ }
+
+ if (baselineEntry.type !== localeEntry.type) {
+ problems.push(
+ `messages/${locale}.json has a structural mismatch at "${key}": expected ${baselineEntry.type}, found ${localeEntry.type}`
+ );
+ continue;
+ }
+
+ if (localeEntry.type !== "string") {
+ continue;
+ }
+
+ if (localeEntry.value.trim() === "") {
+ problems.push(`messages/${locale}.json has an empty string at "${key}"`);
+ continue;
+ }
+
+ const baselineArguments = extractIcuArguments(baselineEntry.value);
+ const localeArguments = extractIcuArguments(localeEntry.value);
+
+ if (!sameList(baselineArguments, localeArguments)) {
+ problems.push(
+ `messages/${locale}.json has ICU placeholder mismatch at "${key}": expected {${baselineArguments.join(
+ ", "
+ )}}, found {${localeArguments.join(", ")}}`
+ );
+ }
+
+ const baselineTags = extractRichTextTags(baselineEntry.value);
+ const localeTags = extractRichTextTags(localeEntry.value);
+
+ if (!sameList(baselineTags, localeTags)) {
+ problems.push(
+ `messages/${locale}.json has rich-text tag mismatch at "${key}": expected <${baselineTags.join(
+ ">, <"
+ )}>, found <${localeTags.join(">, <")}>`
+ );
+ }
+ }
+
+ return problems;
+}
+
+function main() {
+ const { locales, defaultLocale } = readConfig();
+ const baselineMessages = readMessages(defaultLocale);
+ const baselineEntries = flattenMessages(baselineMessages);
+ const problems = [];
+
+ for (const locale of locales) {
+ const messages = readMessages(locale);
+ const entries = flattenMessages(messages);
+
+ for (const [key, entry] of entries) {
+ if (entry.type === "string" && entry.value.trim() === "") {
+ problems.push(
+ `messages/${locale}.json has an empty string at "${key}"`
+ );
+ }
+ }
+
+ if (locale === defaultLocale) {
+ continue;
+ }
+
+ problems.push(
+ ...compareLocale(locale, defaultLocale, baselineEntries, entries)
+ );
+ }
+
+ if (problems.length > 0) {
+ console.error(
+ `i18n message check failed with ${problems.length} problem(s):`
+ );
+ console.error("");
+ console.error(problems.join("\n\n"));
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(
+ `i18n message check passed for ${locales.length} locale(s): ${locales.join(
+ ", "
+ )}`
+ );
+}
+
+try {
+ main();
+} catch (error) {
+ console.error(error instanceof Error ? error.message : error);
+ process.exitCode = 1;
+}
diff --git a/src/app/(core)/(interact)/(centered)/profile/page.js b/src/app/(core)/(interact)/(centered)/profile/page.js
index 22e58012..06f705c8 100644
--- a/src/app/(core)/(interact)/(centered)/profile/page.js
+++ b/src/app/(core)/(interact)/(centered)/profile/page.js
@@ -7,6 +7,7 @@ import LegalFooter from "@/components/LegalFooter";
import { styled } from "@pigment-css/react";
import { Suspense } from "react";
import Toast from "@/components/Toast";
+import { getTranslations } from "next-intl/server";
export const metadata = {
title: "Profile",
@@ -14,6 +15,7 @@ export const metadata = {
// Keep URL-based feedback in a client leaf so server rendering is driven by auth/data only.
export default async function ProfilePage() {
+ const t = await getTranslations("Profile.sections");
const supabase = await createClient();
// Get the authenticated user first, then fetch profile data in parallel.
const {
@@ -42,17 +44,17 @@ export default async function ProfilePage() {
-
Listings
+
{t("listings")}
-
Account
+
{t("account")}
-
Actions
+
{t("actions")}
diff --git a/src/app/(core)/(static)/newsletter/(issues)/[slug]/page.tsx b/src/app/(core)/(static)/newsletter/(issues)/[slug]/page.tsx
index 752615c1..ff687409 100644
--- a/src/app/(core)/(static)/newsletter/(issues)/[slug]/page.tsx
+++ b/src/app/(core)/(static)/newsletter/(issues)/[slug]/page.tsx
@@ -12,6 +12,7 @@ import StaticPageSection from "@/components/StaticPageSection";
import HeaderBlock from "@/components/HeaderBlock";
import FooterBlock from "@/components/FooterBlock";
import { getNewsletterIssueImageUrl } from "@/utils/storage";
+import { getTranslations } from "next-intl/server";
type NewsletterIssuePageProps = {
params: Promise<{ slug: string }>;
@@ -54,6 +55,7 @@ export default async function NewsletterIssuePage({
params,
}: NewsletterIssuePageProps) {
const { slug } = await params;
+ const t = await getTranslations("Newsletter");
const { metadata, customMetadata, formattedDate } =
await getNewsletterIssueMetadata(slug);
const title = customMetadata.verboseTitle
@@ -75,8 +77,11 @@ export default async function NewsletterIssuePage({
@@ -85,14 +90,17 @@ export default async function NewsletterIssuePage({
-
- This is your own listing{isStub && ", marked as a stub"}.{" "}
- {visibility
- ? "Lookin’ good!"
- : "You’ve hidden it from the map, so only you can see this right now."}
+ {t("Listings.read.ownerNote", {
+ stub: isStub ? "true" : "false",
+ visibility: visibility ? "true" : "false",
+ })}
- This is a stub created by the Peels team. Double-check the listing
- information before visiting.
-
-
- Are you the owner?{" "}
-
- Reach out
- {" "}
- to claim this listing or to request changes.
+ {t.rich("Listings.read.stubClaim", {
+ link: (chunks) => (
+
+ {chunks}
+
+ ),
+ })}
@@ -81,10 +83,12 @@ function ListingCta({ viewer, slug, visibility = true, isStub = false }) {
width="full"
href={`/sign-in?redirect_to=/listings/${slug}`}
>
- Sign in to contact
+ {t("Actions.signInToContact")}
- First time here? Sign up
+ {t.rich("Listings.read.firstTime", {
+ link: (chunks) => {chunks},
+ })}
);
diff --git a/src/components/ListingHeader/ListingHeader.jsx b/src/components/ListingHeader/ListingHeader.jsx
index 064f6936..4458567c 100644
--- a/src/components/ListingHeader/ListingHeader.jsx
+++ b/src/components/ListingHeader/ListingHeader.jsx
@@ -4,6 +4,7 @@ import Avatar from "@/components/Avatar";
import Lozenge from "@/components/Lozenge";
import { styled } from "@pigment-css/react";
+import { useTranslations } from "next-intl";
const listingHeaderStyles = {
display: "flex",
@@ -96,6 +97,7 @@ const StyledLozenge = styled(Lozenge)(({ theme }) => ({
}));
function ListingHeader({ presentation, listing, listingName, user }) {
+ const t = useTranslations();
const avatarProps = getListingAvatar(listing, user);
return (
@@ -105,7 +107,7 @@ function ListingHeader({ presentation, listing, listingName, user }) {
src={avatarProps?.isDemo ? avatarProps.path : undefined}
bucket={!avatarProps?.isDemo ? avatarProps?.bucket : undefined}
filename={!avatarProps?.isDemo ? avatarProps?.filename : undefined}
- alt={avatarProps?.alt || "The avatar for this listing"}
+ alt={avatarProps?.alt || t("Listings.read.avatarAlt")}
size="large"
listing={listing}
/>
@@ -117,31 +119,31 @@ function ListingHeader({ presentation, listing, listingName, user }) {
{listing?.type === "residential" && (