Merge pull request #1 from knadh/master

update
This commit is contained in:
TomBoss 2021-02-15 19:06:05 +01:00 committed by GitHub
commit b4fea57543
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 495 additions and 490 deletions

View File

@ -118,15 +118,14 @@ func handleViewCampaignMessage(c echo.Context) error {
} }
// Get the subscriber. // Get the subscriber.
var sub models.Subscriber sub, err := getSubscriber(0, subUUID, "", app)
if err := app.queries.GetSubscriber.Get(&sub, 0, subUUID); err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return c.Render(http.StatusNotFound, tplMessage, return c.Render(http.StatusNotFound, tplMessage,
makeMsgTpl(app.i18n.T("public.notFoundTitle"), "", makeMsgTpl(app.i18n.T("public.notFoundTitle"), "",
app.i18n.T("public.errorFetchingEmail"))) app.i18n.T("public.errorFetchingEmail")))
} }
app.log.Printf("error fetching campaign subscriber: %v", err)
return c.Render(http.StatusInternalServerError, tplMessage, return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", makeMsgTpl(app.i18n.T("public.errorTitle"), "",
app.i18n.Ts("public.errorFetchingCampaign"))) app.i18n.Ts("public.errorFetchingCampaign")))
@ -324,14 +323,18 @@ func handleSubscriptionForm(c echo.Context) error {
// Insert the subscriber into the DB. // Insert the subscriber into the DB.
req.Status = models.SubscriberStatusEnabled req.Status = models.SubscriberStatusEnabled
req.ListUUIDs = pq.StringArray(req.SubListUUIDs) req.ListUUIDs = pq.StringArray(req.SubListUUIDs)
if _, err := insertSubscriber(req.SubReq, app); err != nil && err != errSubscriberExists { _, _, hasOptin, err := insertSubscriber(req.SubReq, app)
if err != nil {
return c.Render(http.StatusInternalServerError, tplMessage, return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", fmt.Sprintf("%s", err.(*echo.HTTPError).Message))) makeMsgTpl(app.i18n.T("public.errorTitle"), "", fmt.Sprintf("%s", err.(*echo.HTTPError).Message)))
} }
return c.Render(http.StatusOK, tplMessage, msg := "public.subConfirmed"
makeMsgTpl(app.i18n.T("public.subTitle"), "", if hasOptin {
app.i18n.Ts("public.subConfirmed"))) msg = "public.subOptinPending"
}
return c.Render(http.StatusOK, tplMessage, makeMsgTpl(app.i18n.T("public.subTitle"), "", app.i18n.Ts(msg)))
} }
// handleLinkRedirect redirects a link UUID to its original underlying link // handleLinkRedirect redirects a link UUID to its original underlying link

View File

