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.
var sub models.Subscriber
if err := app.queries.GetSubscriber.Get(&sub, 0, subUUID); err != nil {
sub, err := getSubscriber(0, subUUID, "", app)
if err != nil {
if err == sql.ErrNoRows {
return c.Render(http.StatusNotFound, tplMessage,
makeMsgTpl(app.i18n.T("public.notFoundTitle"), "",
app.i18n.T("public.errorFetchingEmail")))
}
app.log.Printf("error fetching campaign subscriber: %v", err)
return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "",
app.i18n.Ts("public.errorFetchingCampaign")))
@ -324,14 +323,18 @@ func handleSubscriptionForm(c echo.Context) error {
// Insert the subscriber into the DB.
req.Status = models.SubscriberStatusEnabled
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,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", fmt.Sprintf("%s", err.(*echo.HTTPError).Message)))
}
return c.Render(http.StatusOK, tplMessage,
makeMsgTpl(app.i18n.T("public.subTitle"), "",
app.i18n.Ts("public.subConfirmed")))
msg := "public.subConfirmed"
if hasOptin {
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

View File

@ -79,7 +79,11 @@ func handleGetSubscriber(c echo.Context) error {
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 {
return err
}
@ -273,14 +277,13 @@ func handleCreateSubscriber(c echo.Context) error {
}
// Insert the subscriber into the DB.
sub, err := insertSubscriber(req, app)
sub, isNew, _, err := insertSubscriber(req, app)
if err != nil {
if err == errSubscriberExists {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.emailExists"))
}
return err
}
if !isNew {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.emailExists"))
}
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).
sub, err := getSubscriber(int(id), app)
sub, err := getSubscriber(int(id), "", "", app)
if err != nil {
return err
}
_ = sendOptinConfirmation(sub, []int64(req.Lists), app)
_, _ = sendOptinConfirmation(sub, []int64(req.Lists), app)
return c.JSON(http.StatusOK, okResp{sub})
}
@ -335,7 +338,6 @@ func handleSubscriberSendOptin(c echo.Context) error {
var (
app = c.Get("app").(*App)
id, _ = strconv.Atoi(c.Param("id"))
out models.Subscribers
)
if id < 1 {
@ -343,19 +345,15 @@ func handleSubscriberSendOptin(c echo.Context) error {
}
// Fetch the subscriber.
err := app.queries.GetSubscriber.Select(&out, id, nil)
out, err := getSubscriber(id, "", "", app)
if err != nil {
app.log.Printf("error fetching subscriber: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
"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,
app.i18n.T("subscribers.errorSendingOptin"))
}
@ -619,56 +617,53 @@ func handleExportSubscriberData(c echo.Context) error {
return c.Blob(http.StatusOK, "application/json", b)
}
// insertSubscriber inserts a subscriber and returns the ID.
func insertSubscriber(req subimporter.SubReq, app *App) (models.Subscriber, error) {
// insertSubscriber inserts a subscriber and returns the ID. The first bool indicates if
// 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()
if err != nil {
return req.Subscriber, err
return req.Subscriber, false, false, err
}
req.UUID = uu.String()
err = app.queries.InsertSubscriber.Get(&req.ID,
isNew := true
if err = app.queries.InsertSubscriber.Get(&req.ID,
req.UUID,
req.Email,
strings.TrimSpace(req.Name),
req.Status,
req.Attribs,
req.Lists,
req.ListUUIDs)
if err != nil {
req.ListUUIDs); err != nil {
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.
sub, err := getSubscriber(req.ID, app)
// Fetch the subscriber's full data. If the subscriber already existed and wasn't
// 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 {
return sub, err
return sub, false, false, err
}
// Send a confirmation e-mail (if there are any double opt-in lists).
_ = sendOptinConfirmation(sub, []int64(req.Lists), app)
return sub, nil
num, _ := sendOptinConfirmation(sub, []int64(req.Lists), app)
return sub, isNew, num > 0, nil
}
// getSubscriber gets a single subscriber by ID.
func getSubscriber(id int, app *App) (models.Subscriber, error) {
var (
out models.Subscribers
)
// getSubscriber gets a single subscriber by ID, uuid, or e-mail in that order.
// Only one of these params should have a value.
func getSubscriber(id int, uuid, email string, app *App) (models.Subscriber, error) {
var out models.Subscribers
if id < 1 {
return models.Subscriber{},
echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
if err := app.queries.GetSubscriber.Select(&out, id, nil); err != nil {
if err := app.queries.GetSubscriber.Select(&out, id, uuid, email); err != nil {
app.log.Printf("error fetching subscriber: %v", err)
return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
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
// if at least one of the given listIDs is set to optin=double
func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) error {
// if at least one of the given listIDs is set to optin=double. It returns the number of
// opt-in lists that were found.
func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) (int, error) {
var lists []models.List
// 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,
pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil {
app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
return err
return 0, err
}
// None.
if len(lists) == 0 {
return nil
return 0, nil
}
var (
@ -764,9 +760,9 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err
if err := app.sendNotification([]string{sub.Email},
app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil {
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

View File

@ -5,7 +5,7 @@
<b-tag v-for="l in selectedItems"
:key="l.id"
:class="l.subscriptionStatus"
:closable="!disabled && l.subscriptionStatus !== 'unsubscribed'"
:closable="true"
:data-id="l.id"
@close="removeList(l.id)">
{{ 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.sub": "Subscribe",
"public.subConfirmed": "Subscribed successfully.",
"public.subOptinPending": "An e-mail has been sent to you to confirm your subscription(s).",
"public.subConfirmedTitle": "Confirmed",
"public.subName": "Name (optional)",
"public.subNotFound": "Subscription not found.",

View File

@ -1,432 +1,432 @@
{
"_.code": "fr",
"_.name": "Français (fr)",
"admin.errorMarshallingConfig": "Erreur lors de la lecture de la configuration : {error}",
"campaigns.addAltText": "Ajouter un message de replacement en texte brut",
"campaigns.cantUpdate": "Impossible de mettre à jour une campagne en cours ou terminée.",
"campaigns.clicks": "Clics",
"campaigns.confirmDelete": "Supprimer {name}",
"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.content": "Contenu",
"campaigns.contentHelp": "Contenu ici",
"campaigns.continue": "Continuer",
"campaigns.copyOf": "Copie de {name}",
"campaigns.dateAndTime": "Date et heure",
"campaigns.ended": "Terminé",
"campaigns.errorSendTest": "Erreur lors de l'envoi test : {error}",
"campaigns.fieldInvalidBody": "Erreur lors de la compilation du corps de la campagne : {error}",
"campaigns.fieldInvalidFromEmail": "`Émetteur` non valide.",
"campaigns.fieldInvalidListIDs": "ID de liste non valides.",
"campaigns.fieldInvalidMessenger": "Messager inconnu {name}.",
"campaigns.fieldInvalidName": "Longueur du nom non valide.",
"campaigns.fieldInvalidSendAt": "La date planifiée doit être future.",
"campaigns.fieldInvalidSubject": "Longueur d'objet non valide.",
"campaigns.fromAddress": "Émetteur",
"campaigns.fromAddressPlaceholder": "Votre nom <noreply@votresite.com>",
"campaigns.invalid": "Campagne non valide",
"campaigns.needsSendAt": "Une date est nécessaire pour planifier la campagne.",
"campaigns.newCampaign": "Nouvelle campagne",
"campaigns.noKnownSubsToTest": "Aucun abonné connu à tester.",
"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.noSubsToTest": "Il n'y a aucun abonné à cibler.",
"campaigns.notFound": "Campagne introuvable.",
"campaigns.onlyActiveCancel": "Seules les campagnes actives peuvent être annulées.",
"campaigns.onlyActivePause": "Seules les campagnes actives peuvent être mises en pause.",
"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.onlyScheduledAsDraft": "Seules les campagnes planifiées peuvent être enregistrées en tant que brouillons.",
"campaigns.pause": "Pause",
"campaigns.plainText": "Texte brut",
"campaigns.preview": "Aperçu",
"campaigns.progress": "Avancement",
"campaigns.queryPlaceholder": "Nom ou objet",
"campaigns.rawHTML": "HTML brut",
"campaigns.removeAltText": "Supprimer le message de replacement en texte brut",
"campaigns.richText": "Texte riche",
"campaigns.schedule": "Planifier la campagne",
"campaigns.scheduled": "Planifiée",
"campaigns.send": "Envoyer",
"campaigns.sendLater": "Envoyer plus tard",
"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.sendToLists": "Listes à envoyer à",
"campaigns.sent": "Envoyé",
"campaigns.start": "Lancer la campagne",
"campaigns.started": "\"{name}\" a commencé",
"campaigns.startedAt": "Commencé",
"campaigns.stats": "Statistiques",
"campaigns.status.cancelled": "Annulé",
"campaigns.status.draft": "Brouillon",
"campaigns.status.finished": "Terminé",
"campaigns.status.paused": "En pause",
"campaigns.status.running": "En cours",
"campaigns.status.scheduled": "Planifiée",
"campaigns.statusChanged": "\"{name}\" est {status}",
"campaigns.subject": "Objet",
"campaigns.testEmails": "Emails de test",
"campaigns.testSent": "Message de test envoyé",
"campaigns.timestamps": "Horodatages",
"campaigns.views": "Vues",
"dashboard.campaignViews": "Vues de la campagne",
"dashboard.linkClicks": "Clics sur les liens",
"dashboard.messagesSent": "Messages envoyés",
"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.title": "Vos données",
"email.optin.confirmSub": "Confirmer l'abonnement",
"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.confirmSubTitle": "Confirmer l'abonnement",
"email.optin.confirmSubWelcome": "Bonjour,",
"email.optin.privateList": "Liste privée",
"email.status.campaignReason": "Raison",
"email.status.campaignSent": "Envoyé",
"email.status.campaignUpdateTitle": "Mise à jour de la campagne",
"email.status.importFile": "Fichier",
"email.status.importRecords": "Enregistrements",
"email.status.importTitle": "Importer la mise à jour",
"email.status.status": "Statut",
"email.unsub": "Se désabonner",
"email.unsubHelp": "Vous ne souhaitez pas recevoir ces emails ?",
"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.noPublicLists": "Il n'y a pas de listes publiques pour générer un formulaire.",
"forms.publicLists": "Listes publiques",
"forms.publicSubPage": "Page d'abonnement publique",
"forms.selectHelp": "Sélectionnez les listes à ajouter au formulaire.",
"forms.title": "Formulaires",
"globals.buttons.add": "Ajouter",
"globals.buttons.addNew": "Ajouter nouveau",
"globals.buttons.cancel": "Annuler",
"globals.buttons.clone": "Cloner",
"globals.buttons.close": "Fermer",
"globals.buttons.continue": "Continuer",
"globals.buttons.delete": "Supprimer",
"globals.buttons.edit": "Éditer",
"globals.buttons.enabled": "Activée",
"globals.buttons.learnMore": "En savoir plus",
"globals.buttons.new": "Nouveau",
"globals.buttons.ok": "Ok",
"globals.buttons.remove": "Supprimer",
"globals.buttons.save": "Enregistrer",
"globals.buttons.saveChanges": "Enregistrer les changements",
"globals.days.1": "lun",
"globals.days.2": "mar",
"globals.days.3": "mer",
"globals.days.4": "jeu",
"globals.days.5": "ven",
"globals.days.6": "sam",
"globals.days.7": "dim",
"globals.fields.createdAt": "Créé le",
"globals.fields.id": "ID",
"globals.fields.name": "Nom",
"globals.fields.status": "Statut",
"globals.fields.type": "Type",
"globals.fields.updatedAt": "Mis à jour le",
"globals.fields.uuid": "UUID",
"globals.messages.confirm": "Êtes-vous sûr ?",
"globals.messages.created": "\"{name}\" créé",
"globals.messages.deleted": "\"{name}\" supprimé",
"globals.messages.emptyState": "Rien",
"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.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.errorUpdating": "Erreur lors de la mise à jour de {name}: {error}",
"globals.messages.invalidID": "ID non valide",
"globals.messages.invalidUUID": "UUID non valide",
"globals.messages.notFound": "{name} introuvable",
"globals.messages.passwordChange": "Entrez une valeur à modifier",
"globals.messages.updated": "\"{name}\" mis à jour",
"globals.months.1": "jan",
"globals.months.10": "oct",
"globals.months.11": "nov",
"globals.months.12": "déc",
"globals.months.2": "fév",
"globals.months.3": "mars",
"globals.months.4": "avr",
"globals.months.5": "mai",
"globals.months.6": "juin",
"globals.months.7": "juil",
"globals.months.8": "août",
"globals.months.9": "sept",
"globals.terms.campaign": "Campagne | Campagnes",
"globals.terms.campaigns": "Campagnes",
"globals.terms.dashboard": "Tableau de bord",
"globals.terms.list": "Liste | Listes",
"globals.terms.lists": "Listes",
"globals.terms.media": "Médias | Médias",
"globals.terms.messenger": "Messenger | Messagers",
"globals.terms.messengers": "Messagers",
"globals.terms.settings": "Paramètres",
"globals.terms.subscriber": "Abonné | Abonnés",
"globals.terms.subscribers": "Abonnés",
"globals.terms.tag": "Étiquette | Étiquettes",
"globals.terms.tags": "Étiquettes",
"globals.terms.template": "Modèle | 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.blocklist": "Liste des adresses bloquées",
"import.csvDelim": "Délimiteur CSV",
"import.csvDelimHelp": "Le délimiteur par défaut est la virgule.",
"import.csvExample": "Exemple CSV brut",
"import.csvFile": "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.errorProcessingZIP": "Erreur lors du traitement du fichier ZIP : {error}",
"import.errorStarting": "Erreur lors du démarrage de l'importation : {error}",
"import.importDone": "Terminé",
"import.importStarted": "L'importation a commencé",
"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.invalidDelim": "Le délimiteur doit être un seul caractère.",
"import.invalidFile": "Fichier non valide : {error}",
"import.invalidMode": "Mode invalide",
"import.invalidParams": "Paramètres non valides : {error}",
"import.listSubHelp": "Listes auxquelles s'abonner.",
"import.mode": "Mode",
"import.overwrite": "Écraser ?",
"import.overwriteHelp": "Remplacer le nom et les attributs des abonnés existants ?",
"import.recordsCount": "{num} \/ {total} enregistrements",
"import.stopImport": "Arrêter l'importation",
"import.subscribe": "S'abonner",
"import.title": "Importer des abonnés",
"import.upload": "Télécharger",
"lists.confirmDelete": "Êtes-vous sûr ? Cela ne supprime pas les abonnés.",
"lists.confirmSub": "Confirmer les abonnements à {name}",
"lists.invalidName": "Nom incorrect",
"lists.newList": "Nouvelle liste",
"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.optinTo": "Activer {name}",
"lists.optins.double": "Opt-in double",
"lists.optins.single": "Opt-in simple",
"lists.sendCampaign": "Envoyer la campagne",
"lists.sendOptinCampaign": "Envoyer une campagne opt-in",
"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.types.private": "Privée",
"lists.types.public": "Publique",
"logs.title": "Journaux",
"media.errorReadingFile": "Erreur de lecture du fichier : {error}",
"media.errorResizing": "Erreur de redimensionnement de l'image : {error}",
"media.errorSavingThumbnail": "Erreur lors de l'enregistrement de la miniature : {error}",
"media.errorUploading": "Erreur lors du téléchargement du fichier : {error}",
"media.invalidFile": "Fichier non valide : {error}",
"media.title": "Médias",
"media.unsupportedFileType": "Type de fichier non pris en charge ({type})",
"media.upload": "Télécharger",
"media.uploadHelp": "Cliquez ou glissez-déposez ici une ou plusieurs images",
"media.uploadImage": "Télécharger l'image",
"menu.allCampaigns": "Toutes les campagnes",
"menu.allLists": "Toutes les listes",
"menu.allSubscribers": "Tous les abonnés",
"menu.dashboard": "Tableau de bord",
"menu.forms": "Formulaires",
"menu.import": "Importer",
"menu.logs": "Journaux",
"menu.media": "Médias",
"menu.newCampaign": "Créer nouveau",
"menu.settings": "Paramètres",
"public.campaignNotFound": "La liste de diffusion est introuvable.",
"public.confirmOptinSubTitle": "Confirmer l'abonnement",
"public.confirmSub": "Confirmer l'abonnement",
"public.confirmSubInfo": "Vous avez été ajouté aux listes suivantes :",
"public.confirmSubTitle": "Confirmer",
"public.dataRemoved": "Vos abonnements et toutes les données associées ont été supprimés.",
"public.dataRemovedTitle": "Données supprimées",
"public.dataSent": "Vos données vous ont été 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.errorFetchingEmail": "Message email introuvable",
"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.errorTitle": "Erreur",
"public.invalidFeature": "Cette fonctionnalité n'est pas disponible.",
"public.invalidLink": "Lien invalide",
"public.noListsAvailable": "Aucune liste disponible pour vous abonner.",
"public.noListsSelected": "Aucune liste valide sélectionnée pour s'abonner.",
"public.noSubInfo": "Il n'y a pas d'abonnement à confirmer.",
"public.noSubTitle": "Aucun abonnement",
"public.notFoundTitle": "Pas trouvé",
"public.privacyConfirmWipe": "Êtes-vous sûr de vouloir supprimer définitivement toutes vos données d'abonnement ?",
"public.privacyExport": "Exportez vos données",
"public.privacyExportHelp": "Une copie de vos données vous sera envoyée par email.",
"public.privacyTitle": "Confidentialité et 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.sub": "S'abonner",
"public.subConfirmed": "Abonné avec succès.",
"public.subConfirmedTitle": "Confirmé",
"public.subName": "Nom (facultatif)",
"public.subNotFound": "Abonnement introuvable.",
"public.subPrivateList": "Liste privée",
"public.subTitle": "S'abonner",
"public.unsub": "Se désabonner",
"public.unsubFull": "Se désabonner aussi de tous futurs emails.",
"public.unsubHelp": "Voulez-vous vous désabonner de cette liste de diffusion ?",
"public.unsubTitle": "Se désabonner",
"public.unsubbedInfo": "Vous vous êtes désabonné avec succès.",
"public.unsubbedTitle": "Désabonné",
"public.unsubscribeTitle": "Se désabonner de la liste de diffusion",
"settings.duplicateMessengerName": "Nom de messagerie en double : {name}",
"settings.errorEncoding": "Erreur lors du codage des paramètres : {error}",
"settings.errorNoSMTP": "Au moins un bloc SMTP doit être activé",
"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.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.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.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.language": "Langue",
"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.name": "Général",
"settings.general.rootURL": "URL racine",
"settings.general.rootURLHelp": "URL publique de l'installation (pas de barre oblique finale).",
"settings.invalidMessengerName": "Nom de messagerie non valide.",
"settings.media.provider": "Fournisseur",
"settings.media.s3.bucket": "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.bucketType": "Type de bucket",
"settings.media.s3.bucketTypePrivate": "Privé",
"settings.media.s3.bucketTypePublic": "Publique",
"settings.media.s3.key": "AWS access key",
"settings.media.s3.region": "Région",
"settings.media.s3.secret": "AWS access secret",
"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.title": "Téléchargements de médias",
"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.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.messengers.maxConns": "Nb. connexions max.",
"settings.messengers.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur.",
"settings.messengers.messageDiscard": "Annuler les modifications ?",
"settings.messengers.messageSaved": "Paramètres sauvegardés. Rechargement de l'application...",
"settings.messengers.name": "Messagers",
"settings.messengers.nameHelp": "Par exemple : my-sms. Alphanumérique \/ tiret.",
"settings.messengers.password": "Mot de passe",
"settings.messengers.retries": "Tentatives",
"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.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.url": "URL",
"settings.messengers.urlHelp": "URL racine du serveur Postback.",
"settings.messengers.username": "Nom d'utilisateur",
"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.concurrency": "Concurrence",
"settings.performance.concurrencyHelp": "Nombre de worker (threads) concurrents maximum qui enverrons les messages simultanément.",
"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.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.name": "Performance",
"settings.performance.slidingWindow": "Activer une limite par fenêtre glissante",
"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.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.slidingWindowRateHelp": "Nombre maximum de messages à envoyer pendant la durée de la fenêtre.",
"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.allowExport": "Autoriser l'exportation",
"settings.privacy.allowExportHelp": "Autoriser les abonnés à exporter les données collectées à leur sujet ?",
"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.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.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.name": "Vie privée",
"settings.smtp.authProtocol": "Protocole d'authentification",
"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.enabled": "Activée",
"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.host": "Hôte",
"settings.smtp.hostHelp": "Adresse hôte du serveur SMTP.",
"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.maxConns": "Nb. connexions max.",
"settings.smtp.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur SMTP.",
"settings.smtp.name": "SMTP",
"settings.smtp.password": "Mot de passe",
"settings.smtp.passwordHelp": "Entrée pour modifier",
"settings.smtp.port": "Port",
"settings.smtp.portHelp": "Port du serveur SMTP.",
"settings.smtp.retries": "Tentatives",
"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.skipTLS": "Ignorer la vérification TLS",
"settings.smtp.skipTLSHelp": "Ignorez la vérification du nom d'hôte sur le certificat TLS.",
"settings.smtp.tls": "TLS",
"settings.smtp.tlsHelp": "Activez STARTTLS.",
"settings.smtp.username": "Nom d'utilisateur",
"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.title": "Paramètres",
"subscribers.advancedQuery": "Avancées",
"subscribers.advancedQueryHelp": "Expression SQL partielle pour interroger les attributs de l'abonné",
"subscribers.attribs": "Attributs",
"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.confirmBlocklist": "Liste de blocage {num} abonné(s) ?",
"subscribers.confirmDelete": "Supprimer {num} abonné(s) ?",
"subscribers.confirmExport": "Exporter {num} abonné(s) ?",
"subscribers.downloadData": "Télécharger les données",
"subscribers.email": "Email",
"subscribers.emailExists": "L'email existe déjà.",
"subscribers.errorBlocklisting": "Erreur lors du blocage des abonnés : {error}",
"subscribers.errorInvalidIDs": "Un ou plusieurs identifiants non valides fournis : {error}",
"subscribers.errorNoIDs": "Aucune ID fournie.",
"subscribers.errorNoListsGiven": "Aucune liste donnée.",
"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.export": "Exportation",
"subscribers.invalidAction": "Action non valide.",
"subscribers.invalidEmail": "Email invalide.",
"subscribers.invalidJSON": "JSON non valide dans les attributs.",
"subscribers.invalidName": "Nom incorrect.",
"subscribers.listChangeApplied": "Modification de la liste effectuée.",
"subscribers.lists": "Listes",
"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.manageLists": "Gérer les listes",
"subscribers.markUnsubscribed": "Marquer comme désabonné",
"subscribers.newSubscriber": "Nouvel abonné",
"subscribers.numSelected": "{num} abonné(s) sélectionné(s)",
"subscribers.optinSubject": "Confirmer l'abonnement",
"subscribers.query": "Requête",
"subscribers.queryPlaceholder": "Email ou nom",
"subscribers.reset": "Réinitialiser",
"subscribers.selectAll": "Sélectionner tout {num}",
"subscribers.status.blocklisted": "Liste bloquée",
"subscribers.status.confirmed": "Confirmé",
"subscribers.status.enabled": "Activée",
"subscribers.status.subscribed": "Abonné",
"subscribers.status.unconfirmed": "Non confirmé",
"subscribers.status.unsubscribed": "Désabonné",
"subscribers.subscribersDeleted": "{num} abonné(s) supprimé(s)",
"templates.cantDeleteDefault": "Impossible de supprimer le modèle par défaut",
"templates.default": "Défaut",
"templates.dummyName": "Campagne de test",
"templates.dummySubject": "Objet de la campagne de test",
"templates.errorCompiling": "Erreur lors de la compilation du modèle : {error}",
"templates.errorRendering": "Message d'erreur lors du rendu : {error}",
"templates.fieldInvalidName": "Longueur du nom non valide.",
"templates.makeDefault": "Définir par défaut",
"templates.newTemplate": "Nouveau modèle",
"templates.placeholderHelp": "L'espace réservé {placeholder} doit apparaître exactement une fois dans le modèle.",
"templates.preview": "Aperçu",
"templates.rawHTML": "HTML brut"
}
"_.code": "fr",
"_.name": "Français (fr)",
"admin.errorMarshallingConfig": "Erreur lors de la lecture de la configuration : {error}",
"campaigns.addAltText": "Ajouter un message de replacement en texte brut",
"campaigns.cantUpdate": "Impossible de mettre à jour une campagne en cours ou terminée.",
"campaigns.clicks": "Clics",
"campaigns.confirmDelete": "Supprimer {name}",
"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.content": "Contenu",
"campaigns.contentHelp": "Contenu ici",
"campaigns.continue": "Continuer",
"campaigns.copyOf": "Copie de {name}",
"campaigns.dateAndTime": "Date et heure",
"campaigns.ended": "Terminé",
"campaigns.errorSendTest": "Erreur lors de l'envoi test : {error}",
"campaigns.fieldInvalidBody": "Erreur lors de la compilation du corps de la campagne : {error}",
"campaigns.fieldInvalidFromEmail": "`Émetteur` non valide.",
"campaigns.fieldInvalidListIDs": "ID de liste non valides.",
"campaigns.fieldInvalidMessenger": "Messager inconnu {name}.",
"campaigns.fieldInvalidName": "Longueur du nom non valide.",
"campaigns.fieldInvalidSendAt": "La date planifiée doit être future.",
"campaigns.fieldInvalidSubject": "Longueur d'objet non valide.",
"campaigns.fromAddress": "Émetteur",
"campaigns.fromAddressPlaceholder": "Votre nom <noreply@votresite.com>",
"campaigns.invalid": "Campagne non valide",
"campaigns.needsSendAt": "Une date est nécessaire pour planifier la campagne.",
"campaigns.newCampaign": "Nouvelle campagne",
"campaigns.noKnownSubsToTest": "Aucun abonné connu à tester.",
"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.noSubsToTest": "Il n'y a aucun abonné à cibler.",
"campaigns.notFound": "Campagne introuvable.",
"campaigns.onlyActiveCancel": "Seules les campagnes actives peuvent être annulées.",
"campaigns.onlyActivePause": "Seules les campagnes actives peuvent être mises en pause.",
"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.onlyScheduledAsDraft": "Seules les campagnes planifiées peuvent être enregistrées en tant que brouillons.",
"campaigns.pause": "Pause",
"campaigns.plainText": "Texte brut",
"campaigns.preview": "Aperçu",
"campaigns.progress": "Avancement",
"campaigns.queryPlaceholder": "Nom ou objet",
"campaigns.rawHTML": "HTML brut",
"campaigns.removeAltText": "Supprimer le message de replacement en texte brut",
"campaigns.richText": "Texte riche",
"campaigns.schedule": "Planifier la campagne",
"campaigns.scheduled": "Planifiée",
"campaigns.send": "Envoyer",
"campaigns.sendLater": "Envoyer plus tard",
"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.sendToLists": "Listes à envoyer à",
"campaigns.sent": "Envoyé",
"campaigns.start": "Lancer la campagne",
"campaigns.started": "\"{name}\" a commencé",
"campaigns.startedAt": "Commencé",
"campaigns.stats": "Statistiques",
"campaigns.status.cancelled": "Annulé",
"campaigns.status.draft": "Brouillon",
"campaigns.status.finished": "Terminé",
"campaigns.status.paused": "En pause",
"campaigns.status.running": "En cours",
"campaigns.status.scheduled": "Planifiée",
"campaigns.statusChanged": "\"{name}\" est {status}",
"campaigns.subject": "Objet",
"campaigns.testEmails": "Emails de test",
"campaigns.testSent": "Message de test envoyé",
"campaigns.timestamps": "Horodatages",
"campaigns.views": "Vues",
"dashboard.campaignViews": "Vues de la campagne",
"dashboard.linkClicks": "Clics sur les liens",
"dashboard.messagesSent": "Messages envoyés",
"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.title": "Vos données",
"email.optin.confirmSub": "Confirmer l'abonnement",
"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.confirmSubTitle": "Confirmer l'abonnement",
"email.optin.confirmSubWelcome": "Bonjour,",
"email.optin.privateList": "Liste privée",
"email.status.campaignReason": "Raison",
"email.status.campaignSent": "Envoyé",
"email.status.campaignUpdateTitle": "Mise à jour de la campagne",
"email.status.importFile": "Fichier",
"email.status.importRecords": "Enregistrements",
"email.status.importTitle": "Importer la mise à jour",
"email.status.status": "Statut",
"email.unsub": "Se désabonner",
"email.unsubHelp": "Vous ne souhaitez pas recevoir ces emails ?",
"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.noPublicLists": "Il n'y a pas de listes publiques pour générer un formulaire.",
"forms.publicLists": "Listes publiques",
"forms.publicSubPage": "Page d'abonnement publique",
"forms.selectHelp": "Sélectionnez les listes à ajouter au formulaire.",
"forms.title": "Formulaires",
"globals.buttons.add": "Ajouter",
"globals.buttons.addNew": "Ajouter nouveau",
"globals.buttons.cancel": "Annuler",
"globals.buttons.clone": "Cloner",
"globals.buttons.close": "Fermer",
"globals.buttons.continue": "Continuer",
"globals.buttons.delete": "Supprimer",
"globals.buttons.edit": "Éditer",
"globals.buttons.enabled": "Activée",
"globals.buttons.learnMore": "En savoir plus",
"globals.buttons.new": "Nouveau",
"globals.buttons.ok": "Ok",
"globals.buttons.remove": "Supprimer",
"globals.buttons.save": "Enregistrer",
"globals.buttons.saveChanges": "Enregistrer les changements",
"globals.days.1": "lun",
"globals.days.2": "mar",
"globals.days.3": "mer",
"globals.days.4": "jeu",
"globals.days.5": "ven",
"globals.days.6": "sam",
"globals.days.7": "dim",
"globals.fields.createdAt": "Créé le",
"globals.fields.id": "ID",
"globals.fields.name": "Nom",
"globals.fields.status": "Statut",
"globals.fields.type": "Type",
"globals.fields.updatedAt": "Mis à jour le",
"globals.fields.uuid": "UUID",
"globals.messages.confirm": "Êtes-vous sûr ?",
"globals.messages.created": "\"{name}\" créé",
"globals.messages.deleted": "\"{name}\" supprimé",
"globals.messages.emptyState": "Rien",
"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.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.errorUpdating": "Erreur lors de la mise à jour de {name}: {error}",
"globals.messages.invalidID": "ID non valide",
"globals.messages.invalidUUID": "UUID non valide",
"globals.messages.notFound": "{name} introuvable",
"globals.messages.passwordChange": "Entrez une valeur à modifier",
"globals.messages.updated": "\"{name}\" mis à jour",
"globals.months.1": "jan",
"globals.months.10": "oct",
"globals.months.11": "nov",
"globals.months.12": "déc",
"globals.months.2": "fév",
"globals.months.3": "mars",
"globals.months.4": "avr",
"globals.months.5": "mai",
"globals.months.6": "juin",
"globals.months.7": "juil",
"globals.months.8": "août",
"globals.months.9": "sept",
"globals.terms.campaign": "Campagne | Campagnes",
"globals.terms.campaigns": "Campagnes",
"globals.terms.dashboard": "Tableau de bord",
"globals.terms.list": "Liste | Listes",
"globals.terms.lists": "Listes",
"globals.terms.media": "Médias | Médias",
"globals.terms.messenger": "Messenger | Messagers",
"globals.terms.messengers": "Messagers",
"globals.terms.settings": "Paramètres",
"globals.terms.subscriber": "Abonné | Abonnés",
"globals.terms.subscribers": "Abonnés",
"globals.terms.tag": "Étiquette | Étiquettes",
"globals.terms.tags": "Étiquettes",
"globals.terms.template": "Modèle | 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.blocklist": "Liste des adresses bloquées",
"import.csvDelim": "Délimiteur CSV",
"import.csvDelimHelp": "Le délimiteur par défaut est la virgule.",
"import.csvExample": "Exemple CSV brut",
"import.csvFile": "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.errorProcessingZIP": "Erreur lors du traitement du fichier ZIP : {error}",
"import.errorStarting": "Erreur lors du démarrage de l'importation : {error}",
"import.importDone": "Terminé",
"import.importStarted": "L'importation a commencé",
"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.invalidDelim": "Le délimiteur doit être un seul caractère.",
"import.invalidFile": "Fichier non valide : {error}",
"import.invalidMode": "Mode invalide",
"import.invalidParams": "Paramètres non valides : {error}",
"import.listSubHelp": "Listes auxquelles s'abonner.",
"import.mode": "Mode",
"import.overwrite": "Écraser ?",
"import.overwriteHelp": "Remplacer le nom et les attributs des abonnés existants ?",
"import.recordsCount": "{num} / {total} enregistrements",
"import.stopImport": "Arrêter l'importation",
"import.subscribe": "S'abonner",
"import.title": "Importer des abonnés",
"import.upload": "Télécharger",
"lists.confirmDelete": "Êtes-vous sûr ? Cela ne supprime pas les abonnés.",
"lists.confirmSub": "Confirmer les abonnements à {name}",
"lists.invalidName": "Nom incorrect",
"lists.newList": "Nouvelle liste",
"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.optinTo": "Activer {name}",
"lists.optins.double": "Opt-in double",
"lists.optins.single": "Opt-in simple",
"lists.sendCampaign": "Envoyer la campagne",
"lists.sendOptinCampaign": "Envoyer une campagne opt-in",
"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.types.private": "Privée",
"lists.types.public": "Publique",
"logs.title": "Journaux",
"media.errorReadingFile": "Erreur de lecture du fichier : {error}",
"media.errorResizing": "Erreur de redimensionnement de l'image : {error}",
"media.errorSavingThumbnail": "Erreur lors de l'enregistrement de la miniature : {error}",
"media.errorUploading": "Erreur lors du téléchargement du fichier : {error}",
"media.invalidFile": "Fichier non valide : {error}",
"media.title": "Médias",
"media.unsupportedFileType": "Type de fichier non pris en charge ({type})",
"media.upload": "Télécharger",
"media.uploadHelp": "Cliquez ou glissez-déposez ici une ou plusieurs images",
"media.uploadImage": "Télécharger l'image",
"menu.allCampaigns": "Toutes les campagnes",
"menu.allLists": "Toutes les listes",
"menu.allSubscribers": "Tous les abonnés",
"menu.dashboard": "Tableau de bord",
"menu.forms": "Formulaires",
"menu.import": "Importer",
"menu.logs": "Journaux",
"menu.media": "Médias",
"menu.newCampaign": "Créer nouveau",
"menu.settings": "Paramètres",
"public.campaignNotFound": "La liste de diffusion est introuvable.",
"public.confirmOptinSubTitle": "Confirmer l'abonnement",
"public.confirmSub": "Confirmer l'abonnement",
"public.confirmSubInfo": "Vous avez été ajouté aux listes suivantes :",
"public.confirmSubTitle": "Confirmer",
"public.dataRemoved": "Vos abonnements et toutes les données associées ont été supprimés.",
"public.dataRemovedTitle": "Données supprimées",
"public.dataSent": "Vos données vous ont été 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.errorFetchingEmail": "Message email introuvable",
"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.errorTitle": "Erreur",
"public.invalidFeature": "Cette fonctionnalité n'est pas disponible.",
"public.invalidLink": "Lien invalide",
"public.noListsAvailable": "Aucune liste disponible pour vous abonner.",
"public.noListsSelected": "Aucune liste valide sélectionnée pour s'abonner.",
"public.noSubInfo": "Il n'y a pas d'abonnement à confirmer.",
"public.noSubTitle": "Aucun abonnement",
"public.notFoundTitle": "Pas trouvé",
"public.privacyConfirmWipe": "Êtes-vous sûr de vouloir supprimer définitivement toutes vos données d'abonnement ?",
"public.privacyExport": "Exportez vos données",
"public.privacyExportHelp": "Une copie de vos données vous sera envoyée par email.",
"public.privacyTitle": "Confidentialité et 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.sub": "S'abonner",
"public.subConfirmed": "Abonné avec succès.",
"public.subConfirmedTitle": "Confirmé",
"public.subName": "Nom (facultatif)",
"public.subNotFound": "Abonnement introuvable.",
"public.subPrivateList": "Liste privée",
"public.subTitle": "S'abonner",
"public.unsub": "Se désabonner",
"public.unsubFull": "Se désabonner aussi de tous futurs emails.",
"public.unsubHelp": "Voulez-vous vous désabonner de cette liste de diffusion ?",
"public.unsubTitle": "Se désabonner",
"public.unsubbedInfo": "Vous vous êtes désabonné avec succès.",
"public.unsubbedTitle": "Désabonné",
"public.unsubscribeTitle": "Se désabonner de la liste de diffusion",
"settings.duplicateMessengerName": "Nom de messagerie en double : {name}",
"settings.errorEncoding": "Erreur lors du codage des paramètres : {error}",
"settings.errorNoSMTP": "Au moins un bloc SMTP doit être activé",
"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.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.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.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.language": "Langue",
"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.name": "Général",
"settings.general.rootURL": "URL racine",
"settings.general.rootURLHelp": "URL publique de l'installation (pas de barre oblique finale).",
"settings.invalidMessengerName": "Nom de messagerie non valide.",
"settings.media.provider": "Fournisseur",
"settings.media.s3.bucket": "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.bucketType": "Type de bucket",
"settings.media.s3.bucketTypePrivate": "Privé",
"settings.media.s3.bucketTypePublic": "Publique",
"settings.media.s3.key": "AWS access key",
"settings.media.s3.region": "Région",
"settings.media.s3.secret": "AWS access secret",
"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.title": "Téléchargements de médias",
"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.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.messengers.maxConns": "Nb. connexions max.",
"settings.messengers.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur.",
"settings.messengers.messageDiscard": "Annuler les modifications ?",
"settings.messengers.messageSaved": "Paramètres sauvegardés. Rechargement de l'application...",
"settings.messengers.name": "Messagers",
"settings.messengers.nameHelp": "Par exemple : my-sms. Alphanumérique / tiret.",
"settings.messengers.password": "Mot de passe",
"settings.messengers.retries": "Tentatives",
"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.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.url": "URL",
"settings.messengers.urlHelp": "URL racine du serveur Postback.",
"settings.messengers.username": "Nom d'utilisateur",
"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.concurrency": "Concurrence",
"settings.performance.concurrencyHelp": "Nombre de worker (threads) concurrents maximum qui enverrons les messages simultanément.",
"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.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.name": "Performance",
"settings.performance.slidingWindow": "Activer une limite par fenêtre glissante",
"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.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.slidingWindowRateHelp": "Nombre maximum de messages à envoyer pendant la durée de la fenêtre.",
"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.allowExport": "Autoriser l'exportation",
"settings.privacy.allowExportHelp": "Autoriser les abonnés à exporter les données collectées à leur sujet ?",
"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.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.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.name": "Vie privée",
"settings.smtp.authProtocol": "Protocole d'authentification",
"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.enabled": "Activée",
"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.host": "Hôte",
"settings.smtp.hostHelp": "Adresse hôte du serveur SMTP.",
"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.maxConns": "Nb. connexions max.",
"settings.smtp.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur SMTP.",
"settings.smtp.name": "SMTP",
"settings.smtp.password": "Mot de passe",
"settings.smtp.passwordHelp": "Entrée pour modifier",
"settings.smtp.port": "Port",
"settings.smtp.portHelp": "Port du serveur SMTP.",
"settings.smtp.retries": "Tentatives",
"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.skipTLS": "Ignorer la vérification TLS",
"settings.smtp.skipTLSHelp": "Ignorez la vérification du nom d'hôte sur le certificat TLS.",
"settings.smtp.tls": "TLS",
"settings.smtp.tlsHelp": "Activez STARTTLS.",
"settings.smtp.username": "Nom d'utilisateur",
"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.title": "Paramètres",
"subscribers.advancedQuery": "Avancées",
"subscribers.advancedQueryHelp": "Expression SQL partielle pour interroger les attributs de l'abonné",
"subscribers.attribs": "Attributs",
"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.confirmBlocklist": "Liste de blocage {num} abonné(s) ?",
"subscribers.confirmDelete": "Supprimer {num} abonné(s) ?",
"subscribers.confirmExport": "Exporter {num} abonné(s) ?",
"subscribers.downloadData": "Télécharger les données",
"subscribers.email": "Email",
"subscribers.emailExists": "L'email existe déjà.",
"subscribers.errorBlocklisting": "Erreur lors du blocage des abonnés : {error}",
"subscribers.errorInvalidIDs": "Un ou plusieurs identifiants non valides fournis : {error}",
"subscribers.errorNoIDs": "Aucune ID fournie.",
"subscribers.errorNoListsGiven": "Aucune liste donnée.",
"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.export": "Exportation",
"subscribers.invalidAction": "Action non valide.",
"subscribers.invalidEmail": "Email invalide.",
"subscribers.invalidJSON": "JSON non valide dans les attributs.",
"subscribers.invalidName": "Nom incorrect.",
"subscribers.listChangeApplied": "Modification de la liste effectuée.",
"subscribers.lists": "Listes",
"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.manageLists": "Gérer les listes",
"subscribers.markUnsubscribed": "Marquer comme désabonné",
"subscribers.newSubscriber": "Nouvel abonné",
"subscribers.numSelected": "{num} abonné(s) sélectionné(s)",
"subscribers.optinSubject": "Confirmer l'abonnement",
"subscribers.query": "Requête",
"subscribers.queryPlaceholder": "Email ou nom",
"subscribers.reset": "Réinitialiser",
"subscribers.selectAll": "Sélectionner tout {num}",
"subscribers.status.blocklisted": "Liste bloquée",
"subscribers.status.confirmed": "Confirmé",
"subscribers.status.enabled": "Activée",
"subscribers.status.subscribed": "Abonné",
"subscribers.status.unconfirmed": "Non confirmé",
"subscribers.status.unsubscribed": "Désabonné",
"subscribers.subscribersDeleted": "{num} abonné(s) supprimé(s)",
"templates.cantDeleteDefault": "Impossible de supprimer le modèle par défaut",
"templates.default": "Défaut",
"templates.dummyName": "Campagne de test",
"templates.dummySubject": "Objet de la campagne de test",
"templates.errorCompiling": "Erreur lors de la compilation du modèle : {error}",
"templates.errorRendering": "Message d'erreur lors du rendu : {error}",
"templates.fieldInvalidName": "Longueur du nom non valide.",
"templates.makeDefault": "Définir par défaut",
"templates.newTemplate": "Nouveau modèle",
"templates.placeholderHelp": "L'espace réservé {placeholder} doit apparaître exactement une fois dans le modèle.",
"templates.preview": "Aperçu",
"templates.rawHTML": "HTML brut"
}

View File

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