@ -79,7 +79,11 @@ func handleGetSubscriber(c echo.Context) error {
id, _ = strconv.Atoi(c.Param("id")) id, _ = strconv.Atoi(c.Param("id"))
) )
sub, err := getSubscriber(id, app) if id < 1 {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
sub, err := getSubscriber(id, "", "", app)
if err != nil { if err != nil {
return err return err
} }
@ -273,14 +277,13 @@ func handleCreateSubscriber(c echo.Context) error {
} }
// Insert the subscriber into the DB. // Insert the subscriber into the DB.
sub, err := insertSubscriber(req, app) sub, isNew, _, err := insertSubscriber(req, app)
if err != nil { if err != nil {
if err == errSubscriberExists {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.emailExists"))
}
return err return err
} }
if !isNew {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.emailExists"))
}
return c.JSON(http.StatusOK, okResp{sub}) return c.JSON(http.StatusOK, okResp{sub})
} }
@ -321,11 +324,11 @@ func handleUpdateSubscriber(c echo.Context) error {
} }
// Send a confirmation e-mail (if there are any double opt-in lists). // Send a confirmation e-mail (if there are any double opt-in lists).
sub, err := getSubscriber(int(id), app) sub, err := getSubscriber(int(id), "", "", app)
if err != nil { if err != nil {
return err return err
} }
_ = sendOptinConfirmation(sub, []int64(req.Lists), app) _, _ = sendOptinConfirmation(sub, []int64(req.Lists), app)
return c.JSON(http.StatusOK, okResp{sub}) return c.JSON(http.StatusOK, okResp{sub})
} }
@ -335,7 +338,6 @@ func handleSubscriberSendOptin(c echo.Context) error {
var ( var (
app = c.Get("app").(*App) app = c.Get("app").(*App)
id, _ = strconv.Atoi(c.Param("id")) id, _ = strconv.Atoi(c.Param("id"))
out models.Subscribers
) )
if id < 1 { if id < 1 {
@ -343,19 +345,15 @@ func handleSubscriberSendOptin(c echo.Context) error {
} }
// Fetch the subscriber. // Fetch the subscriber.
err := app.queries.GetSubscriber.Select(&out, id, nil) out, err := getSubscriber(id, "", "", app)
if err != nil { if err != nil {
app.log.Printf("error fetching subscriber: %v", err) app.log.Printf("error fetching subscriber: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching", app.i18n.Ts("globals.messages.errorFetching",
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err))) "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
} }
if len(out) == 0 {
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.subscriber}"))
}
if err := sendOptinConfirmation(out[0], nil, app); err != nil { if _, err := sendOptinConfirmation(out, nil, app); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.T("subscribers.errorSendingOptin")) app.i18n.T("subscribers.errorSendingOptin"))
} }
@ -619,56 +617,53 @@ func handleExportSubscriberData(c echo.Context) error {
return c.Blob(http.StatusOK, "application/json", b) return c.Blob(http.StatusOK, "application/json", b)
} }
// insertSubscriber inserts a subscriber and returns the ID. // insertSubscriber inserts a subscriber and returns the ID. The first bool indicates if
func insertSubscriber(req subimporter.SubReq, app *App) (models.Subscriber, error) { // it was a new subscriber, and the second bool indicates if the subscriber was sent an optin confirmation.
func insertSubscriber(req subimporter.SubReq, app *App) (models.Subscriber, bool, bool, error) {
uu, err := uuid.NewV4() uu, err := uuid.NewV4()
if err != nil { if err != nil {
return req.Subscriber, err return req.Subscriber, false, false, err
} }
req.UUID = uu.String() req.UUID = uu.String()
err = app.queries.InsertSubscriber.Get(&req.ID, isNew := true
if err = app.queries.InsertSubscriber.Get(&req.ID,
req.UUID, req.UUID,
req.Email, req.Email,
strings.TrimSpace(req.Name), strings.TrimSpace(req.Name),
req.Status, req.Status,
req.Attribs, req.Attribs,
req.Lists, req.Lists,
req.ListUUIDs) req.ListUUIDs); err != nil {
if err != nil {
if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" { if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" {
return req.Subscriber, errSubscriberExists isNew = false
} else {
// return req.Subscriber, errSubscriberExists
app.log.Printf("error inserting subscriber: %v", err)
return req.Subscriber, false, false, echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorCreating",
"name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
} }
app.log.Printf("error inserting subscriber: %v", err)
return req.Subscriber, echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorCreating",
"name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
} }
// Fetch the subscriber's full data. // Fetch the subscriber's full data. If the subscriber already existed and wasn't
sub, err := getSubscriber(req.ID, app) // created, the id will be empty. Fetch the details by e-mail then.
sub, err := getSubscriber(req.ID, "", strings.ToLower(req.Email), app)
if err != nil { if err != nil {
return sub, err return sub, false, false, err
} }
// Send a confirmation e-mail (if there are any double opt-in lists). // Send a confirmation e-mail (if there are any double opt-in lists).
_ = sendOptinConfirmation(sub, []int64(req.Lists), app) num, _ := sendOptinConfirmation(sub, []int64(req.Lists), app)
return sub, nil return sub, isNew, num > 0, nil
} }
// getSubscriber gets a single subscriber by ID. // getSubscriber gets a single subscriber by ID, uuid, or e-mail in that order.
func getSubscriber(id int, app *App) (models.Subscriber, error) { // Only one of these params should have a value.
var ( func getSubscriber(id int, uuid, email string, app *App) (models.Subscriber, error) {
out models.Subscribers var out models.Subscribers
)
if id < 1 { if err := app.queries.GetSubscriber.Select(&out, id, uuid, email); err != nil {
return models.Subscriber{},
echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
if err := app.queries.GetSubscriber.Select(&out, id, nil); err != nil {
app.log.Printf("error fetching subscriber: %v", err) app.log.Printf("error fetching subscriber: %v", err)
return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError, return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching", app.i18n.Ts("globals.messages.errorFetching",
@ -733,8 +728,9 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool,
} }
// sendOptinConfirmation sends a double opt-in confirmation e-mail to a subscriber // sendOptinConfirmation sends a double opt-in confirmation e-mail to a subscriber
// if at least one of the given listIDs is set to optin=double // if at least one of the given listIDs is set to optin=double. It returns the number of
func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) error { // opt-in lists that were found.
func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) (int, error) {
var lists []models.List var lists []models.List
// Fetch double opt-in lists from the given list IDs. // Fetch double opt-in lists from the given list IDs.
@ -742,12 +738,12 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err
if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil, if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil,
pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil { pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil {
app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err)) app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
return err return 0, err
} }
// None. // None.
if len(lists) == 0 { if len(lists) == 0 {
return nil return 0, nil
} }
var ( var (
@ -764,9 +760,9 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err
if err := app.sendNotification([]string{sub.Email}, if err := app.sendNotification([]string{sub.Email},
app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil { app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil {
app.log.Printf("error sending opt-in e-mail: %s", err) app.log.Printf("error sending opt-in e-mail: %s", err)
return err return 0, err
} }
return nil return len(lists), nil
} }
// sanitizeSQLExp does basic sanitisation on arbitrary // sanitizeSQLExp does basic sanitisation on arbitrary

View File

@ -5,7 +5,7 @@
<b-tag v-for="l in selectedItems" <b-tag v-for="l in selectedItems"
:key="l.id" :key="l.id"
:class="l.subscriptionStatus" :class="l.subscriptionStatus"
:closable="!disabled && l.subscriptionStatus !== 'unsubscribed'" :closable="true"
:data-id="l.id" :data-id="l.id"
@close="removeList(l.id)"> @close="removeList(l.id)">
{{ l.name }} <sup>{{ l.subscriptionStatus }}</sup> {{ l.name }} <sup>{{ l.subscriptionStatus }}</sup>

View File

@ -259,6 +259,7 @@
"public.privacyWipeHelp": "Delete all your subscriptions and related data from the database permanently.", "public.privacyWipeHelp": "Delete all your subscriptions and related data from the database permanently.",
"public.sub": "Subscribe", "public.sub": "Subscribe",
"public.subConfirmed": "Subscribed successfully.", "public.subConfirmed": "Subscribed successfully.",
"public.subOptinPending": "An e-mail has been sent to you to confirm your subscription(s).",
"public.subConfirmedTitle": "Confirmed", "public.subConfirmedTitle": "Confirmed",
"public.subName": "Name (optional)", "public.subName": "Name (optional)",
"public.subNotFound": "Subscription not found.", "public.subNotFound": "Subscription not found.",

View File

@ -1,432 +1,432 @@
{ {
"_.code": "fr", "_.code": "fr",
"_.name": "Français (fr)", "_.name": "Français (fr)",
"admin.errorMarshallingConfig": "Erreur lors de la lecture de la configuration : {error}", "admin.errorMarshallingConfig": "Erreur lors de la lecture de la configuration : {error}",
"campaigns.addAltText": "Ajouter un message de replacement en texte brut", "campaigns.addAltText": "Ajouter un message de replacement en texte brut",
"campaigns.cantUpdate": "Impossible de mettre à jour une campagne en cours ou terminée.", "campaigns.cantUpdate": "Impossible de mettre à jour une campagne en cours ou terminée.",
"campaigns.clicks": "Clics", "campaigns.clicks": "Clics",
"campaigns.confirmDelete": "Supprimer {name}", "campaigns.confirmDelete": "Supprimer {name}",
"campaigns.confirmSchedule": "Cette campagne démarrera automatiquement à la date et à l'heure planifiées. Planifier maintenant ?", "campaigns.confirmSchedule": "Cette campagne démarrera automatiquement à la date et à l'heure planifiées. Planifier maintenant ?",
"campaigns.confirmSwitchFormat": "Le contenu peut perdre sa mise en forme. Continuer ?", "campaigns.confirmSwitchFormat": "Le contenu peut perdre sa mise en forme. Continuer ?",
"campaigns.content": "Contenu", "campaigns.content": "Contenu",
"campaigns.contentHelp": "Contenu ici", "campaigns.contentHelp": "Contenu ici",
"campaigns.continue": "Continuer", "campaigns.continue": "Continuer",
"campaigns.copyOf": "Copie de {name}", "campaigns.copyOf": "Copie de {name}",
"campaigns.dateAndTime": "Date et heure", "campaigns.dateAndTime": "Date et heure",
"campaigns.ended": "Terminé", "campaigns.ended": "Terminé",
"campaigns.errorSendTest": "Erreur lors de l'envoi test : {error}", "campaigns.errorSendTest": "Erreur lors de l'envoi test : {error}",
"campaigns.fieldInvalidBody": "Erreur lors de la compilation du corps de la campagne : {error}", "campaigns.fieldInvalidBody": "Erreur lors de la compilation du corps de la campagne : {error}",
"campaigns.fieldInvalidFromEmail": "`Émetteur` non valide.", "campaigns.fieldInvalidFromEmail": "`Émetteur` non valide.",
"campaigns.fieldInvalidListIDs": "ID de liste non valides.", "campaigns.fieldInvalidListIDs": "ID de liste non valides.",
"campaigns.fieldInvalidMessenger": "Messager inconnu {name}.", "campaigns.fieldInvalidMessenger": "Messager inconnu {name}.",
"campaigns.fieldInvalidName": "Longueur du nom non valide.", "campaigns.fieldInvalidName": "Longueur du nom non valide.",
"campaigns.fieldInvalidSendAt": "La date planifiée doit être future.", "campaigns.fieldInvalidSendAt": "La date planifiée doit être future.",
"campaigns.fieldInvalidSubject": "Longueur d'objet non valide.", "campaigns.fieldInvalidSubject": "Longueur d'objet non valide.",
"campaigns.fromAddress": "Émetteur", "campaigns.fromAddress": "Émetteur",
"campaigns.fromAddressPlaceholder": "Votre nom <noreply@votresite.com>", "campaigns.fromAddressPlaceholder": "Votre nom <noreply@votresite.com>",
"campaigns.invalid": "Campagne non valide", "campaigns.invalid": "Campagne non valide",
"campaigns.needsSendAt": "Une date est nécessaire pour planifier la campagne.", "campaigns.needsSendAt": "Une date est nécessaire pour planifier la campagne.",
"campaigns.newCampaign": "Nouvelle campagne", "campaigns.newCampaign": "Nouvelle campagne",
"campaigns.noKnownSubsToTest": "Aucun abonné connu à tester.", "campaigns.noKnownSubsToTest": "Aucun abonné connu à tester.",
"campaigns.noOptinLists": "Aucune liste opt-in trouvée pour créer une campagne.", "campaigns.noOptinLists": "Aucune liste opt-in trouvée pour créer une campagne.",
"campaigns.noSubs": "Il n'y a aucun abonné dans les listes sélectionnées pour créer la campagne.", "campaigns.noSubs": "Il n'y a aucun abonné dans les listes sélectionnées pour créer la campagne.",
"campaigns.noSubsToTest": "Il n'y a aucun abonné à cibler.", "campaigns.noSubsToTest": "Il n'y a aucun abonné à cibler.",
"campaigns.notFound": "Campagne introuvable.", "campaigns.notFound": "Campagne introuvable.",
"campaigns.onlyActiveCancel": "Seules les campagnes actives peuvent être annulées.", "campaigns.onlyActiveCancel": "Seules les campagnes actives peuvent être annulées.",
"campaigns.onlyActivePause": "Seules les campagnes actives peuvent être mises en pause.", "campaigns.onlyActivePause": "Seules les campagnes actives peuvent être mises en pause.",
"campaigns.onlyDraftAsScheduled": "Seuls les brouillons de campagnes peuvent être planifiés.", "campaigns.onlyDraftAsScheduled": "Seuls les brouillons de campagnes peuvent être planifiés.",
"campaigns.onlyPausedDraft": "Seuls les brouillons et les campagnes mis en pause peuvent être lancés.", "campaigns.onlyPausedDraft": "Seuls les brouillons et les campagnes mis en pause peuvent être lancés.",
"campaigns.onlyScheduledAsDraft": "Seules les campagnes planifiées peuvent être enregistrées en tant que brouillons.", "campaigns.onlyScheduledAsDraft": "Seules les campagnes planifiées peuvent être enregistrées en tant que brouillons.",
"campaigns.pause": "Pause", "campaigns.pause": "Pause",
"campaigns.plainText": "Texte brut", "campaigns.plainText": "Texte brut",
"campaigns.preview": "Aperçu", "campaigns.preview": "Aperçu",
"campaigns.progress": "Avancement", "campaigns.progress": "Avancement",
"campaigns.queryPlaceholder": "Nom ou objet", "campaigns.queryPlaceholder": "Nom ou objet",
"campaigns.rawHTML": "HTML brut", "campaigns.rawHTML": "HTML brut",
"campaigns.removeAltText": "Supprimer le message de replacement en texte brut", "campaigns.removeAltText": "Supprimer le message de replacement en texte brut",
"campaigns.richText": "Texte riche", "campaigns.richText": "Texte riche",
"campaigns.schedule": "Planifier la campagne", "campaigns.schedule": "Planifier la campagne",
"campaigns.scheduled": "Planifiée", "campaigns.scheduled": "Planifiée",
"campaigns.send": "Envoyer", "campaigns.send": "Envoyer",
"campaigns.sendLater": "Envoyer plus tard", "campaigns.sendLater": "Envoyer plus tard",
"campaigns.sendTest": "Envoyer un message de test", "campaigns.sendTest": "Envoyer un message de test",
"campaigns.sendTestHelp": "Pour ajouter plusieurs destinataires, appuyez sur Entrée après avoir tapé une adresse. Les adresses doivent appartenir à des abonnés existants.", "campaigns.sendTestHelp": "Pour ajouter plusieurs destinataires, appuyez sur Entrée après avoir tapé une adresse. Les adresses doivent appartenir à des abonnés existants.",
"campaigns.sendToLists": "Listes à envoyer à", "campaigns.sendToLists": "Listes à envoyer à",
"campaigns.sent": "Envoyé", "campaigns.sent": "Envoyé",
"campaigns.start": "Lancer la campagne", "campaigns.start": "Lancer la campagne",
"campaigns.started": "\"{name}\" a commencé", "campaigns.started": "\"{name}\" a commencé",
"campaigns.startedAt": "Commencé", "campaigns.startedAt": "Commencé",
"campaigns.stats": "Statistiques", "campaigns.stats": "Statistiques",
"campaigns.status.cancelled": "Annulé", "campaigns.status.cancelled": "Annulé",
"campaigns.status.draft": "Brouillon", "campaigns.status.draft": "Brouillon",
"campaigns.status.finished": "Terminé", "campaigns.status.finished": "Terminé",
"campaigns.status.paused": "En pause", "campaigns.status.paused": "En pause",
"campaigns.status.running": "En cours", "campaigns.status.running": "En cours",
"campaigns.status.scheduled": "Planifiée", "campaigns.status.scheduled": "Planifiée",
"campaigns.statusChanged": "\"{name}\" est {status}", "campaigns.statusChanged": "\"{name}\" est {status}",
"campaigns.subject": "Objet", "campaigns.subject": "Objet",
"campaigns.testEmails": "Emails de test", "campaigns.testEmails": "Emails de test",
"campaigns.testSent": "Message de test envoyé", "campaigns.testSent": "Message de test envoyé",
"campaigns.timestamps": "Horodatages", "campaigns.timestamps": "Horodatages",
"campaigns.views": "Vues", "campaigns.views": "Vues",
"dashboard.campaignViews": "Vues de la campagne", "dashboard.campaignViews": "Vues de la campagne",
"dashboard.linkClicks": "Clics sur les liens", "dashboard.linkClicks": "Clics sur les liens",
"dashboard.messagesSent": "Messages envoyés", "dashboard.messagesSent": "Messages envoyés",
"dashboard.orphanSubs": "Orphelins", "dashboard.orphanSubs": "Orphelins",
"email.data.info": "Un fichier au format JSON contenant l'ensemble des données enregistrées à votre sujet est jointe. Il peut être visualisé dans un éditeur de texte.", "email.data.info": "Un fichier au format JSON contenant l'ensemble des données enregistrées à votre sujet est jointe. Il peut être visualisé dans un éditeur de texte.",
"email.data.title": "Vos données", "email.data.title": "Vos données",
"email.optin.confirmSub": "Confirmer l'abonnement", "email.optin.confirmSub": "Confirmer l'abonnement",
"email.optin.confirmSubHelp": "Confirmez votre abonnement en cliquant sur le bouton ci-dessous.", "email.optin.confirmSubHelp": "Confirmez votre abonnement en cliquant sur le bouton ci-dessous.",
"email.optin.confirmSubInfo": "Vous avez été ajouté aux listes suivantes :", "email.optin.confirmSubInfo": "Vous avez été ajouté aux listes suivantes :",
"email.optin.confirmSubTitle": "Confirmer l'abonnement", "email.optin.confirmSubTitle": "Confirmer l'abonnement",
"email.optin.confirmSubWelcome": "Bonjour,", "email.optin.confirmSubWelcome": "Bonjour,",
"email.optin.privateList": "Liste privée", "email.optin.privateList": "Liste privée",
"email.status.campaignReason": "Raison", "email.status.campaignReason": "Raison",
"email.status.campaignSent": "Envoyé", "email.status.campaignSent": "Envoyé",
"email.status.campaignUpdateTitle": "Mise à jour de la campagne", "email.status.campaignUpdateTitle": "Mise à jour de la campagne",
"email.status.importFile": "Fichier", "email.status.importFile": "Fichier",
"email.status.importRecords": "Enregistrements", "email.status.importRecords": "Enregistrements",
"email.status.importTitle": "Importer la mise à jour", "email.status.importTitle": "Importer la mise à jour",
"email.status.status": "Statut", "email.status.status": "Statut",
"email.unsub": "Se désabonner", "email.unsub": "Se désabonner",
"email.unsubHelp": "Vous ne souhaitez pas recevoir ces emails ?", "email.unsubHelp": "Vous ne souhaitez pas recevoir ces emails ?",
"forms.formHTML": "Formulaire HTML", "forms.formHTML": "Formulaire HTML",
"forms.formHTMLHelp": "Utilisez le code HTML suivant pour afficher un formulaire d'abonnement sur une page Web externe. Le formulaire doit avoir le champ email et un ou plusieurs champs `l` (listes UUID). Le champ nom est facultatif.", "forms.formHTMLHelp": "Utilisez le code HTML suivant pour afficher un formulaire d'abonnement sur une page Web externe. Le formulaire doit avoir le champ email et un ou plusieurs champs `l` (listes UUID). Le champ nom est facultatif.",
"forms.noPublicLists": "Il n'y a pas de listes publiques pour générer un formulaire.", "forms.noPublicLists": "Il n'y a pas de listes publiques pour générer un formulaire.",
"forms.publicLists": "Listes publiques", "forms.publicLists": "Listes publiques",
"forms.publicSubPage": "Page d'abonnement publique", "forms.publicSubPage": "Page d'abonnement publique",
"forms.selectHelp": "Sélectionnez les listes à ajouter au formulaire.", "forms.selectHelp": "Sélectionnez les listes à ajouter au formulaire.",
"forms.title": "Formulaires", "forms.title": "Formulaires",
"globals.buttons.add": "Ajouter", "globals.buttons.add": "Ajouter",
"globals.buttons.addNew": "Ajouter nouveau", "globals.buttons.addNew": "Ajouter nouveau",
"globals.buttons.cancel": "Annuler", "globals.buttons.cancel": "Annuler",
"globals.buttons.clone": "Cloner", "globals.buttons.clone": "Cloner",
"globals.buttons.close": "Fermer", "globals.buttons.close": "Fermer",
"globals.buttons.continue": "Continuer", "globals.buttons.continue": "Continuer",
"globals.buttons.delete": "Supprimer", "globals.buttons.delete": "Supprimer",
"globals.buttons.edit": "Éditer", "globals.buttons.edit": "Éditer",
"globals.buttons.enabled": "Activée", "globals.buttons.enabled": "Activée",
"globals.buttons.learnMore": "En savoir plus", "globals.buttons.learnMore": "En savoir plus",
"globals.buttons.new": "Nouveau", "globals.buttons.new": "Nouveau",
"globals.buttons.ok": "Ok", "globals.buttons.ok": "Ok",
"globals.buttons.remove": "Supprimer", "globals.buttons.remove": "Supprimer",
"globals.buttons.save": "Enregistrer", "globals.buttons.save": "Enregistrer",
"globals.buttons.saveChanges": "Enregistrer les changements", "globals.buttons.saveChanges": "Enregistrer les changements",
"globals.days.1": "lun", "globals.days.1": "lun",
"globals.days.2": "mar", "globals.days.2": "mar",
"globals.days.3": "mer", "globals.days.3": "mer",
"globals.days.4": "jeu", "globals.days.4": "jeu",
"globals.days.5": "ven", "globals.days.5": "ven",
"globals.days.6": "sam", "globals.days.6": "sam",
"globals.days.7": "dim", "globals.days.7": "dim",
"globals.fields.createdAt": "Créé le", "globals.fields.createdAt": "Créé le",
"globals.fields.id": "ID", "globals.fields.id": "ID",
"globals.fields.name": "Nom", "globals.fields.name": "Nom",
"globals.fields.status": "Statut", "globals.fields.status": "Statut",
"globals.fields.type": "Type", "globals.fields.type": "Type",
"globals.fields.updatedAt": "Mis à jour le", "globals.fields.updatedAt": "Mis à jour le",
"globals.fields.uuid": "UUID", "globals.fields.uuid": "UUID",
"globals.messages.confirm": "Êtes-vous sûr ?", "globals.messages.confirm": "Êtes-vous sûr ?",
"globals.messages.created": "\"{name}\" créé", "globals.messages.created": "\"{name}\" créé",
"globals.messages.deleted": "\"{name}\" supprimé", "globals.messages.deleted": "\"{name}\" supprimé",
"globals.messages.emptyState": "Rien", "globals.messages.emptyState": "Rien",
"globals.messages.errorCreating": "Erreur lors de la création de {name} : {error}", "globals.messages.errorCreating": "Erreur lors de la création de {name} : {error}",
"globals.messages.errorDeleting": "Erreur lors de la suppression de {name} : {error}", "globals.messages.errorDeleting": "Erreur lors de la suppression de {name} : {error}",
"globals.messages.errorFetching": "Erreur lors de la récupération de {name} : {error}", "globals.messages.errorFetching": "Erreur lors de la récupération de {name} : {error}",
"globals.messages.errorUUID": "Erreur lors de la génération de l'UUID : {error}", "globals.messages.errorUUID": "Erreur lors de la génération de l'UUID : {error}",
"globals.messages.errorUpdating": "Erreur lors de la mise à jour de {name}: {error}", "globals.messages.errorUpdating": "Erreur lors de la mise à jour de {name}: {error}",
"globals.messages.invalidID": "ID non valide", "globals.messages.invalidID": "ID non valide",
"globals.messages.invalidUUID": "UUID non valide", "globals.messages.invalidUUID": "UUID non valide",
"globals.messages.notFound": "{name} introuvable", "globals.messages.notFound": "{name} introuvable",
"globals.messages.passwordChange": "Entrez une valeur à modifier", "globals.messages.passwordChange": "Entrez une valeur à modifier",
"globals.messages.updated": "\"{name}\" mis à jour", "globals.messages.updated": "\"{name}\" mis à jour",
"globals.months.1": "jan", "globals.months.1": "jan",
"globals.months.10": "oct", "globals.months.10": "oct",
"globals.months.11": "nov", "globals.months.11": "nov",
"globals.months.12": "déc", "globals.months.12": "déc",
"globals.months.2": "fév", "globals.months.2": "fév",
"globals.months.3": "mars", "globals.months.3": "mars",
"globals.months.4": "avr", "globals.months.4": "avr",
"globals.months.5": "mai", "globals.months.5": "mai",
"globals.months.6": "juin", "globals.months.6": "juin",
"globals.months.7": "juil", "globals.months.7": "juil",
"globals.months.8": "août", "globals.months.8": "août",
"globals.months.9": "sept", "globals.months.9": "sept",
"globals.terms.campaign": "Campagne | Campagnes", "globals.terms.campaign": "Campagne | Campagnes",
"globals.terms.campaigns": "Campagnes", "globals.terms.campaigns": "Campagnes",
"globals.terms.dashboard": "Tableau de bord", "globals.terms.dashboard": "Tableau de bord",
"globals.terms.list": "Liste | Listes", "globals.terms.list": "Liste | Listes",
"globals.terms.lists": "Listes", "globals.terms.lists": "Listes",
"globals.terms.media": "Médias | Médias", "globals.terms.media": "Médias | Médias",
"globals.terms.messenger": "Messenger | Messagers", "globals.terms.messenger": "Messenger | Messagers",
"globals.terms.messengers": "Messagers", "globals.terms.messengers": "Messagers",
"globals.terms.settings": "Paramètres", "globals.terms.settings": "Paramètres",
"globals.terms.subscriber": "Abonné | Abonnés", "globals.terms.subscriber": "Abonné | Abonnés",
"globals.terms.subscribers": "Abonnés", "globals.terms.subscribers": "Abonnés",
"globals.terms.tag": "Étiquette | Étiquettes", "globals.terms.tag": "Étiquette | Étiquettes",
"globals.terms.tags": "Étiquettes", "globals.terms.tags": "Étiquettes",
"globals.terms.template": "Modèle | Modèles", "globals.terms.template": "Modèle | Modèles",
"globals.terms.templates": "Modèles", "globals.terms.templates": "Modèles",
"import.alreadyRunning": "Une importation est déjà en cours. Attendez qu'elle se termine ou arrêtez-la avant de réessayer.", "import.alreadyRunning": "Une importation est déjà en cours. Attendez qu'elle se termine ou arrêtez-la avant de réessayer.",
"import.blocklist": "Liste des adresses bloquées", "import.blocklist": "Liste des adresses bloquées",
"import.csvDelim": "Délimiteur CSV", "import.csvDelim": "Délimiteur CSV",
"import.csvDelimHelp": "Le délimiteur par défaut est la virgule.", "import.csvDelimHelp": "Le délimiteur par défaut est la virgule.",
"import.csvExample": "Exemple CSV brut", "import.csvExample": "Exemple CSV brut",
"import.csvFile": "Fichier CSV ou ZIP", "import.csvFile": "Fichier CSV ou ZIP",
"import.csvFileHelp": "Cliquez ou glissez-déposez ici un fichier CSV ou ZIP", "import.csvFileHelp": "Cliquez ou glissez-déposez ici un fichier CSV ou ZIP",
"import.errorCopyingFile": "Erreur lors de la copie du fichier : {error}", "import.errorCopyingFile": "Erreur lors de la copie du fichier : {error}",
"import.errorProcessingZIP": "Erreur lors du traitement du fichier ZIP : {error}", "import.errorProcessingZIP": "Erreur lors du traitement du fichier ZIP : {error}",
"import.errorStarting": "Erreur lors du démarrage de l'importation : {error}", "import.errorStarting": "Erreur lors du démarrage de l'importation : {error}",
"import.importDone": "Terminé", "import.importDone": "Terminé",
"import.importStarted": "L'importation a commencé", "import.importStarted": "L'importation a commencé",
"import.instructions": "Instructions", "import.instructions": "Instructions",
"import.instructionsHelp": "Téléchargez un fichier CSV ou un fichier ZIP contenant un seul fichier CSV pour importer des abonnés en masse. Le fichier CSV doit avoir les en-têtes suivants avec les noms de colonne exacts. Les attributs (facultatifs) doivent être des chaînes JSON valides entre guillemets doubles.", "import.instructionsHelp": "Téléchargez un fichier CSV ou un fichier ZIP contenant un seul fichier CSV pour importer des abonnés en masse. Le fichier CSV doit avoir les en-têtes suivants avec les noms de colonne exacts. Les attributs (facultatifs) doivent être des chaînes JSON valides entre guillemets doubles.",
"import.invalidDelim": "Le délimiteur doit être un seul caractère.", "import.invalidDelim": "Le délimiteur doit être un seul caractère.",
"import.invalidFile": "Fichier non valide : {error}", "import.invalidFile": "Fichier non valide : {error}",
"import.invalidMode": "Mode invalide", "import.invalidMode": "Mode invalide",
"import.invalidParams": "Paramètres non valides : {error}", "import.invalidParams": "Paramètres non valides : {error}",
"import.listSubHelp": "Listes auxquelles s'abonner.", "import.listSubHelp": "Listes auxquelles s'abonner.",
"import.mode": "Mode", "import.mode": "Mode",
"import.overwrite": "Écraser ?", "import.overwrite": "Écraser ?",
"import.overwriteHelp": "Remplacer le nom et les attributs des abonnés existants ?", "import.overwriteHelp": "Remplacer le nom et les attributs des abonnés existants ?",
"import.recordsCount": "{num} \/ {total} enregistrements", "import.recordsCount": "{num} / {total} enregistrements",
"import.stopImport": "Arrêter l'importation", "import.stopImport": "Arrêter l'importation",
"import.subscribe": "S'abonner", "import.subscribe": "S'abonner",
"import.title": "Importer des abonnés", "import.title": "Importer des abonnés",
"import.upload": "Télécharger", "import.upload": "Télécharger",
"lists.confirmDelete": "Êtes-vous sûr ? Cela ne supprime pas les abonnés.", "lists.confirmDelete": "Êtes-vous sûr ? Cela ne supprime pas les abonnés.",
"lists.confirmSub": "Confirmer les abonnements à {name}", "lists.confirmSub": "Confirmer les abonnements à {name}",
"lists.invalidName": "Nom incorrect", "lists.invalidName": "Nom incorrect",
"lists.newList": "Nouvelle liste", "lists.newList": "Nouvelle liste",
"lists.optin": "Abonnement", "lists.optin": "Abonnement",
"lists.optinHelp": "Opt-in double envoie un email à l'abonné demandant sa confirmation. Pour les listes opt-in double, les campagnes ne sont envoyées qu'aux abonnés s'étant confirmés.", "lists.optinHelp": "Opt-in double envoie un email à l'abonné demandant sa confirmation. Pour les listes opt-in double, les campagnes ne sont envoyées qu'aux abonnés s'étant confirmés.",
"lists.optinTo": "Activer {name}", "lists.optinTo": "Activer {name}",
"lists.optins.double": "Opt-in double", "lists.optins.double": "Opt-in double",
"lists.optins.single": "Opt-in simple", "lists.optins.single": "Opt-in simple",
"lists.sendCampaign": "Envoyer la campagne", "lists.sendCampaign": "Envoyer la campagne",
"lists.sendOptinCampaign": "Envoyer une campagne opt-in", "lists.sendOptinCampaign": "Envoyer une campagne opt-in",
"lists.type": "Type", "lists.type": "Type",
"lists.typeHelp": "Les listes publiques sont libres d'accès en abonnement et leurs noms sont visibles sur les pages publiques telles que la page de gestion des abonnements.", "lists.typeHelp": "Les listes publiques sont libres d'accès en abonnement et leurs noms sont visibles sur les pages publiques telles que la page de gestion des abonnements.",
"lists.types.private": "Privée", "lists.types.private": "Privée",
"lists.types.public": "Publique", "lists.types.public": "Publique",
"logs.title": "Journaux", "logs.title": "Journaux",
"media.errorReadingFile": "Erreur de lecture du fichier : {error}", "media.errorReadingFile": "Erreur de lecture du fichier : {error}",
"media.errorResizing": "Erreur de redimensionnement de l'image : {error}", "media.errorResizing": "Erreur de redimensionnement de l'image : {error}",
"media.errorSavingThumbnail": "Erreur lors de l'enregistrement de la miniature : {error}", "media.errorSavingThumbnail": "Erreur lors de l'enregistrement de la miniature : {error}",
"media.errorUploading": "Erreur lors du téléchargement du fichier : {error}", "media.errorUploading": "Erreur lors du téléchargement du fichier : {error}",
"media.invalidFile": "Fichier non valide : {error}", "media.invalidFile": "Fichier non valide : {error}",
"media.title": "Médias", "media.title": "Médias",
"media.unsupportedFileType": "Type de fichier non pris en charge ({type})", "media.unsupportedFileType": "Type de fichier non pris en charge ({type})",
"media.upload": "Télécharger", "media.upload": "Télécharger",
"media.uploadHelp": "Cliquez ou glissez-déposez ici une ou plusieurs images", "media.uploadHelp": "Cliquez ou glissez-déposez ici une ou plusieurs images",
"media.uploadImage": "Télécharger l'image", "media.uploadImage": "Télécharger l'image",
"menu.allCampaigns": "Toutes les campagnes", "menu.allCampaigns": "Toutes les campagnes",
"menu.allLists": "Toutes les listes", "menu.allLists": "Toutes les listes",
"menu.allSubscribers": "Tous les abonnés", "menu.allSubscribers": "Tous les abonnés",
"menu.dashboard": "Tableau de bord", "menu.dashboard": "Tableau de bord",
"menu.forms": "Formulaires", "menu.forms": "Formulaires",
"menu.import": "Importer", "menu.import": "Importer",
"menu.logs": "Journaux", "menu.logs": "Journaux",
"menu.media": "Médias", "menu.media": "Médias",
"menu.newCampaign": "Créer nouveau", "menu.newCampaign": "Créer nouveau",
"menu.settings": "Paramètres", "menu.settings": "Paramètres",
"public.campaignNotFound": "La liste de diffusion est introuvable.", "public.campaignNotFound": "La liste de diffusion est introuvable.",
"public.confirmOptinSubTitle": "Confirmer l'abonnement", "public.confirmOptinSubTitle": "Confirmer l'abonnement",
"public.confirmSub": "Confirmer l'abonnement", "public.confirmSub": "Confirmer l'abonnement",
"public.confirmSubInfo": "Vous avez été ajouté aux listes suivantes :", "public.confirmSubInfo": "Vous avez été ajouté aux listes suivantes :",
"public.confirmSubTitle": "Confirmer", "public.confirmSubTitle": "Confirmer",
"public.dataRemoved": "Vos abonnements et toutes les données associées ont été supprimés.", "public.dataRemoved": "Vos abonnements et toutes les données associées ont été supprimés.",
"public.dataRemovedTitle": "Données supprimées", "public.dataRemovedTitle": "Données supprimées",
"public.dataSent": "Vos données vous ont été envoyées par email.", "public.dataSent": "Vos données vous ont été envoyées par email.",
"public.dataSentTitle": "Données envoyées par email", "public.dataSentTitle": "Données envoyées par email",
"public.errorFetchingCampaign": "Erreur lors de la récupération de l'email.", "public.errorFetchingCampaign": "Erreur lors de la récupération de l'email.",
"public.errorFetchingEmail": "Message email introuvable", "public.errorFetchingEmail": "Message email introuvable",
"public.errorFetchingLists": "Erreur lors de la récupération des listes. Veuillez réessayer.", "public.errorFetchingLists": "Erreur lors de la récupération des listes. Veuillez réessayer.",
"public.errorProcessingRequest": "Erreur lors du traitement de la demande. Veuillez réessayer.", "public.errorProcessingRequest": "Erreur lors du traitement de la demande. Veuillez réessayer.",
"public.errorTitle": "Erreur", "public.errorTitle": "Erreur",
"public.invalidFeature": "Cette fonctionnalité n'est pas disponible.", "public.invalidFeature": "Cette fonctionnalité n'est pas disponible.",
"public.invalidLink": "Lien invalide", "public.invalidLink": "Lien invalide",
"public.noListsAvailable": "Aucune liste disponible pour vous abonner.", "public.noListsAvailable": "Aucune liste disponible pour vous abonner.",
"public.noListsSelected": "Aucune liste valide sélectionnée pour s'abonner.", "public.noListsSelected": "Aucune liste valide sélectionnée pour s'abonner.",
"public.noSubInfo": "Il n'y a pas d'abonnement à confirmer.", "public.noSubInfo": "Il n'y a pas d'abonnement à confirmer.",
"public.noSubTitle": "Aucun abonnement", "public.noSubTitle": "Aucun abonnement",
"public.notFoundTitle": "Pas trouvé", "public.notFoundTitle": "Pas trouvé",
"public.privacyConfirmWipe": "Êtes-vous sûr de vouloir supprimer définitivement toutes vos données d'abonnement ?", "public.privacyConfirmWipe": "Êtes-vous sûr de vouloir supprimer définitivement toutes vos données d'abonnement ?",
"public.privacyExport": "Exportez vos données", "public.privacyExport": "Exportez vos données",
"public.privacyExportHelp": "Une copie de vos données vous sera envoyée par email.", "public.privacyExportHelp": "Une copie de vos données vous sera envoyée par email.",
"public.privacyTitle": "Confidentialité et données", "public.privacyTitle": "Confidentialité et données",
"public.privacyWipe": "Effacez vos données", "public.privacyWipe": "Effacez vos données",
"public.privacyWipeHelp": "Supprimez définitivement tous vos abonnements et données associées de la base de données.", "public.privacyWipeHelp": "Supprimez définitivement tous vos abonnements et données associées de la base de données.",
"public.sub": "S'abonner", "public.sub": "S'abonner",
"public.subConfirmed": "Abonné avec succès.", "public.subConfirmed": "Abonné avec succès.",
"public.subConfirmedTitle": "Confirmé", "public.subConfirmedTitle": "Confirmé",
"public.subName": "Nom (facultatif)", "public.subName": "Nom (facultatif)",
"public.subNotFound": "Abonnement introuvable.", "public.subNotFound": "Abonnement introuvable.",
"public.subPrivateList": "Liste privée", "public.subPrivateList": "Liste privée",
"public.subTitle": "S'abonner", "public.subTitle": "S'abonner",
"public.unsub": "Se désabonner", "public.unsub": "Se désabonner",
"public.unsubFull": "Se désabonner aussi de tous futurs emails.", "public.unsubFull": "Se désabonner aussi de tous futurs emails.",
"public.unsubHelp": "Voulez-vous vous désabonner de cette liste de diffusion ?", "public.unsubHelp": "Voulez-vous vous désabonner de cette liste de diffusion ?",
"public.unsubTitle": "Se désabonner", "public.unsubTitle": "Se désabonner",
"public.unsubbedInfo": "Vous vous êtes désabonné avec succès.", "public.unsubbedInfo": "Vous vous êtes désabonné avec succès.",
"public.unsubbedTitle": "Désabonné", "public.unsubbedTitle": "Désabonné",
"public.unsubscribeTitle": "Se désabonner de la liste de diffusion", "public.unsubscribeTitle": "Se désabonner de la liste de diffusion",
"settings.duplicateMessengerName": "Nom de messagerie en double : {name}", "settings.duplicateMessengerName": "Nom de messagerie en double : {name}",
"settings.errorEncoding": "Erreur lors du codage des paramètres : {error}", "settings.errorEncoding": "Erreur lors du codage des paramètres : {error}",
"settings.errorNoSMTP": "Au moins un bloc SMTP doit être activé", "settings.errorNoSMTP": "Au moins un bloc SMTP doit être activé",
"settings.general.adminNotifEmails": "Emails de notification administrateur", "settings.general.adminNotifEmails": "Emails de notification administrateur",
"settings.general.adminNotifEmailsHelp": "Liste d'adresses email séparées par des virgules auxquelles les notifications administration telles que les mises à jour d'importation, la fin de la campagne, l'échec, etc. seront envoyées.", "settings.general.adminNotifEmailsHelp": "Liste d'adresses email séparées par des virgules auxquelles les notifications administration telles que les mises à jour d'importation, la fin de la campagne, l'échec, etc. seront envoyées.",
"settings.general.enablePublicSubPage": "Activer la page d'abonnement publique", "settings.general.enablePublicSubPage": "Activer la page d'abonnement publique",
"settings.general.enablePublicSubPageHelp": "Afficher une page d'abonnement publique avec toutes les listes publiques auxquelles les personnes peuvent s'abonner.", "settings.general.enablePublicSubPageHelp": "Afficher une page d'abonnement publique avec toutes les listes publiques auxquelles les personnes peuvent s'abonner.",
"settings.general.faviconURL": "URL du favicon", "settings.general.faviconURL": "URL du favicon",
"settings.general.faviconURLHelp": "(Facultatif) URL complète du favicon statique visible par l'utilisateur, comme sur la page de désabonnement.", "settings.general.faviconURLHelp": "(Facultatif) URL complète du favicon statique visible par l'utilisateur, comme sur la page de désabonnement.",
"settings.general.fromEmail": "Adresse email `Émetteur` par défaut", "settings.general.fromEmail": "Adresse email `Émetteur` par défaut",
"settings.general.fromEmailHelp": "Adresse email `Émetteur` visible par défaut dans les emails de campagne sortants. Ce paramètre est modifiable pour chaque campagne.", "settings.general.fromEmailHelp": "Adresse email `Émetteur` visible par défaut dans les emails de campagne sortants. Ce paramètre est modifiable pour chaque campagne.",
"settings.general.language": "Langue", "settings.general.language": "Langue",
"settings.general.logoURL": "URL du logo", "settings.general.logoURL": "URL du logo",
"settings.general.logoURLHelp": "(Facultatif) URL complète du logo statique visible par l'utilisateur, comme sur la page de désabonnement.", "settings.general.logoURLHelp": "(Facultatif) URL complète du logo statique visible par l'utilisateur, comme sur la page de désabonnement.",
"settings.general.name": "Général", "settings.general.name": "Général",
"settings.general.rootURL": "URL racine", "settings.general.rootURL": "URL racine",
"settings.general.rootURLHelp": "URL publique de l'installation (pas de barre oblique finale).", "settings.general.rootURLHelp": "URL publique de l'installation (pas de barre oblique finale).",
"settings.invalidMessengerName": "Nom de messagerie non valide.", "settings.invalidMessengerName": "Nom de messagerie non valide.",
"settings.media.provider": "Fournisseur", "settings.media.provider": "Fournisseur",
"settings.media.s3.bucket": "Bucket", "settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Chemin du bucket", "settings.media.s3.bucketPath": "Chemin du bucket",
"settings.media.s3.bucketPathHelp": "Chemin à l'intérieur du bucket pour télécharger les fichiers. La valeur par défaut est \/", "settings.media.s3.bucketPathHelp": "Chemin à l'intérieur du bucket pour télécharger les fichiers. La valeur par défaut est /",
"settings.media.s3.bucketType": "Type de bucket", "settings.media.s3.bucketType": "Type de bucket",
"settings.media.s3.bucketTypePrivate": "Privé", "settings.media.s3.bucketTypePrivate": "Privé",
"settings.media.s3.bucketTypePublic": "Publique", "settings.media.s3.bucketTypePublic": "Publique",
"settings.media.s3.key": "AWS access key", "settings.media.s3.key": "AWS access key",
"settings.media.s3.region": "Région", "settings.media.s3.region": "Région",
"settings.media.s3.secret": "AWS access secret", "settings.media.s3.secret": "AWS access secret",
"settings.media.s3.uploadExpiry": "Expiration du téléchargement", "settings.media.s3.uploadExpiry": "Expiration du téléchargement",
"settings.media.s3.uploadExpiryHelp": "(Facultatif) Spécifiez le TTL (en secondes) pour l'URL prédéfinie générée. Uniquement applicable pour les buckets privés (s, m, h, d pour les secondes, minutes, heures, jours).", "settings.media.s3.uploadExpiryHelp": "(Facultatif) Spécifiez le TTL (en secondes) pour l'URL prédéfinie générée. Uniquement applicable pour les buckets privés (s, m, h, d pour les secondes, minutes, heures, jours).",
"settings.media.title": "Téléchargements de médias", "settings.media.title": "Téléchargements de médias",
"settings.media.upload.path": "Chemin du téléchargement", "settings.media.upload.path": "Chemin du téléchargement",
"settings.media.upload.pathHelp": "Chemin vers le répertoire où les médias seront téléchargés.", "settings.media.upload.pathHelp": "Chemin vers le répertoire où les médias seront téléchargés.",
"settings.media.upload.uri": "URI de téléchargement", "settings.media.upload.uri": "URI de téléchargement",
"settings.media.upload.uriHelp": "URI de téléchargement qui sera visible du monde extérieur. Le média téléchargé dans le chemin du téléchargement sera accessible publiquement sous {root_url}, par exemple, https:\/\/listmonk.votresite.com\/uploads.", "settings.media.upload.uriHelp": "URI de téléchargement qui sera visible du monde extérieur. Le média téléchargé dans le chemin du téléchargement sera accessible publiquement sous {root_url}, par exemple, https://listmonk.votresite.com/uploads.",
"settings.messengers.maxConns": "Nb. connexions max.", "settings.messengers.maxConns": "Nb. connexions max.",
"settings.messengers.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur.", "settings.messengers.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur.",
"settings.messengers.messageDiscard": "Annuler les modifications ?", "settings.messengers.messageDiscard": "Annuler les modifications ?",
"settings.messengers.messageSaved": "Paramètres sauvegardés. Rechargement de l'application...", "settings.messengers.messageSaved": "Paramètres sauvegardés. Rechargement de l'application...",
"settings.messengers.name": "Messagers", "settings.messengers.name": "Messagers",
"settings.messengers.nameHelp": "Par exemple : my-sms. Alphanumérique \/ tiret.", "settings.messengers.nameHelp": "Par exemple : my-sms. Alphanumérique / tiret.",
"settings.messengers.password": "Mot de passe", "settings.messengers.password": "Mot de passe",
"settings.messengers.retries": "Tentatives", "settings.messengers.retries": "Tentatives",
"settings.messengers.retriesHelp": "Nombre de tentatives en cas d'échec d'un message.", "settings.messengers.retriesHelp": "Nombre de tentatives en cas d'échec d'un message.",
"settings.messengers.skipTLSHelp": "Ignorez la vérification du nom d'hôte sur le certificat TLS.", "settings.messengers.skipTLSHelp": "Ignorez la vérification du nom d'hôte sur le certificat TLS.",
"settings.messengers.timeout": "Délai d'inactivité", "settings.messengers.timeout": "Délai d'inactivité",
"settings.messengers.timeoutHelp": "Temps d'attente avant une nouvelle activité sur la connexion avant fermeture et suppression du pool (s pour seconde, m pour minute).", "settings.messengers.timeoutHelp": "Temps d'attente avant une nouvelle activité sur la connexion avant fermeture et suppression du pool (s pour seconde, m pour minute).",
"settings.messengers.url": "URL", "settings.messengers.url": "URL",
"settings.messengers.urlHelp": "URL racine du serveur Postback.", "settings.messengers.urlHelp": "URL racine du serveur Postback.",
"settings.messengers.username": "Nom d'utilisateur", "settings.messengers.username": "Nom d'utilisateur",
"settings.performance.batchSize": "Taille du lot", "settings.performance.batchSize": "Taille du lot",
"settings.performance.batchSizeHelp": "Le nombre d'abonnés à extraire de la base de données en une seule itération. Chaque itération extrait les abonnés de la base de données, leur envoie les messages, puis passe à l'itération suivante pour extraire le lot suivant. Idéalement cette valeur devrait être supérieure au débit maximum possible (Concurrence * Taux de message).", "settings.performance.batchSizeHelp": "Le nombre d'abonnés à extraire de la base de données en une seule itération. Chaque itération extrait les abonnés de la base de données, leur envoie les messages, puis passe à l'itération suivante pour extraire le lot suivant. Idéalement cette valeur devrait être supérieure au débit maximum possible (Concurrence * Taux de message).",
"settings.performance.concurrency": "Concurrence", "settings.performance.concurrency": "Concurrence",
"settings.performance.concurrencyHelp": "Nombre de worker (threads) concurrents maximum qui enverrons les messages simultanément.", "settings.performance.concurrencyHelp": "Nombre de worker (threads) concurrents maximum qui enverrons les messages simultanément.",
"settings.performance.maxErrThreshold": "Seuil maximum d'erreur", "settings.performance.maxErrThreshold": "Seuil maximum d'erreur",
"settings.performance.maxErrThresholdHelp": "Le nombre d'erreurs (par exemple : délais d'expiration SMTP lors de l'envoi d'emails) qu'une campagne en cours d'exécution doit tolérer avant d'être suspendue pour une vérification ou une intervention manuelle. Réglez sur 0 pour ne jamais mettre en pause.", "settings.performance.maxErrThresholdHelp": "Le nombre d'erreurs (par exemple : délais d'expiration SMTP lors de l'envoi d'emails) qu'une campagne en cours d'exécution doit tolérer avant d'être suspendue pour une vérification ou une intervention manuelle. Réglez sur 0 pour ne jamais mettre en pause.",
"settings.performance.messageRate": "Taux de message", "settings.performance.messageRate": "Taux de message",
"settings.performance.messageRateHelp": "Nombre maximum de messages à envoyer par worker en une seconde. Si concurrence = 10 et taux de message = 10, alors jusqu'à 10x10 = 100 messages peuvent être poussés chaque seconde. Ce paramètre ainsi que le paramètre concurrence devraient être modifié pour maintenir les messages sortants par seconde sous les limites de débit des serveurs de messages cibles, le cas échéant.", "settings.performance.messageRateHelp": "Nombre maximum de messages à envoyer par worker en une seconde. Si concurrence = 10 et taux de message = 10, alors jusqu'à 10x10 = 100 messages peuvent être poussés chaque seconde. Ce paramètre ainsi que le paramètre concurrence devraient être modifié pour maintenir les messages sortants par seconde sous les limites de débit des serveurs de messages cibles, le cas échéant.",
"settings.performance.name": "Performance", "settings.performance.name": "Performance",
"settings.performance.slidingWindow": "Activer une limite par fenêtre glissante", "settings.performance.slidingWindow": "Activer une limite par fenêtre glissante",
"settings.performance.slidingWindowDuration": "Durée", "settings.performance.slidingWindowDuration": "Durée",
"settings.performance.slidingWindowDurationHelp": "Durée de la période de la fenêtre glissante (m pour minute, h pour heure).", "settings.performance.slidingWindowDurationHelp": "Durée de la période de la fenêtre glissante (m pour minute, h pour heure).",
"settings.performance.slidingWindowHelp": "Limitez le nombre total de messages envoyés au cours d'une période donnée. Une fois cette limite atteinte, l'envoi des messages est suspendu jusqu'à ce que la fenêtre de temps soit passée.", "settings.performance.slidingWindowHelp": "Limitez le nombre total de messages envoyés au cours d'une période donnée. Une fois cette limite atteinte, l'envoi des messages est suspendu jusqu'à ce que la fenêtre de temps soit passée.",
"settings.performance.slidingWindowRate": "Nb. messages max.", "settings.performance.slidingWindowRate": "Nb. messages max.",
"settings.performance.slidingWindowRateHelp": "Nombre maximum de messages à envoyer pendant la durée de la fenêtre.", "settings.performance.slidingWindowRateHelp": "Nombre maximum de messages à envoyer pendant la durée de la fenêtre.",
"settings.privacy.allowBlocklist": "Autoriser la liste de blocage", "settings.privacy.allowBlocklist": "Autoriser la liste de blocage",
"settings.privacy.allowBlocklistHelp": "Autoriser les abonnés à se désabonner de toutes les listes de diffusion et à se marquer comme étant bloqués ?", "settings.privacy.allowBlocklistHelp": "Autoriser les abonnés à se désabonner de toutes les listes de diffusion et à se marquer comme étant bloqués ?",
"settings.privacy.allowExport": "Autoriser l'exportation", "settings.privacy.allowExport": "Autoriser l'exportation",
"settings.privacy.allowExportHelp": "Autoriser les abonnés à exporter les données collectées à leur sujet ?", "settings.privacy.allowExportHelp": "Autoriser les abonnés à exporter les données collectées à leur sujet ?",
"settings.privacy.allowWipe": "Autoriser l'effacement", "settings.privacy.allowWipe": "Autoriser l'effacement",
"settings.privacy.allowWipeHelp": "Autoriser les abonnés à supprimer leurs abonnements et toutes les autres données de la base de données. Les vues de campagne et les clics sur les liens sont également supprimés, tandis que le compteur global de vues et de nombre de clics restent inchangés (aucun abonné ne leur est associé) afin que les statistiques et les analyses ne soient pas affectées.", "settings.privacy.allowWipeHelp": "Autoriser les abonnés à supprimer leurs abonnements et toutes les autres données de la base de données. Les vues de campagne et les clics sur les liens sont également supprimés, tandis que le compteur global de vues et de nombre de clics restent inchangés (aucun abonné ne leur est associé) afin que les statistiques et les analyses ne soient pas affectées.",
"settings.privacy.individualSubTracking": "Suivi individuel des abonnés", "settings.privacy.individualSubTracking": "Suivi individuel des abonnés",
"settings.privacy.individualSubTrackingHelp": "Suivez les vues et les clics des campagnes par abonné. Lorsqu'elle est désactivée, la vue et le suivi des clics continuent sans être liés à des abonnés individuels.", "settings.privacy.individualSubTrackingHelp": "Suivez les vues et les clics des campagnes par abonné. Lorsqu'elle est désactivée, la vue et le suivi des clics continuent sans être liés à des abonnés individuels.",
"settings.privacy.listUnsubHeader": "Inclure l'en-tête `List-Unsubscribe`", "settings.privacy.listUnsubHeader": "Inclure l'en-tête `List-Unsubscribe`",
"settings.privacy.listUnsubHeaderHelp": "Incluez des en-têtes de désabonnement qui permettre aux utilisateurs de se désabonner en un seul clic depuis leur client de messagerie.", "settings.privacy.listUnsubHeaderHelp": "Incluez des en-têtes de désabonnement qui permettre aux utilisateurs de se désabonner en un seul clic depuis leur client de messagerie.",
"settings.privacy.name": "Vie privée", "settings.privacy.name": "Vie privée",
"settings.smtp.authProtocol": "Protocole d'authentification", "settings.smtp.authProtocol": "Protocole d'authentification",
"settings.smtp.customHeaders": "En-têtes personnalisés", "settings.smtp.customHeaders": "En-têtes personnalisés",
"settings.smtp.customHeadersHelp": "Tableau facultatif d'en-têtes des emails à inclure dans tous les messages envoyés depuis ce serveur. Par exemple : [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]", "settings.smtp.customHeadersHelp": "Tableau facultatif d'en-têtes des emails à inclure dans tous les messages envoyés depuis ce serveur. Par exemple : [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
"settings.smtp.enabled": "Activée", "settings.smtp.enabled": "Activée",
"settings.smtp.heloHost": "Nom d'hôte HELO", "settings.smtp.heloHost": "Nom d'hôte HELO",
"settings.smtp.heloHostHelp": "Facultatif. Certains serveurs SMTP nécessitent un nom de domaine complet dans le nom d'hôte. Par défaut, HELLOs vient avec `localhost`. Définissez ce paramètre si un nom d'hôte personnalisé doit être utilisé.", "settings.smtp.heloHostHelp": "Facultatif. Certains serveurs SMTP nécessitent un nom de domaine complet dans le nom d'hôte. Par défaut, HELLOs vient avec `localhost`. Définissez ce paramètre si un nom d'hôte personnalisé doit être utilisé.",
"settings.smtp.host": "Hôte", "settings.smtp.host": "Hôte",
"settings.smtp.hostHelp": "Adresse hôte du serveur SMTP.", "settings.smtp.hostHelp": "Adresse hôte du serveur SMTP.",
"settings.smtp.idleTimeout": "Délai d'inactivité", "settings.smtp.idleTimeout": "Délai d'inactivité",
"settings.smtp.idleTimeoutHelp": "Temps d'attente avant une nouvelle activité sur la connexion avant fermeture et suppression du pool (s pour seconde, m pour minute).", "settings.smtp.idleTimeoutHelp": "Temps d'attente avant une nouvelle activité sur la connexion avant fermeture et suppression du pool (s pour seconde, m pour minute).",
"settings.smtp.maxConns": "Nb. connexions max.", "settings.smtp.maxConns": "Nb. connexions max.",
"settings.smtp.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur SMTP.", "settings.smtp.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur SMTP.",
"settings.smtp.name": "SMTP", "settings.smtp.name": "SMTP",
"settings.smtp.password": "Mot de passe", "settings.smtp.password": "Mot de passe",
"settings.smtp.passwordHelp": "Entrée pour modifier", "settings.smtp.passwordHelp": "Entrée pour modifier",
"settings.smtp.port": "Port", "settings.smtp.port": "Port",
"settings.smtp.portHelp": "Port du serveur SMTP.", "settings.smtp.portHelp": "Port du serveur SMTP.",
"settings.smtp.retries": "Tentatives", "settings.smtp.retries": "Tentatives",
"settings.smtp.retriesHelp": "Nombre de tentatives en cas d'échec d'un message.", "settings.smtp.retriesHelp": "Nombre de tentatives en cas d'échec d'un message.",
"settings.smtp.setCustomHeaders": "Définir des en-têtes personnalisés", "settings.smtp.setCustomHeaders": "Définir des en-têtes personnalisés",
"settings.smtp.skipTLS": "Ignorer la vérification TLS", "settings.smtp.skipTLS": "Ignorer la vérification TLS",
"settings.smtp.skipTLSHelp": "Ignorez la vérification du nom d'hôte sur le certificat TLS.", "settings.smtp.skipTLSHelp": "Ignorez la vérification du nom d'hôte sur le certificat TLS.",
"settings.smtp.tls": "TLS", "settings.smtp.tls": "TLS",
"settings.smtp.tlsHelp": "Activez STARTTLS.", "settings.smtp.tlsHelp": "Activez STARTTLS.",
"settings.smtp.username": "Nom d'utilisateur", "settings.smtp.username": "Nom d'utilisateur",
"settings.smtp.waitTimeout": "Délai d'attente", "settings.smtp.waitTimeout": "Délai d'attente",
"settings.smtp.waitTimeoutHelp": "Temps d'attente pour une nouvelle activité sur une connexion avant de sa fermeture et sa suppression du pool (s pour seconde, m pour minute).", "settings.smtp.waitTimeoutHelp": "Temps d'attente pour une nouvelle activité sur une connexion avant de sa fermeture et sa suppression du pool (s pour seconde, m pour minute).",
"settings.title": "Paramètres", "settings.title": "Paramètres",
"subscribers.advancedQuery": "Avancées", "subscribers.advancedQuery": "Avancées",
"subscribers.advancedQueryHelp": "Expression SQL partielle pour interroger les attributs de l'abonné", "subscribers.advancedQueryHelp": "Expression SQL partielle pour interroger les attributs de l'abonné",
"subscribers.attribs": "Attributs", "subscribers.attribs": "Attributs",
"subscribers.attribsHelp": "Les attributs sont définis comme une map JSON, par exemple :", "subscribers.attribsHelp": "Les attributs sont définis comme une map JSON, par exemple :",
"subscribers.blocklistedHelp": "Les abonnés bloqués ne recevront jamais d'emails.", "subscribers.blocklistedHelp": "Les abonnés bloqués ne recevront jamais d'emails.",
"subscribers.confirmBlocklist": "Liste de blocage {num} abonné(s) ?", "subscribers.confirmBlocklist": "Liste de blocage {num} abonné(s) ?",
"subscribers.confirmDelete": "Supprimer {num} abonné(s) ?", "subscribers.confirmDelete": "Supprimer {num} abonné(s) ?",
"subscribers.confirmExport": "Exporter {num} abonné(s) ?", "subscribers.confirmExport": "Exporter {num} abonné(s) ?",
"subscribers.downloadData": "Télécharger les données", "subscribers.downloadData": "Télécharger les données",
"subscribers.email": "Email", "subscribers.email": "Email",
"subscribers.emailExists": "L'email existe déjà.", "subscribers.emailExists": "L'email existe déjà.",
"subscribers.errorBlocklisting": "Erreur lors du blocage des abonnés : {error}", "subscribers.errorBlocklisting": "Erreur lors du blocage des abonnés : {error}",
"subscribers.errorInvalidIDs": "Un ou plusieurs identifiants non valides fournis : {error}", "subscribers.errorInvalidIDs": "Un ou plusieurs identifiants non valides fournis : {error}",
"subscribers.errorNoIDs": "Aucune ID fournie.", "subscribers.errorNoIDs": "Aucune ID fournie.",
"subscribers.errorNoListsGiven": "Aucune liste donnée.", "subscribers.errorNoListsGiven": "Aucune liste donnée.",
"subscribers.errorPreparingQuery": "Erreur lors de la préparation de la requête d'abonné : {error}", "subscribers.errorPreparingQuery": "Erreur lors de la préparation de la requête d'abonné : {error}",
"subscribers.errorSendingOptin": "Erreur lors de l'envoi de l'email opt-in.", "subscribers.errorSendingOptin": "Erreur lors de l'envoi de l'email opt-in.",
"subscribers.export": "Exportation", "subscribers.export": "Exportation",
"subscribers.invalidAction": "Action non valide.", "subscribers.invalidAction": "Action non valide.",
"subscribers.invalidEmail": "Email invalide.", "subscribers.invalidEmail": "Email invalide.",
"subscribers.invalidJSON": "JSON non valide dans les attributs.", "subscribers.invalidJSON": "JSON non valide dans les attributs.",
"subscribers.invalidName": "Nom incorrect.", "subscribers.invalidName": "Nom incorrect.",
"subscribers.listChangeApplied": "Modification de la liste effectuée.", "subscribers.listChangeApplied": "Modification de la liste effectuée.",
"subscribers.lists": "Listes", "subscribers.lists": "Listes",
"subscribers.listsHelp": "Les listes dont les abonnés se sont désabonnés ne peuvent pas être supprimées.", "subscribers.listsHelp": "Les listes dont les abonnés se sont désabonnés ne peuvent pas être supprimées.",
"subscribers.listsPlaceholder": "Listes auxquelles s'abonner", "subscribers.listsPlaceholder": "Listes auxquelles s'abonner",
"subscribers.manageLists": "Gérer les listes", "subscribers.manageLists": "Gérer les listes",
"subscribers.markUnsubscribed": "Marquer comme désabonné", "subscribers.markUnsubscribed": "Marquer comme désabonné",
"subscribers.newSubscriber": "Nouvel abonné", "subscribers.newSubscriber": "Nouvel abonné",
"subscribers.numSelected": "{num} abonné(s) sélectionné(s)", "subscribers.numSelected": "{num} abonné(s) sélectionné(s)",
"subscribers.optinSubject": "Confirmer l'abonnement", "subscribers.optinSubject": "Confirmer l'abonnement",
"subscribers.query": "Requête", "subscribers.query": "Requête",
"subscribers.queryPlaceholder": "Email ou nom", "subscribers.queryPlaceholder": "Email ou nom",
"subscribers.reset": "Réinitialiser", "subscribers.reset": "Réinitialiser",
"subscribers.selectAll": "Sélectionner tout {num}", "subscribers.selectAll": "Sélectionner tout {num}",
"subscribers.status.blocklisted": "Liste bloquée", "subscribers.status.blocklisted": "Liste bloquée",
"subscribers.status.confirmed": "Confirmé", "subscribers.status.confirmed": "Confirmé",
"subscribers.status.enabled": "Activée", "subscribers.status.enabled": "Activée",
"subscribers.status.subscribed": "Abonné", "subscribers.status.subscribed": "Abonné",
"subscribers.status.unconfirmed": "Non confirmé", "subscribers.status.unconfirmed": "Non confirmé",
"subscribers.status.unsubscribed": "Désabonné", "subscribers.status.unsubscribed": "Désabonné",
"subscribers.subscribersDeleted": "{num} abonné(s) supprimé(s)", "subscribers.subscribersDeleted": "{num} abonné(s) supprimé(s)",
"templates.cantDeleteDefault": "Impossible de supprimer le modèle par défaut", "templates.cantDeleteDefault": "Impossible de supprimer le modèle par défaut",
"templates.default": "Défaut", "templates.default": "Défaut",
"templates.dummyName": "Campagne de test", "templates.dummyName": "Campagne de test",
"templates.dummySubject": "Objet de la campagne de test", "templates.dummySubject": "Objet de la campagne de test",
"templates.errorCompiling": "Erreur lors de la compilation du modèle : {error}", "templates.errorCompiling": "Erreur lors de la compilation du modèle : {error}",
"templates.errorRendering": "Message d'erreur lors du rendu : {error}", "templates.errorRendering": "Message d'erreur lors du rendu : {error}",
"templates.fieldInvalidName": "Longueur du nom non valide.", "templates.fieldInvalidName": "Longueur du nom non valide.",
"templates.makeDefault": "Définir par défaut", "templates.makeDefault": "Définir par défaut",
"templates.newTemplate": "Nouveau modèle", "templates.newTemplate": "Nouveau modèle",
"templates.placeholderHelp": "L'espace réservé {placeholder} doit apparaître exactement une fois dans le modèle.", "templates.placeholderHelp": "L'espace réservé {placeholder} doit apparaître exactement une fois dans le modèle.",
"templates.preview": "Aperçu", "templates.preview": "Aperçu",
"templates.rawHTML": "HTML brut" "templates.rawHTML": "HTML brut"
} }

View File

@ -1,8 +1,13 @@
-- subscribers -- subscribers
-- name: get-subscriber -- name: get-subscriber
-- Get a single subscriber by id or UUID. -- Get a single subscriber by id or UUID or email.
SELECT * FROM subscribers WHERE CASE WHEN $1 > 0 THEN id = $1 ELSE uuid = $2 END; SELECT * FROM subscribers WHERE
CASE
WHEN $1 > 0 THEN id = $1
WHEN $2 != '' THEN uuid = $2::UUID
WHEN $3 != '' THEN email = $3
END;
-- name: subscriber-exists -- name: subscriber-exists
-- Check if a subscriber exists by id or UUID. -- Check if a subscriber exists by id or UUID.