mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 00:06:17 +00:00
feat: warn before sending a message with unfilled placeholders
This commit is contained in:
@@ -292,6 +292,25 @@
|
||||
"no_sent_folder": "لم يتم العثور على مجلد المرسل",
|
||||
"permission_camera": "يلزم إذن الكاميرا لالتقاط صورة.",
|
||||
"permission_photos": "يلزم إذن مكتبة الصور لإرفاق الصور.",
|
||||
"placeholder_warning": {
|
||||
"title": "التحقق من العناصر النائبة؟",
|
||||
"message": "تحتوي رسالتك على نص نائب سيراه المستلمون. هل تريد تعديله قبل الإرسال؟",
|
||||
"found": "العناصر النائبة التي عُثر عليها",
|
||||
"more": "و{count} أخرى",
|
||||
"dont_ask_again": "عدم السؤال مرة أخرى",
|
||||
"send_anyway": "الإرسال على أي حال",
|
||||
"back": "العودة إلى التعديل"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"أدخل",
|
||||
"ادخل",
|
||||
"أضف",
|
||||
"اكتب"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "إضافة إلى جهات الاتصال",
|
||||
"recipient_copy": "نسخ العنوان",
|
||||
"recipient_move_to": "نقل إلى {field}",
|
||||
@@ -820,6 +839,10 @@
|
||||
"ask_mobile": "السؤال",
|
||||
"block_mobile": "الحظر"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "تحذير العناصر النائبة",
|
||||
"description": "طلب تأكيد قبل إرسال رسالة لا تزال تحتوي على عناصر نائبة مثل [TIME] أو [أدخل الاسم]، أو حقول قالب فارغة، أو نص غير مكتمل مثل TODO أو TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "الكتابة والقراءة بنص عادي فقط."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,30 @@
|
||||
"no_sent_folder": "No s'ha trobat la carpeta Enviats",
|
||||
"permission_camera": "Cal permís per a la càmera per fer una foto.",
|
||||
"permission_photos": "Cal permís per a la galeria de fotos per adjuntar imatges.",
|
||||
"placeholder_warning": {
|
||||
"title": "Voleu revisar els marcadors?",
|
||||
"message": "El missatge té text de marcador que veuran els destinataris. Voleu editar-lo abans d'enviar-lo?",
|
||||
"found": "Marcadors trobats",
|
||||
"more": "i {count} més",
|
||||
"dont_ask_again": "No ho tornis a preguntar",
|
||||
"send_anyway": "Envia igualment",
|
||||
"back": "Torna a l'edició"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"el teu",
|
||||
"la teva",
|
||||
"el vostre",
|
||||
"la vostra",
|
||||
"insereix",
|
||||
"inseriu",
|
||||
"afegeix",
|
||||
"afegiu",
|
||||
"escriviu"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Afegeix als contactes",
|
||||
"recipient_copy": "Copia l'adreça",
|
||||
"recipient_move_to": "Mou a {field}",
|
||||
@@ -820,6 +844,10 @@
|
||||
"ask_mobile": "Pregunta",
|
||||
"block_mobile": "Bloqueja"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Avís de marcadors",
|
||||
"description": "Demana confirmació abans d'enviar un missatge que encara té marcadors com [TIME] o [El teu nom], camps de plantilla buits o text inacabat com TODO o TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Redacta i llegeix només en text sense format."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,30 @@
|
||||
"no_sent_folder": "Složka Odeslané nebyla nalezena",
|
||||
"permission_camera": "K pořízení fotky je potřeba oprávnění k fotoaparátu.",
|
||||
"permission_photos": "K připojení obrázků je potřeba oprávnění ke knihovně fotek.",
|
||||
"placeholder_warning": {
|
||||
"title": "Zkontrolovat zástupné texty?",
|
||||
"message": "Vaše zpráva obsahuje zástupný text, který příjemci uvidí. Chcete ji před odesláním upravit?",
|
||||
"found": "Nalezené zástupné texty",
|
||||
"more": "a další ({count})",
|
||||
"dont_ask_again": "Znovu se neptat",
|
||||
"send_anyway": "Přesto odeslat",
|
||||
"back": "Zpět k úpravám"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"vaše",
|
||||
"váš",
|
||||
"vaši",
|
||||
"tvoje",
|
||||
"tvůj",
|
||||
"vložte",
|
||||
"doplňte",
|
||||
"zadejte",
|
||||
"uveďte"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Přidat do kontaktů",
|
||||
"recipient_copy": "Kopírovat adresu",
|
||||
"recipient_move_to": "Přesunout do pole {field}",
|
||||
@@ -820,6 +844,10 @@
|
||||
"ask_mobile": "Ptát se",
|
||||
"block_mobile": "Blokovat"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Upozornění na zástupné texty",
|
||||
"description": "Před odesláním zprávy, která stále obsahuje zástupné texty jako [TIME] nebo [Vaše jméno], nevyplněná pole šablony nebo nedokončený text jako TODO či TBD, požádat o potvrzení."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Psát a číst pouze v prostém textu."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,27 @@
|
||||
"no_sent_folder": "Ingen Sendt-mappe fundet",
|
||||
"permission_camera": "Der kræves kameratilladelse for at tage et foto.",
|
||||
"permission_photos": "Der kræves tilladelse til fotobiblioteket for at vedhæfte billeder.",
|
||||
"placeholder_warning": {
|
||||
"title": "Tjek pladsholdere?",
|
||||
"message": "Din besked indeholder pladsholdertekst, som modtagerne vil se. Vil du redigere den, før du sender?",
|
||||
"found": "Fundne pladsholdere",
|
||||
"more": "og {count} mere",
|
||||
"dont_ask_again": "Spørg ikke igen",
|
||||
"send_anyway": "Send alligevel",
|
||||
"back": "Tilbage til redigering"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"din",
|
||||
"dit",
|
||||
"dine",
|
||||
"indsæt",
|
||||
"tilføj",
|
||||
"skriv"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Tilføj til kontakter",
|
||||
"recipient_copy": "Kopiér adresse",
|
||||
"recipient_move_to": "Flyt til {field}",
|
||||
@@ -820,6 +841,10 @@
|
||||
"ask_mobile": "Spørg",
|
||||
"block_mobile": "Blokér"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Advarsel om pladsholdere",
|
||||
"description": "Bed om bekræftelse, før en besked sendes, der stadig har pladsholdere som [TIME] eller [Dit navn], tomme skabelonfelter eller ufærdig tekst som TODO eller TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Skriv og læs kun i ren tekst."
|
||||
},
|
||||
|
||||
@@ -294,6 +294,31 @@
|
||||
"no_sent_folder": "Kein Gesendet-Ordner gefunden",
|
||||
"permission_camera": "Zum Aufnehmen eines Fotos ist die Kameraberechtigung erforderlich.",
|
||||
"permission_photos": "Zum Anhängen von Bildern ist die Berechtigung für die Fotomediathek erforderlich.",
|
||||
"placeholder_warning": {
|
||||
"title": "Platzhalter prüfen?",
|
||||
"message": "Die Nachricht enthält Platzhaltertext, den die Empfänger sehen. Vor dem Senden bearbeiten?",
|
||||
"found": "Gefundene Platzhalter",
|
||||
"more": "und {count} weitere",
|
||||
"dont_ask_again": "Nicht mehr fragen",
|
||||
"send_anyway": "Trotzdem senden",
|
||||
"back": "Zurück zur Bearbeitung"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"ihr",
|
||||
"ihre",
|
||||
"ihren",
|
||||
"dein",
|
||||
"deine",
|
||||
"deinen"
|
||||
],
|
||||
"after": [
|
||||
"einfügen",
|
||||
"eintragen",
|
||||
"ergänzen"
|
||||
],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Zu Kontakten hinzufügen",
|
||||
"recipient_copy": "Adresse kopieren",
|
||||
"recipient_move_to": "Verschieben nach {field}",
|
||||
@@ -829,6 +854,10 @@
|
||||
"ask_mobile": "Fragen",
|
||||
"block_mobile": "Blockieren"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Warnung bei Platzhaltern",
|
||||
"description": "Vor dem Senden einer Nachricht nachfragen, die noch Platzhalter wie [TIME] oder [Ihr Name], leere Vorlagenfelder oder unfertigen Text wie TODO oder TBD enthält."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Nur im Klartext verfassen und lesen."
|
||||
},
|
||||
|
||||
@@ -306,6 +306,32 @@
|
||||
"no_sent_folder": "No Sent folder found",
|
||||
"permission_camera": "Camera permission is required to take a photo.",
|
||||
"permission_photos": "Photo library permission is required to attach images.",
|
||||
"placeholder_warning": {
|
||||
"back": "Back to editing",
|
||||
"dont_ask_again": "Don't ask again",
|
||||
"found": "Placeholders found",
|
||||
"message": "Your message has placeholder text that recipients will see. Do you want to edit it before sending?",
|
||||
"more": "and {count} more",
|
||||
"send_anyway": "Send anyway",
|
||||
"title": "Check placeholders?"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"after": [],
|
||||
"before": [
|
||||
"your",
|
||||
"insert",
|
||||
"enter",
|
||||
"add"
|
||||
],
|
||||
"unfinished": [
|
||||
"TODO",
|
||||
"TBD",
|
||||
"TBA",
|
||||
"TBC",
|
||||
"FIXME",
|
||||
"XXX"
|
||||
]
|
||||
},
|
||||
"read_receipt_off": "Request a read receipt",
|
||||
"recipient_add_contact": "Add to contacts",
|
||||
"recipient_copy": "Copy address",
|
||||
@@ -911,6 +937,10 @@
|
||||
"ask_mobile": "Ask",
|
||||
"block_mobile": "Block"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"description": "Ask for confirmation before sending a message that still has placeholders like [TIME] or [Your Name], template fields left empty, or unfinished text like TODO or TBD.",
|
||||
"label": "Placeholder warning"
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Compose and read in plain text only."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,36 @@
|
||||
"no_sent_folder": "No se encontró la carpeta Enviados",
|
||||
"permission_camera": "Se necesita permiso de cámara para tomar una foto.",
|
||||
"permission_photos": "Se necesita acceso a la galería de fotos para adjuntar imágenes.",
|
||||
"placeholder_warning": {
|
||||
"title": "¿Revisar los marcadores?",
|
||||
"message": "El mensaje tiene texto de marcador que verán los destinatarios. ¿Editarlo antes de enviar?",
|
||||
"found": "Marcadores encontrados",
|
||||
"more": "y {count} más",
|
||||
"dont_ask_again": "No volver a preguntar",
|
||||
"send_anyway": "Enviar de todos modos",
|
||||
"back": "Volver a editar"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"tu",
|
||||
"tus",
|
||||
"su",
|
||||
"sus",
|
||||
"insertar",
|
||||
"inserte",
|
||||
"inserta",
|
||||
"añadir",
|
||||
"añade",
|
||||
"agregar",
|
||||
"agrega",
|
||||
"escribe",
|
||||
"escriba",
|
||||
"indica",
|
||||
"indique"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Añadir a contactos",
|
||||
"recipient_copy": "Copiar dirección",
|
||||
"recipient_move_to": "Mover a {field}",
|
||||
@@ -820,6 +850,10 @@
|
||||
"ask_mobile": "Preguntar",
|
||||
"block_mobile": "Bloquear"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Aviso de marcadores",
|
||||
"description": "Pedir confirmación antes de enviar un mensaje que todavía tiene marcadores como [TIME] o [Tu nombre], campos de plantilla vacíos o texto sin terminar como TODO o TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Redactar y leer solo en texto sin formato."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,24 @@
|
||||
"no_sent_folder": "پوشه ارسال شده پیدا نشد",
|
||||
"permission_camera": "برای گرفتن عکس، دسترسی به دوربین لازم است.",
|
||||
"permission_photos": "برای پیوست کردن تصاویر، دسترسی به گالری عکس لازم است.",
|
||||
"placeholder_warning": {
|
||||
"title": "متنهای جایگزین بررسی شود؟",
|
||||
"message": "پیام شما متن جایگزینی دارد که گیرندگان آن را میبینند. میخواهید پیش از ارسال آن را ویرایش کنید؟",
|
||||
"found": "متنهای جایگزین یافتشده",
|
||||
"more": "و {count} مورد دیگر",
|
||||
"dont_ask_again": "دیگر نپرس",
|
||||
"send_anyway": "ارسال به هر حال",
|
||||
"back": "بازگشت به ویرایش"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [],
|
||||
"after": [
|
||||
"شما",
|
||||
"وارد کنید",
|
||||
"اضافه کنید"
|
||||
],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "افزودن به مخاطبین",
|
||||
"recipient_copy": "کپی نشانی",
|
||||
"recipient_move_to": "انتقال به {field}",
|
||||
@@ -820,6 +838,10 @@
|
||||
"ask_mobile": "بپرس",
|
||||
"block_mobile": "مسدود"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "هشدار متن جایگزین",
|
||||
"description": "پیش از ارسال پیامی که هنوز متن جایگزین مانند [TIME] یا [نام شما]، فیلدهای خالی الگو یا متن ناتمام مانند TODO یا TBD دارد، تأیید بگیرید."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "نوشتن و خواندن فقط بهصورت متن ساده."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,34 @@
|
||||
"no_sent_folder": "Aucun dossier Envoyés trouvé",
|
||||
"permission_camera": "L'autorisation d'accès à l'appareil photo est nécessaire pour prendre une photo.",
|
||||
"permission_photos": "L'autorisation d'accès à la photothèque est nécessaire pour joindre des images.",
|
||||
"placeholder_warning": {
|
||||
"title": "Vérifier les espaces réservés ?",
|
||||
"message": "Votre message contient du texte d'espace réservé que les destinataires verront. Voulez-vous le modifier avant l'envoi ?",
|
||||
"found": "Espaces réservés trouvés",
|
||||
"more": "et {count} de plus",
|
||||
"dont_ask_again": "Ne plus me demander",
|
||||
"send_anyway": "Envoyer quand même",
|
||||
"back": "Retour à l'édition"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"votre",
|
||||
"vos",
|
||||
"ton",
|
||||
"ta",
|
||||
"tes",
|
||||
"insérer",
|
||||
"insérez",
|
||||
"ajouter",
|
||||
"ajoutez",
|
||||
"saisir",
|
||||
"saisissez",
|
||||
"indiquer",
|
||||
"indiquez"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Ajouter aux contacts",
|
||||
"recipient_copy": "Copier l'adresse",
|
||||
"recipient_move_to": "Déplacer vers {field}",
|
||||
@@ -820,6 +848,10 @@
|
||||
"ask_mobile": "Demander",
|
||||
"block_mobile": "Bloquer"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Avertissement d'espaces réservés",
|
||||
"description": "Demander confirmation avant d'envoyer un message qui contient encore des espaces réservés comme [TIME] ou [Votre nom], des champs de modèle vides ou du texte inachevé comme TODO ou TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Rédiger et lire uniquement en texte brut."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,28 @@
|
||||
"no_sent_folder": "לא נמצאה תיקיית \"נשלח\"",
|
||||
"permission_camera": "נדרשת הרשאת מצלמה כדי לצלם תמונה.",
|
||||
"permission_photos": "נדרשת הרשאה לספריית התמונות כדי לצרף תמונות.",
|
||||
"placeholder_warning": {
|
||||
"title": "לבדוק מצייני מיקום?",
|
||||
"message": "ההודעה מכילה טקסט מציין מיקום שהנמענים יראו. לערוך אותה לפני השליחה?",
|
||||
"found": "מצייני מיקום שנמצאו",
|
||||
"more": "ועוד {count}",
|
||||
"dont_ask_again": "אל תשאל שוב",
|
||||
"send_anyway": "שלח בכל זאת",
|
||||
"back": "חזור לעריכה"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"הכנס",
|
||||
"הכניסו",
|
||||
"הזן",
|
||||
"הוסף"
|
||||
],
|
||||
"after": [
|
||||
"שלך",
|
||||
"שלכם"
|
||||
],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "הוסף לאנשי הקשר",
|
||||
"recipient_copy": "העתק כתובת",
|
||||
"recipient_move_to": "העבר אל {field}",
|
||||
@@ -820,6 +842,10 @@
|
||||
"ask_mobile": "שאל",
|
||||
"block_mobile": "חסום"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "אזהרה על מצייני מיקום",
|
||||
"description": "בקש אישור לפני שליחת הודעה שעדיין יש בה מצייני מיקום כמו [TIME] או [השם שלך], שדות תבנית ריקים או טקסט לא גמור כמו TODO או TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "כתיבה וקריאה בטקסט רגיל בלבד."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,26 @@
|
||||
"no_sent_folder": "Nem található Elküldött mappa",
|
||||
"permission_camera": "Fénykép készítéséhez kameraengedély szükséges.",
|
||||
"permission_photos": "Képek csatolásához hozzáférés szükséges a fotókhoz.",
|
||||
"placeholder_warning": {
|
||||
"title": "Ellenőrzi a helykitöltőket?",
|
||||
"message": "Az üzenet olyan helykitöltő szöveget tartalmaz, amelyet a címzettek is látni fognak. Szerkeszti küldés előtt?",
|
||||
"found": "Talált helykitöltők",
|
||||
"more": "és még {count}",
|
||||
"dont_ask_again": "Ne kérdezze többé",
|
||||
"send_anyway": "Küldés így is",
|
||||
"back": "Vissza a szerkesztéshez"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"az ön",
|
||||
"a te",
|
||||
"írja be",
|
||||
"illessze be",
|
||||
"adja meg"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Hozzáadás a névjegyekhez",
|
||||
"recipient_copy": "Cím másolása",
|
||||
"recipient_move_to": "Áthelyezés ide: {field}",
|
||||
@@ -820,6 +840,10 @@
|
||||
"ask_mobile": "Rákérdezés",
|
||||
"block_mobile": "Blokkolás"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Figyelmeztetés helykitöltők esetén",
|
||||
"description": "Megerősítés kérése olyan üzenet küldése előtt, amelyben még helykitöltők vannak, például [TIME] vagy [Az Ön neve], üres sablonmezők vagy befejezetlen szöveg, például TODO vagy TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Írás és olvasás csak egyszerű szövegként."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,32 @@
|
||||
"no_sent_folder": "Nessuna cartella Inviati trovata",
|
||||
"permission_camera": "Serve l'autorizzazione alla fotocamera per scattare una foto.",
|
||||
"permission_photos": "Serve l'autorizzazione alla galleria foto per allegare immagini.",
|
||||
"placeholder_warning": {
|
||||
"title": "Controllare i segnaposto?",
|
||||
"message": "Il messaggio contiene testo segnaposto che i destinatari vedranno. Vuoi modificarlo prima di inviarlo?",
|
||||
"found": "Segnaposto trovati",
|
||||
"more": "e altri {count}",
|
||||
"dont_ask_again": "Non chiedere più",
|
||||
"send_anyway": "Invia comunque",
|
||||
"back": "Torna alla modifica"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"il tuo",
|
||||
"la tua",
|
||||
"i tuoi",
|
||||
"le tue",
|
||||
"il suo",
|
||||
"la sua",
|
||||
"inserisci",
|
||||
"inserire",
|
||||
"aggiungi",
|
||||
"aggiungere",
|
||||
"indica"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Aggiungi ai contatti",
|
||||
"recipient_copy": "Copia indirizzo",
|
||||
"recipient_move_to": "Sposta in {field}",
|
||||
@@ -820,6 +846,10 @@
|
||||
"ask_mobile": "Chiedi",
|
||||
"block_mobile": "Blocca"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Avviso segnaposto",
|
||||
"description": "Chiedi conferma prima di inviare un messaggio che contiene ancora segnaposto come [TIME] o [Il tuo nome], campi del modello vuoti o testo incompleto come TODO o TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Scrivi e leggi solo in testo normale."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,30 @@
|
||||
"no_sent_folder": "送信済みフォルダーが見つかりません",
|
||||
"permission_camera": "写真を撮影するには、カメラへのアクセス許可が必要です。",
|
||||
"permission_photos": "画像を添付するには、写真ライブラリへのアクセス許可が必要です。",
|
||||
"placeholder_warning": {
|
||||
"title": "プレースホルダーを確認しますか?",
|
||||
"message": "メッセージに受信者に表示されるプレースホルダーのテキストが含まれています。送信前に編集しますか?",
|
||||
"found": "見つかったプレースホルダー",
|
||||
"more": "ほか {count} 件",
|
||||
"dont_ask_again": "今後は確認しない",
|
||||
"send_anyway": "そのまま送信",
|
||||
"back": "編集に戻る"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"あなたの-",
|
||||
"貴社の-",
|
||||
"御社の-"
|
||||
],
|
||||
"after": [
|
||||
"-を入力",
|
||||
"-を記入",
|
||||
"-を挿入",
|
||||
"-を追加",
|
||||
"-を入れる"
|
||||
],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "連絡先に追加",
|
||||
"recipient_copy": "アドレスをコピー",
|
||||
"recipient_move_to": "{field}に移動",
|
||||
@@ -820,6 +844,10 @@
|
||||
"ask_mobile": "確認",
|
||||
"block_mobile": "ブロック"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "プレースホルダーの警告",
|
||||
"description": "[TIME] や [あなたの名前] などのプレースホルダー、空のテンプレート項目、TODO や TBD などの未完成のテキストが残っているメッセージを送信する前に確認します。"
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "プレーンテキストのみで作成・閲覧します。"
|
||||
},
|
||||
|
||||
@@ -292,6 +292,28 @@
|
||||
"no_sent_folder": "보낸편지함을 찾을 수 없어요",
|
||||
"permission_camera": "사진을 찍으려면 카메라 권한이 필요해요.",
|
||||
"permission_photos": "이미지를 첨부하려면 사진 보관함 권한이 필요해요.",
|
||||
"placeholder_warning": {
|
||||
"title": "자리표시자를 확인할까요?",
|
||||
"message": "메시지에 받는 사람에게 보이는 자리표시자 텍스트가 있습니다. 보내기 전에 수정할까요?",
|
||||
"found": "찾은 자리표시자",
|
||||
"more": "외 {count}개",
|
||||
"dont_ask_again": "다시 묻지 않기",
|
||||
"send_anyway": "그래도 보내기",
|
||||
"back": "편집으로 돌아가기"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"귀하의",
|
||||
"당신의",
|
||||
"고객님의"
|
||||
],
|
||||
"after": [
|
||||
"입력",
|
||||
"삽입",
|
||||
"추가"
|
||||
],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "연락처에 추가",
|
||||
"recipient_copy": "주소 복사",
|
||||
"recipient_move_to": "{field}(으)로 이동",
|
||||
@@ -820,6 +842,10 @@
|
||||
"ask_mobile": "묻기",
|
||||
"block_mobile": "차단"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "자리표시자 경고",
|
||||
"description": "[TIME]이나 [귀하의 이름] 같은 자리표시자, 비어 있는 템플릿 필드, TODO나 TBD 같은 미완성 텍스트가 남아 있는 메시지를 보내기 전에 확인을 요청합니다."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "일반 텍스트로만 작성하고 읽어요."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,29 @@
|
||||
"no_sent_folder": "Mape Nosūtītie nav atrasta",
|
||||
"permission_camera": "Lai uzņemtu fotoattēlu, nepieciešama kameras atļauja.",
|
||||
"permission_photos": "Lai pievienotu attēlus, nepieciešama fotoattēlu bibliotēkas atļauja.",
|
||||
"placeholder_warning": {
|
||||
"title": "Pārbaudīt vietturus?",
|
||||
"message": "Ziņā ir viettura teksts, ko redzēs adresāti. Vai rediģēt to pirms sūtīšanas?",
|
||||
"found": "Atrastie vietturi",
|
||||
"more": "un vēl {count}",
|
||||
"dont_ask_again": "Vairs nejautāt",
|
||||
"send_anyway": "Sūtīt tik un tā",
|
||||
"back": "Atpakaļ pie rediģēšanas"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"jūsu",
|
||||
"tavs",
|
||||
"tava",
|
||||
"ievadiet",
|
||||
"ierakstiet",
|
||||
"ievietojiet",
|
||||
"pievienojiet",
|
||||
"norādiet"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Pievienot kontaktpersonām",
|
||||
"recipient_copy": "Kopēt adresi",
|
||||
"recipient_move_to": "Pārvietot uz lauku {field}",
|
||||
@@ -820,6 +843,10 @@
|
||||
"ask_mobile": "Jautāt",
|
||||
"block_mobile": "Bloķēt"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Brīdinājums par vietturiem",
|
||||
"description": "Pieprasīt apstiprinājumu pirms tādas ziņas sūtīšanas, kurā vēl ir vietturi, piemēram, [TIME] vai [Jūsu vārds], tukši veidnes lauki vai nepabeigts teksts, piemēram, TODO vai TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Rakstīt un lasīt tikai vienkāršā tekstā."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,25 @@
|
||||
"no_sent_folder": "Илгээсэн хавтас олдсонгүй",
|
||||
"permission_camera": "Зураг авахын тулд камерын зөвшөөрөл шаардлагатай.",
|
||||
"permission_photos": "Зураг хавсаргахын тулд зургийн сангийн зөвшөөрөл шаардлагатай.",
|
||||
"placeholder_warning": {
|
||||
"title": "Орлуулагч текстийг шалгах уу?",
|
||||
"message": "Таны зурваст хүлээн авагчид харагдах орлуулагч текст байна. Илгээхээс өмнө засах уу?",
|
||||
"found": "Олдсон орлуулагч текст",
|
||||
"more": "болон өөр {count}",
|
||||
"dont_ask_again": "Дахиж битгий асуу",
|
||||
"send_anyway": "Ямар ч байсан илгээнэ үү",
|
||||
"back": "Засварлах руу буцах"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"таны",
|
||||
"чиний"
|
||||
],
|
||||
"after": [
|
||||
"оруулна уу"
|
||||
],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Харилцагчдад нэмэх",
|
||||
"recipient_copy": "Хаягийг хуулах",
|
||||
"recipient_move_to": "\"{field}\" талбар руу зөөх",
|
||||
@@ -820,6 +839,10 @@
|
||||
"ask_mobile": "Асуух",
|
||||
"block_mobile": "Блоклох"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Орлуулагч текстийн анхааруулга",
|
||||
"description": "[TIME] эсвэл [Таны нэр] зэрэг орлуулагч текст, хоосон загварын талбар, эсвэл TODO, TBD зэрэг дуусаагүй текст үлдсэн мессеж илгээхээс өмнө баталгаажуулахыг хүс."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Зөвхөн энгийн текстээр бичих, унших."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,27 @@
|
||||
"no_sent_folder": "Fant ingen Sendt-mappe",
|
||||
"permission_camera": "Du må gi tilgang til kameraet for å ta et bilde.",
|
||||
"permission_photos": "Du må gi tilgang til bildebiblioteket for å legge ved bilder.",
|
||||
"placeholder_warning": {
|
||||
"title": "Sjekke plassholdere?",
|
||||
"message": "Meldingen har plassholdertekst som mottakerne vil se. Vil du redigere den før du sender?",
|
||||
"found": "Plassholdere som ble funnet",
|
||||
"more": "og {count} til",
|
||||
"dont_ask_again": "Ikke spør igjen",
|
||||
"send_anyway": "Send likevel",
|
||||
"back": "Tilbake til redigering"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"din",
|
||||
"ditt",
|
||||
"dine",
|
||||
"sett inn",
|
||||
"legg til",
|
||||
"skriv"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Legg til som kontakt",
|
||||
"recipient_copy": "Kopier adressen",
|
||||
"recipient_move_to": "Flytt til {field}",
|
||||
@@ -820,6 +841,10 @@
|
||||
"ask_mobile": "Spør",
|
||||
"block_mobile": "Blokker"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Advarsel om plassholdere",
|
||||
"description": "Be om bekreftelse før du sender en melding som fortsatt har plassholdere som [TIME] eller [Ditt navn], tomme malfelt eller uferdig tekst som TODO eller TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Skriv og les bare ren tekst."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,30 @@
|
||||
"no_sent_folder": "Geen map Verzonden gevonden",
|
||||
"permission_camera": "Toestemming voor de camera is nodig om een foto te maken.",
|
||||
"permission_photos": "Toestemming voor de fotobibliotheek is nodig om afbeeldingen bij te voegen.",
|
||||
"placeholder_warning": {
|
||||
"title": "Tijdelijke aanduidingen controleren?",
|
||||
"message": "Je bericht bevat tijdelijke tekst die ontvangers zullen zien. Wil je het bewerken voordat je het verzendt?",
|
||||
"found": "Gevonden tijdelijke aanduidingen",
|
||||
"more": "en nog {count}",
|
||||
"dont_ask_again": "Niet meer vragen",
|
||||
"send_anyway": "Toch verzenden",
|
||||
"back": "Terug naar bewerken"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"je",
|
||||
"jouw",
|
||||
"uw",
|
||||
"voeg",
|
||||
"vul"
|
||||
],
|
||||
"after": [
|
||||
"invoegen",
|
||||
"invullen",
|
||||
"toevoegen"
|
||||
],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Aan contacten toevoegen",
|
||||
"recipient_copy": "Adres kopiëren",
|
||||
"recipient_move_to": "Verplaatsen naar {field}",
|
||||
@@ -820,6 +844,10 @@
|
||||
"ask_mobile": "Vragen",
|
||||
"block_mobile": "Blokkeren"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Waarschuwing bij tijdelijke aanduidingen",
|
||||
"description": "Vraag om bevestiging voordat een bericht wordt verzonden dat nog tijdelijke aanduidingen bevat zoals [TIME] of [Uw naam], lege sjabloonvelden of onafgemaakte tekst zoals TODO of TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Alleen in platte tekst opstellen en lezen."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,30 @@
|
||||
"no_sent_folder": "Nie znaleziono folderu Wysłane",
|
||||
"permission_camera": "Aby zrobić zdjęcie, potrzebny jest dostęp do aparatu.",
|
||||
"permission_photos": "Aby załączać obrazy, potrzebny jest dostęp do galerii zdjęć.",
|
||||
"placeholder_warning": {
|
||||
"title": "Sprawdzić symbole zastępcze?",
|
||||
"message": "Wiadomość zawiera tekst zastępczy, który zobaczą odbiorcy. Czy chcesz go poprawić przed wysłaniem?",
|
||||
"found": "Znalezione symbole zastępcze",
|
||||
"more": "i jeszcze {count}",
|
||||
"dont_ask_again": "Nie pytaj ponownie",
|
||||
"send_anyway": "Wyślij mimo to",
|
||||
"back": "Wróć do edycji"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"twoje",
|
||||
"twój",
|
||||
"twoja",
|
||||
"pana",
|
||||
"pani",
|
||||
"wstaw",
|
||||
"wpisz",
|
||||
"dodaj",
|
||||
"podaj"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Dodaj do kontaktów",
|
||||
"recipient_copy": "Kopiuj adres",
|
||||
"recipient_move_to": "Przenieś do pola {field}",
|
||||
@@ -820,6 +844,10 @@
|
||||
"ask_mobile": "Pytaj",
|
||||
"block_mobile": "Blokuj"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Ostrzeżenie o symbolach zastępczych",
|
||||
"description": "Poproś o potwierdzenie przed wysłaniem wiadomości, która wciąż zawiera symbole zastępcze, takie jak [TIME] lub [Twoje imię], puste pola szablonu albo niedokończony tekst, taki jak TODO lub TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Twórz i czytaj wiadomości tylko jako zwykły tekst."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,33 @@
|
||||
"no_sent_folder": "Nenhuma pasta Enviados encontrada",
|
||||
"permission_camera": "É necessária permissão da câmera para tirar uma foto.",
|
||||
"permission_photos": "É necessária permissão da galeria de fotos para anexar imagens.",
|
||||
"placeholder_warning": {
|
||||
"title": "Verificar marcadores?",
|
||||
"message": "A mensagem tem texto de marcador que os destinatários vão ver. Quer editá-la antes de enviar?",
|
||||
"found": "Marcadores encontrados",
|
||||
"more": "e mais {count}",
|
||||
"dont_ask_again": "Não perguntar novamente",
|
||||
"send_anyway": "Enviar mesmo assim",
|
||||
"back": "Voltar à edição"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"seu",
|
||||
"sua",
|
||||
"seus",
|
||||
"suas",
|
||||
"teu",
|
||||
"tua",
|
||||
"inserir",
|
||||
"insira",
|
||||
"adicionar",
|
||||
"adicione",
|
||||
"digite",
|
||||
"informe"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Adicionar aos contatos",
|
||||
"recipient_copy": "Copiar endereço",
|
||||
"recipient_move_to": "Mover para {field}",
|
||||
@@ -820,6 +847,10 @@
|
||||
"ask_mobile": "Perguntar",
|
||||
"block_mobile": "Bloquear"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Aviso de marcadores",
|
||||
"description": "Pedir confirmação antes de enviar uma mensagem que ainda tem marcadores como [TIME] ou [Seu nome], campos de modelo vazios ou texto inacabado como TODO ou TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Escrever e ler apenas em texto simples."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,30 @@
|
||||
"no_sent_folder": "Nu s-a găsit folderul Trimise",
|
||||
"permission_camera": "Este necesară permisiunea pentru cameră pentru a face o fotografie.",
|
||||
"permission_photos": "Este necesară permisiunea pentru galeria foto pentru a atașa imagini.",
|
||||
"placeholder_warning": {
|
||||
"title": "Verificați substituenții?",
|
||||
"message": "Mesajul conține text substituent pe care destinatarii îl vor vedea. Doriți să-l editați înainte de trimitere?",
|
||||
"found": "Substituenți găsiți",
|
||||
"more": "și încă {count}",
|
||||
"dont_ask_again": "Nu mai întreba",
|
||||
"send_anyway": "Trimite oricum",
|
||||
"back": "Înapoi la editare"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"introduceți",
|
||||
"inserați",
|
||||
"adăugați",
|
||||
"completați"
|
||||
],
|
||||
"after": [
|
||||
"tău",
|
||||
"ta",
|
||||
"dvs.",
|
||||
"dumneavoastră"
|
||||
],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Adaugă la contacte",
|
||||
"recipient_copy": "Copiați adresa",
|
||||
"recipient_move_to": "Mutați la {field}",
|
||||
@@ -820,6 +844,10 @@
|
||||
"ask_mobile": "Întrebați",
|
||||
"block_mobile": "Blocați"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Avertisment pentru substituenți",
|
||||
"description": "Cere confirmare înainte de a trimite un mesaj care încă are substituenți precum [TIME] sau [Numele tău], câmpuri de șablon goale sau text neterminat precum TODO sau TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Scrieți și citiți numai în text simplu."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,33 @@
|
||||
"no_sent_folder": "Папка «Отправленные» не найдена",
|
||||
"permission_camera": "Чтобы сделать фото, нужен доступ к камере.",
|
||||
"permission_photos": "Чтобы прикреплять изображения, нужен доступ к фотогалерее.",
|
||||
"placeholder_warning": {
|
||||
"title": "Проверить заполнители?",
|
||||
"message": "В письме есть текст-заполнитель, который увидят получатели. Отредактировать его перед отправкой?",
|
||||
"found": "Найденные заполнители",
|
||||
"more": "и ещё {count}",
|
||||
"dont_ask_again": "Больше не спрашивать",
|
||||
"send_anyway": "Отправить всё равно",
|
||||
"back": "Вернуться к редактированию"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"ваше",
|
||||
"ваш",
|
||||
"ваша",
|
||||
"ваши",
|
||||
"твоё",
|
||||
"твое",
|
||||
"твой",
|
||||
"твоя",
|
||||
"вставьте",
|
||||
"укажите",
|
||||
"введите",
|
||||
"добавьте"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Добавить в контакты",
|
||||
"recipient_copy": "Копировать адрес",
|
||||
"recipient_move_to": "Переместить в «{field}»",
|
||||
@@ -820,6 +847,10 @@
|
||||
"ask_mobile": "Спрашивать",
|
||||
"block_mobile": "Блокировать"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Предупреждение о заполнителях",
|
||||
"description": "Запрашивать подтверждение перед отправкой письма, в котором остались заполнители вроде [TIME] или [Ваше имя], пустые поля шаблона или незаконченный текст вроде TODO или TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Писать и читать письма только в виде простого текста."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,29 @@
|
||||
"no_sent_folder": "Priečinok Odoslané sa nenašiel",
|
||||
"permission_camera": "Na odfotenie je potrebné povolenie pre fotoaparát.",
|
||||
"permission_photos": "Na priloženie obrázkov je potrebné povolenie pre knižnicu fotiek.",
|
||||
"placeholder_warning": {
|
||||
"title": "Skontrolovať zástupné texty?",
|
||||
"message": "Vaša správa obsahuje zástupný text, ktorý uvidia príjemcovia. Chcete ju pred odoslaním upraviť?",
|
||||
"found": "Nájdené zástupné texty",
|
||||
"more": "a ďalšie ({count})",
|
||||
"dont_ask_again": "Znova sa nepýtať",
|
||||
"send_anyway": "Odoslať aj tak",
|
||||
"back": "Späť na úpravu"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"vaše",
|
||||
"váš",
|
||||
"vaša",
|
||||
"tvoje",
|
||||
"vložte",
|
||||
"doplňte",
|
||||
"zadajte",
|
||||
"uveďte"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Pridať do kontaktov",
|
||||
"recipient_copy": "Kopírovať adresu",
|
||||
"recipient_move_to": "Presunúť do poľa {field}",
|
||||
@@ -820,6 +843,10 @@
|
||||
"ask_mobile": "Pýtať sa",
|
||||
"block_mobile": "Blokovať"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Upozornenie na zástupné texty",
|
||||
"description": "Pred odoslaním správy, ktorá ešte obsahuje zástupné texty ako [TIME] alebo [Vaše meno], prázdne polia šablóny alebo nedokončený text ako TODO či TBD, požiadať o potvrdenie."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Písať a čítať iba v obyčajnom texte."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,31 @@
|
||||
"no_sent_folder": "Gönderilenler klasörü bulunamadı",
|
||||
"permission_camera": "Fotoğraf çekmek için kamera izni gerekiyor.",
|
||||
"permission_photos": "Görsel eklemek için fotoğraf arşivi izni gerekiyor.",
|
||||
"placeholder_warning": {
|
||||
"title": "Yer tutucular kontrol edilsin mi?",
|
||||
"message": "İletinizde alıcıların göreceği yer tutucu metin var. Göndermeden önce düzenlemek ister misiniz?",
|
||||
"found": "Bulunan yer tutucular",
|
||||
"more": "ve {count} tane daha",
|
||||
"dont_ask_again": "Bir daha sorma",
|
||||
"send_anyway": "Yine de gönder",
|
||||
"back": "Düzenlemeye dön"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"şirketinizin",
|
||||
"buraya"
|
||||
],
|
||||
"after": [
|
||||
"ekleyin",
|
||||
"girin",
|
||||
"yazın",
|
||||
"-ınız",
|
||||
"-iniz",
|
||||
"-unuz",
|
||||
"-ünüz"
|
||||
],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Kişilere ekle",
|
||||
"recipient_copy": "Adresi kopyala",
|
||||
"recipient_move_to": "{field} alanına taşı",
|
||||
@@ -820,6 +845,10 @@
|
||||
"ask_mobile": "Sor",
|
||||
"block_mobile": "Engelle"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Yer tutucu uyarısı",
|
||||
"description": "Hâlâ [TIME] veya [Adınız] gibi yer tutucular, boş şablon alanları ya da TODO veya TBD gibi bitmemiş metin içeren bir ileti gönderilmeden önce onay iste."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Yalnızca düz metin olarak yazın ve okuyun."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,32 @@
|
||||
"no_sent_folder": "Папку «Надіслані» не знайдено",
|
||||
"permission_camera": "Щоб зробити фото, потрібен доступ до камери.",
|
||||
"permission_photos": "Щоб вкладати зображення, потрібен доступ до фотогалереї.",
|
||||
"placeholder_warning": {
|
||||
"title": "Перевірити заповнювачі?",
|
||||
"message": "У листі є текст-заповнювач, який побачать одержувачі. Відредагувати його перед надсиланням?",
|
||||
"found": "Знайдені заповнювачі",
|
||||
"more": "і ще {count}",
|
||||
"dont_ask_again": "Більше не запитувати",
|
||||
"send_anyway": "Все одно надішліть",
|
||||
"back": "Назад до редагування"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"ваше",
|
||||
"ваш",
|
||||
"ваша",
|
||||
"ваші",
|
||||
"твоє",
|
||||
"твій",
|
||||
"твоя",
|
||||
"вставте",
|
||||
"вкажіть",
|
||||
"введіть",
|
||||
"додайте"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "Додати до контактів",
|
||||
"recipient_copy": "Копіювати адресу",
|
||||
"recipient_move_to": "Перемістити в «{field}»",
|
||||
@@ -820,6 +846,10 @@
|
||||
"ask_mobile": "Питати",
|
||||
"block_mobile": "Блокувати"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "Попередження про заповнювачі",
|
||||
"description": "Запитувати підтвердження перед надсиланням листа, у якому лишилися заповнювачі на кшталт [TIME] або [Ваше ім’я], порожні поля шаблону чи незавершений текст на кшталт TODO або TBD."
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "Писати й читати листи лише як простий текст."
|
||||
},
|
||||
|
||||
@@ -292,6 +292,29 @@
|
||||
"no_sent_folder": "找不到寄件備份信件匣",
|
||||
"permission_camera": "需要相機權限才能拍照。",
|
||||
"permission_photos": "需要相片圖庫權限才能附加圖片。",
|
||||
"placeholder_warning": {
|
||||
"title": "檢查預留位置?",
|
||||
"message": "你的郵件中有收件者會看到的預留位置文字。要在傳送前編輯嗎?",
|
||||
"found": "找到的預留位置",
|
||||
"more": "以及另外 {count} 個",
|
||||
"dont_ask_again": "不再提示",
|
||||
"send_anyway": "仍然傳送",
|
||||
"back": "繼續編輯"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"您的-",
|
||||
"你的-",
|
||||
"貴公司-",
|
||||
"插入-",
|
||||
"輸入-",
|
||||
"填寫-",
|
||||
"新增-",
|
||||
"在此-"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "新增至聯絡人",
|
||||
"recipient_copy": "複製地址",
|
||||
"recipient_move_to": "移至{field}",
|
||||
@@ -820,6 +843,10 @@
|
||||
"ask_mobile": "詢問",
|
||||
"block_mobile": "封鎖"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "預留位置提醒",
|
||||
"description": "傳送仍含有預留位置(如 [TIME] 或 [您的姓名])、空白範本欄位或未完成文字(如 TODO 或 TBD)的郵件前請求確認。"
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "僅以純文字撰寫與閱讀。"
|
||||
},
|
||||
|
||||
@@ -292,6 +292,29 @@
|
||||
"no_sent_folder": "未找到“已发送”文件夹",
|
||||
"permission_camera": "需要相机权限才能拍照。",
|
||||
"permission_photos": "需要相册权限才能附加图片。",
|
||||
"placeholder_warning": {
|
||||
"title": "检查占位符?",
|
||||
"message": "你的邮件中有收件人会看到的占位符文本。要在发送前编辑吗?",
|
||||
"found": "找到的占位符",
|
||||
"more": "以及另外 {count} 个",
|
||||
"dont_ask_again": "不再提示",
|
||||
"send_anyway": "仍然发送",
|
||||
"back": "返回编辑"
|
||||
},
|
||||
"placeholder_words": {
|
||||
"before": [
|
||||
"您的-",
|
||||
"你的-",
|
||||
"贵公司-",
|
||||
"插入-",
|
||||
"输入-",
|
||||
"填写-",
|
||||
"添加-",
|
||||
"在此-"
|
||||
],
|
||||
"after": [],
|
||||
"unfinished": []
|
||||
},
|
||||
"recipient_add_contact": "添加到联系人",
|
||||
"recipient_copy": "复制地址",
|
||||
"recipient_move_to": "移至{field}",
|
||||
@@ -820,6 +843,10 @@
|
||||
"ask_mobile": "询问",
|
||||
"block_mobile": "阻止"
|
||||
},
|
||||
"placeholder_warning": {
|
||||
"label": "占位符提醒",
|
||||
"description": "发送仍含有占位符(如 [TIME] 或 [您的姓名])、空白模板字段或未完成文本(如 TODO 或 TBD)的邮件前请求确认。"
|
||||
},
|
||||
"plain_text_mode": {
|
||||
"description_mobile": "仅以纯文本撰写和阅读。"
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ export function ComposingSettings() {
|
||||
const signatureSeparatorEnabled = useSettingsStore((s) => s.signatureSeparatorEnabled);
|
||||
const requestReadReceiptDefault = useSettingsStore((s) => s.requestReadReceiptDefault);
|
||||
const emptySubjectWarningEnabled = useSettingsStore((s) => s.emptySubjectWarningEnabled);
|
||||
const placeholderWarningEnabled = useSettingsStore((s) => s.placeholderWarningEnabled);
|
||||
const autoSaveDraftInterval = useSettingsStore((s) => s.autoSaveDraftInterval);
|
||||
const subAddressDelimiter = useSettingsStore((s) => s.subAddressDelimiter);
|
||||
const updateSetting = useSettingsStore((s) => s.updateSetting);
|
||||
@@ -141,6 +142,13 @@ export function ComposingSettings() {
|
||||
<ToggleSwitch checked={emptySubjectWarningEnabled} onChange={(v) => updateSetting('emptySubjectWarningEnabled', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('settings.email_behavior.placeholder_warning.label', 'Placeholder warning')}
|
||||
description={t('settings.email_behavior.placeholder_warning.description', 'Ask for confirmation before sending a message that still has placeholders like [TIME] or [Your Name], template fields left empty, or unfinished text like TODO or TBD.')}
|
||||
>
|
||||
<ToggleSwitch checked={placeholderWarningEnabled} onChange={(v) => updateSetting('placeholderWarningEnabled', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<View style={styles.subBlock}>
|
||||
<Text style={styles.subLabel}>{t('settings.email_behavior.signature_position.label', "Signature Position")}</Text>
|
||||
<Text style={styles.subDescription}>
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SUPPORTED_LOCALES } from '../../i18n';
|
||||
import {
|
||||
findUnfilledPlaceholders, placeholderWordsFor, unfilledPlaceholdersInMessage, type PlaceholderWords,
|
||||
} from '../placeholder-check';
|
||||
import { SIGNATURE_RANGE_MARKER } from '../signature-utils';
|
||||
|
||||
// Ported from the webmail's lib/__tests__/placeholder-check.test.ts. The words
|
||||
// come from the RN overlays (locales/rn/<lang>.json) until the vendored
|
||||
// catalogs carry them.
|
||||
const locales = SUPPORTED_LOCALES.map((l) => l.code as string);
|
||||
|
||||
function wordsOf(locale: string): PlaceholderWords {
|
||||
return placeholderWordsFor(locale).at(-1)!;
|
||||
}
|
||||
|
||||
// What the composer passes: English and the UI language.
|
||||
const withUi = (locale: string) => placeholderWordsFor(locale);
|
||||
const EN = withUi('en');
|
||||
const ALL = locales.map(wordsOf);
|
||||
|
||||
describe('findUnfilledPlaceholders', () => {
|
||||
it('finds template placeholders in the subject and the body', () => {
|
||||
expect(findUnfilledPlaceholders('Follow-up: {{subject}}', 'Hi {{ recipient_name }},')).toEqual([
|
||||
'{{subject}}',
|
||||
'{{ recipient_name }}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('finds upper-case fill-ins in brackets', () => {
|
||||
expect(findUnfilledPlaceholders('', 'See you at [TIME]. It costs [PRICE] per [UNIT_NAME].'))
|
||||
.toEqual(['[TIME]', '[PRICE]', '[UNIT_NAME]']);
|
||||
expect(findUnfilledPlaceholders('', 'Best regards, [YOUR NAME]')).toEqual(['[YOUR NAME]']);
|
||||
expect(findUnfilledPlaceholders('', 'Treffpunkt: [ÜBERGABEORT]')).toEqual(['[ÜBERGABEORT]']);
|
||||
});
|
||||
|
||||
it('finds fill-ins that ask the sender to write something', () => {
|
||||
expect(findUnfilledPlaceholders('', 'Best regards,\n[Your Name]\n[Insert date]', EN)).toEqual([
|
||||
'[Your Name]',
|
||||
'[Insert date]',
|
||||
]);
|
||||
expect(findUnfilledPlaceholders('', 'Viele Grüße\n[Ihr Name], [Datum einfügen]', withUi('de'))).toEqual([
|
||||
'[Ihr Name]',
|
||||
'[Datum einfügen]',
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['ar', '[أدخل الاسم]'],
|
||||
['ca', '[El teu nom]'],
|
||||
['cs', '[Vaše jméno]'],
|
||||
['da', '[Dit navn]'],
|
||||
['de', '[Unterschrift eintragen]'],
|
||||
['es', '[Tu nombre]'],
|
||||
['es', '[Inserte la fecha]'],
|
||||
['fa', '[نام شما]'],
|
||||
['fr', '[Votre nom]'],
|
||||
['fr', '[Insérer la date]'],
|
||||
['he', '[השם שלך]'],
|
||||
['hu', '[Az Ön neve]'],
|
||||
['it', '[Il tuo nome]'],
|
||||
['it', '[Inserisci la data]'],
|
||||
['ja', '[あなたの名前]'],
|
||||
['ja', '[会社名を入力]'],
|
||||
['ko', '[귀하의 이름]'],
|
||||
['ko', '[날짜 입력]'],
|
||||
['lv', '[Jūsu vārds]'],
|
||||
['mn', '[Таны нэр]'],
|
||||
['nb', '[Ditt navn]'],
|
||||
['nl', '[Uw naam]'],
|
||||
['nl', '[Datum invullen]'],
|
||||
['pl', '[Twoje imię]'],
|
||||
['pt', '[Seu nome]'],
|
||||
['ro', '[Numele tău]'],
|
||||
['ru', '[Ваше имя]'],
|
||||
['ru', '[Укажите дату]'],
|
||||
['sk', '[Vaše meno]'],
|
||||
['tr', '[Adınız]'],
|
||||
['tr', '[Tarih ekleyin]'],
|
||||
['uk', '[Ваше ім’я]'],
|
||||
['zh', '[您的姓名]'],
|
||||
['zh', '【插入日期】'],
|
||||
['zh-TW', '[填寫公司名稱]'],
|
||||
])('finds a %s fill-in with that UI language: %s', (locale, fillIn) => {
|
||||
expect(findUnfilledPlaceholders('', `Text ${fillIn} text`, withUi(locale))).toEqual([fillIn]);
|
||||
});
|
||||
|
||||
it('checks only the languages it is given', () => {
|
||||
expect(findUnfilledPlaceholders('', 'Cordialement, [Votre nom]', withUi('de'))).toEqual([]);
|
||||
expect(findUnfilledPlaceholders('', 'Cordialement, [Votre nom]', withUi('fr'))).toEqual(['[Votre nom]']);
|
||||
});
|
||||
|
||||
it('finds English fill-ins whatever the UI language', () => {
|
||||
expect(findUnfilledPlaceholders('', 'Thanks, [Your Name]. TODO', withUi('ja'))).toEqual(['[Your Name]', 'TODO']);
|
||||
});
|
||||
|
||||
it('skips word lists that are not lists of strings', () => {
|
||||
const broken = [{ before: 'your' }, { before: [1, '', '-', null] }, null, { after: {} }] as unknown as PlaceholderWords[];
|
||||
expect(findUnfilledPlaceholders('', '[Your Name] [TIME]', broken)).toEqual(['[TIME]']);
|
||||
});
|
||||
|
||||
it('finds the circles that stand in for a name in Japanese and Korean', () => {
|
||||
expect(findUnfilledPlaceholders('', '〇〇様\nお世話になっております。○○○ 님')).toEqual(['〇〇', '○○○']);
|
||||
});
|
||||
|
||||
it('leaves bracketed words and headings in other languages alone', () => {
|
||||
const text = [
|
||||
'【重要】お知らせ 【通知】',
|
||||
'[Votre] [Ваше] [您的]',
|
||||
'Le [ton] juste. Vote [tu].',
|
||||
'Siehe [Anlage 2] und [Kapitel 3].',
|
||||
'Termin: ○ und ◯.',
|
||||
].join('\n');
|
||||
expect(findUnfilledPlaceholders('', text, ALL)).toEqual([]);
|
||||
});
|
||||
|
||||
it('finds unfinished-text markers', () => {
|
||||
expect(findUnfilledPlaceholders('', 'The venue is TBD. TODO: add the agenda. Room XXX', EN)).toEqual([
|
||||
'TBD',
|
||||
'TODO',
|
||||
'XXX',
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports a marker in brackets once', () => {
|
||||
expect(findUnfilledPlaceholders('', 'Agenda: [TODO]', EN)).toEqual(['[TODO]']);
|
||||
});
|
||||
|
||||
it('lists each placeholder once', () => {
|
||||
expect(findUnfilledPlaceholders('{{name}}', 'Hi {{name}}, at [TIME] or [TIME]?')).toEqual([
|
||||
'{{name}}',
|
||||
'[TIME]',
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves ordinary text alone', () => {
|
||||
const text = [
|
||||
'See footnote [1] and tick [x].',
|
||||
'Option [A] is fine, so is [3D] and [2026].',
|
||||
'I still have a todo list and the date is tba.',
|
||||
'Read [the docs](https://example.com) and [Your account](https://example.com/me).',
|
||||
'The mastodon handle is TODOS and the size is XXXL.',
|
||||
'Signed, [Yours truly].',
|
||||
'A {single} brace and {{}} are not placeholders.',
|
||||
].join('\n');
|
||||
expect(findUnfilledPlaceholders('', text, ALL)).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores bracket tags and markers in the subject', () => {
|
||||
expect(findUnfilledPlaceholders('Re: [EXTERNAL] Venue TBD', 'Thanks!', ALL)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('placeholder words in the locale files', () => {
|
||||
it.each(locales)('%s has lists of strings', (locale) => {
|
||||
const words = wordsOf(locale) as Record<string, unknown>;
|
||||
for (const key of ['before', 'after', 'unfinished']) {
|
||||
expect(Array.isArray(words[key]), `${locale}: ${key} is not a list`).toBe(true);
|
||||
for (const word of words[key] as unknown[]) {
|
||||
expect(typeof word === 'string' && word.replace(/-/g, '').trim() !== '', `${locale}: ${JSON.stringify(word)}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('unfilledPlaceholdersInMessage', () => {
|
||||
const signature = { name: 'Me', email: 'me@example.com', textSignature: 'Me\n[YOUR TITLE]', htmlSignature: '' };
|
||||
|
||||
it('leaves the embedded signature and the quoted original out', () => {
|
||||
const body = '<p>See you at [TIME].</p>'
|
||||
+ `<p ${SIGNATURE_RANGE_MARKER}="separator">-- </p><p>Me, [YOUR TITLE]</p><p ${SIGNATURE_RANGE_MARKER}="end"></p>`
|
||||
+ '<blockquote><p>Please confirm [DATE]</p></blockquote>';
|
||||
expect(unfilledPlaceholdersInMessage({
|
||||
subject: 'Re: hello', body, plainTextMode: false, signature, words: EN,
|
||||
})).toEqual(['[TIME]']);
|
||||
});
|
||||
|
||||
it('leaves the plain text signature and quoted lines out', () => {
|
||||
const body = 'See you at [TIME].\n\n-- \nMe\n[YOUR TITLE]\n\n> Please confirm [DATE]';
|
||||
expect(unfilledPlaceholdersInMessage({
|
||||
subject: '{{subject}}', body, plainTextMode: true, signature, words: EN,
|
||||
})).toEqual(['{{subject}}', '[TIME]']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
// Finds text the sender meant to replace before sending: template
|
||||
// placeholders, bracketed fill-ins that AI drafts and copied templates leave
|
||||
// behind, and markers like TODO. Port of the webmail's lib/placeholder-check.ts.
|
||||
|
||||
import { getDictionary, isSupportedLocale } from '../i18n';
|
||||
import { extractUserAuthoredText } from './compose-html';
|
||||
import { getPlainTextSignature, stripEmbeddedSignature, type SignatureSource } from './signature-utils';
|
||||
|
||||
// Template syntax, as in template-utils. Spaces inside the braces are allowed
|
||||
// because templates pasted from other tools often have them.
|
||||
const TEMPLATE_PLACEHOLDER = /\{\{\s*[\w.-]+\s*\}\}/g;
|
||||
|
||||
// [TIME], [PRICE], [YOUR NAME]. Not followed by "(", which would make it a
|
||||
// Markdown link.
|
||||
const CAPS_BRACKET = /\[[\p{Lu}\d][\p{Lu}\d _./-]{0,28}[\p{Lu}\d]\](?!\()/gu;
|
||||
|
||||
/**
|
||||
* The words that mark a fill-in in one language. They live in each locale
|
||||
* file under email_composer.placeholder_words, so a new translation brings
|
||||
* its own.
|
||||
*
|
||||
* - `before` opens a fill-in: "your" in [Your name], "insert" in [Insert date].
|
||||
* - `after` closes one, for languages that put the verb or "your" last:
|
||||
* "einfügen" in [Datum einfügen].
|
||||
* - A "-" on the joining side means the word is written onto the next one
|
||||
* without a space: "您的-" for [您的姓名], "-ınız" for [Adınız].
|
||||
* - `unfinished` are words in capitals that mark unfinished text, like TODO.
|
||||
* They match as written, so "todo" in a sentence does not count.
|
||||
*/
|
||||
export interface PlaceholderWords {
|
||||
before?: unknown;
|
||||
after?: unknown;
|
||||
unfinished?: unknown;
|
||||
}
|
||||
|
||||
const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const alternation = (words: string[]) => words.map(escapeRegExp).join('|');
|
||||
|
||||
// The lists come from locale files that contributors edit, so anything that
|
||||
// is not a list of strings is skipped.
|
||||
function wordsOf(lists: PlaceholderWords[], key: keyof PlaceholderWords): string[] {
|
||||
const words = new Set<string>();
|
||||
for (const list of lists) {
|
||||
const value = list?.[key];
|
||||
if (!Array.isArray(value)) continue;
|
||||
for (const word of value) {
|
||||
if (typeof word === 'string' && word.replace(/-/g, '').trim()) words.add(word.trim());
|
||||
}
|
||||
}
|
||||
return Array.from(words);
|
||||
}
|
||||
|
||||
// Plain brackets, and the full-width and lenticular ones of CJK text.
|
||||
const OPEN = '[\\[[【]';
|
||||
const CLOSE = '[\\]]】]';
|
||||
const INNER = '[^\\[\\][]【】\\n]';
|
||||
|
||||
function promptBracketPattern(lists: PlaceholderWords[]): RegExp | null {
|
||||
const before = wordsOf(lists, 'before');
|
||||
const after = wordsOf(lists, 'after');
|
||||
const branches: string[] = [];
|
||||
const add = (words: string[], branch: (alt: string) => string) => {
|
||||
if (words.length > 0) branches.push(branch(alternation(words)));
|
||||
};
|
||||
add(before.filter((w) => !w.endsWith('-')), (alt) => `(?:${alt})\\s${INNER}{1,40}`);
|
||||
add(before.filter((w) => w.endsWith('-')).map((w) => w.slice(0, -1)), (alt) => `(?:${alt})${INNER}{1,40}`);
|
||||
add(after.filter((w) => !w.startsWith('-')), (alt) => `${INNER}{1,40}\\s(?:${alt})`);
|
||||
add(after.filter((w) => w.startsWith('-')).map((w) => w.slice(1)), (alt) => `${INNER}{1,40}(?:${alt})`);
|
||||
if (branches.length === 0) return null;
|
||||
return new RegExp(`${OPEN}(?:${branches.join('|')})${CLOSE}(?!\\()`, 'giu');
|
||||
}
|
||||
|
||||
function unfinishedWordPattern(lists: PlaceholderWords[]): RegExp | null {
|
||||
const words = wordsOf(lists, 'unfinished');
|
||||
if (words.length === 0) return null;
|
||||
return new RegExp(`(?<![\\p{L}\\d_])(?:${alternation(words)})(?![\\p{L}\\d_])`, 'gu');
|
||||
}
|
||||
|
||||
// Japanese and Korean stand-ins for a name or number: 〇〇様, ○○○ 님. Signs
|
||||
// rather than words, so they are not in the locale files.
|
||||
const CIRCLE_MARKER = /[〇○◯]{2,}/gu;
|
||||
|
||||
interface Match {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function collect(text: string, patterns: RegExp[]): Match[] {
|
||||
const matches: Match[] = [];
|
||||
for (const pattern of patterns) {
|
||||
for (const m of text.matchAll(pattern)) {
|
||||
if (pattern === CAPS_BRACKET && (m[0].match(/\p{Lu}/gu)?.length ?? 0) < 2) continue;
|
||||
const start = m.index ?? 0;
|
||||
matches.push({ start, end: start + m[0].length, text: m[0] });
|
||||
}
|
||||
}
|
||||
// "[TODO]" is a bracket and a word; keep the outer match only.
|
||||
matches.sort((a, b) => a.start - b.start || b.end - a.end);
|
||||
const kept: Match[] = [];
|
||||
for (const match of matches) {
|
||||
const last = kept[kept.length - 1];
|
||||
if (last && match.start < last.end) continue;
|
||||
kept.push(match);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unfilled placeholders in a message, each once, subject first. The
|
||||
* subject is only checked for template placeholders: it often carries tags
|
||||
* like [EXTERNAL] or words from the original message, which the sender did
|
||||
* not write. `bodyText` should be the text the sender wrote, without quoted
|
||||
* replies and the signature. `words` are the fill-in words of the languages
|
||||
* to check (see PlaceholderWords).
|
||||
*/
|
||||
export function findUnfilledPlaceholders(subject: string, bodyText: string, words: PlaceholderWords[] = []): string[] {
|
||||
const bodyPatterns = [TEMPLATE_PLACEHOLDER, CAPS_BRACKET, CIRCLE_MARKER];
|
||||
const prompt = promptBracketPattern(words);
|
||||
const unfinished = unfinishedWordPattern(words);
|
||||
if (prompt) bodyPatterns.push(prompt);
|
||||
if (unfinished) bodyPatterns.push(unfinished);
|
||||
const found = [
|
||||
...collect(subject, [TEMPLATE_PLACEHOLDER]),
|
||||
...collect(bodyText, bodyPatterns),
|
||||
].map((m) => m.text);
|
||||
return Array.from(new Set(found));
|
||||
}
|
||||
|
||||
function placeholderWordsOf(dictionary: Record<string, unknown>): PlaceholderWords {
|
||||
const composer = dictionary.email_composer as Record<string, unknown> | undefined;
|
||||
const words = composer?.placeholder_words;
|
||||
return words && typeof words === 'object' ? (words as PlaceholderWords) : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The fill-in words to check: the UI language's and English, since AI drafts
|
||||
* and templates are often English whatever the UI language is.
|
||||
*/
|
||||
export function placeholderWordsFor(locale: string): PlaceholderWords[] {
|
||||
const lists = [placeholderWordsOf(getDictionary('en'))];
|
||||
if (locale !== 'en' && isSupportedLocale(locale)) lists.push(placeholderWordsOf(getDictionary(locale)));
|
||||
return lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unfilled placeholders of a message as the composer holds it: the body
|
||||
* without its signature (a placeholder there would stop every message) and
|
||||
* without the quoted original.
|
||||
*/
|
||||
export function unfilledPlaceholdersInMessage(message: {
|
||||
subject: string;
|
||||
body: string;
|
||||
plainTextMode: boolean;
|
||||
signature?: SignatureSource | null;
|
||||
forwardedSeparator?: string;
|
||||
words: PlaceholderWords[];
|
||||
}): string[] {
|
||||
let authored: string;
|
||||
if (message.plainTextMode) {
|
||||
const signature = getPlainTextSignature(message.signature ?? undefined);
|
||||
authored = signature ? message.body.split(signature).join('') : message.body;
|
||||
} else {
|
||||
authored = stripEmbeddedSignature(message.body);
|
||||
}
|
||||
const bodyText = extractUserAuthoredText(authored, {
|
||||
plainTextMode: message.plainTextMode,
|
||||
forwardedSeparator: message.forwardedSeparator,
|
||||
});
|
||||
return findUnfilledPlaceholders(message.subject, bodyText, message.words);
|
||||
}
|
||||
@@ -99,6 +99,7 @@ export const SETTINGS_SEARCH_PATHS: Record<SettingsTabId, string[]> = {
|
||||
'settings.email_behavior.plain_text_mode',
|
||||
'settings.email_behavior.request_read_receipt',
|
||||
'settings.email_behavior.empty_subject_warning',
|
||||
'settings.email_behavior.placeholder_warning',
|
||||
'settings.email_behavior.signature_separator',
|
||||
'settings.email_behavior.signature_position',
|
||||
'settings.email_behavior.send_delay',
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
findComposeIdentityId, findDraftIdentityId, resolveComposeAccountEmail, resolveReplyIdentity,
|
||||
} from '../lib/reply-identity';
|
||||
import { shouldBlockEditorRemoteImages } from '../lib/editor-html';
|
||||
import { placeholderWordsFor, unfilledPlaceholdersInMessage } from '../lib/placeholder-check';
|
||||
import {
|
||||
ComposerLoginMovedError, loginMovedFrom, uploadBytesForLogin, uploadFileForLogin,
|
||||
} from '../lib/compose-login';
|
||||
@@ -482,6 +483,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
const signatureSeparatorEnabled = useSettingsStore((s) => s.signatureSeparatorEnabled);
|
||||
const requestReadReceiptDefault = useSettingsStore((s) => s.requestReadReceiptDefault);
|
||||
const emptySubjectWarningEnabled = useSettingsStore((s) => s.emptySubjectWarningEnabled);
|
||||
const placeholderWarningEnabled = useSettingsStore((s) => s.placeholderWarningEnabled);
|
||||
const autoSaveDraftInterval = useSettingsStore((s) => s.autoSaveDraftInterval);
|
||||
const subAddressDelimiter = useSettingsStore((s) => s.subAddressDelimiter);
|
||||
const preferredIdentityIds = useSettingsStore((s) => s.preferredIdentityIds);
|
||||
@@ -2049,6 +2051,54 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
});
|
||||
};
|
||||
|
||||
// Unfilled placeholders: template fields left as {{name}}, fill-ins like
|
||||
// [TIME] from AI drafts, and markers like TODO (webmail
|
||||
// lib/placeholder-check). The signature is left out, or a placeholder in
|
||||
// it would stop every message. Asked once per placeholder: after "Send
|
||||
// anyway" and a trip back to add an attachment, the same ones pass.
|
||||
const confirmedPlaceholdersRef = React.useRef(new Set<string>());
|
||||
const passesPlaceholderCheck = (body: string): Promise<boolean> => {
|
||||
if (!placeholderWarningEnabled) return Promise.resolve(true);
|
||||
const found = unfilledPlaceholdersInMessage({
|
||||
subject,
|
||||
body,
|
||||
plainTextMode,
|
||||
signature: signatureIdentity,
|
||||
forwardedSeparator: quoteLabels.forwardedSeparator,
|
||||
words: placeholderWordsFor(locale),
|
||||
});
|
||||
if (!found.some((p) => !confirmedPlaceholdersRef.current.has(p))) return Promise.resolve(true);
|
||||
const SHOWN = 5;
|
||||
const more = found.length > SHOWN
|
||||
? ` ${t('email_composer.placeholder_warning.more', 'and {count} more', { count: found.length - SHOWN })}`
|
||||
: '';
|
||||
const message = t(
|
||||
'email_composer.placeholder_warning.message',
|
||||
'Your message has placeholder text that recipients will see. Do you want to edit it before sending?',
|
||||
);
|
||||
const label = t('email_composer.placeholder_warning.found', 'Placeholders found');
|
||||
return new Promise<boolean>((resolve) => {
|
||||
Alert.alert(
|
||||
t('email_composer.placeholder_warning.title', 'Check placeholders?'),
|
||||
`${message}\n\n${label}: ${found.slice(0, SHOWN).join(', ')}${more}`,
|
||||
[
|
||||
{ text: t('email_composer.placeholder_warning.back', 'Back to editing'), style: 'cancel', onPress: () => resolve(false) },
|
||||
{
|
||||
text: t('email_composer.placeholder_warning.dont_ask_again', "Don't ask again"),
|
||||
onPress: () => { updateSetting('placeholderWarningEnabled', false); resolve(true); },
|
||||
},
|
||||
{
|
||||
text: t('email_composer.placeholder_warning.send_anyway', 'Send anyway'),
|
||||
onPress: () => {
|
||||
for (const p of found) confirmedPlaceholdersRef.current.add(p);
|
||||
resolve(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Translate an absolute "send at" time into the HOLDFOR seconds the server
|
||||
// expects, clamping to what it actually supports. Returns null (and alerts)
|
||||
// when scheduling isn't possible so the caller can abort.
|
||||
@@ -2198,6 +2248,7 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
}
|
||||
|
||||
if (!(await passesEmptySubjectCheck())) return;
|
||||
if (!(await passesPlaceholderCheck(plainTextMode ? plainBody : liveBodyHtml))) return;
|
||||
if (!(await passesAttachmentReminder(plainTextMode ? plainBody : liveBodyHtml))) return;
|
||||
|
||||
// Autosave must not race the send: cancel a scheduled save and let an
|
||||
|
||||
@@ -173,6 +173,8 @@ interface PersistedSettings {
|
||||
requestReadReceiptDefault: boolean;
|
||||
// Confirm before sending a message without a subject (#684).
|
||||
emptySubjectWarningEnabled: boolean;
|
||||
// Confirm before sending a message with unfilled placeholders like [TIME] or TODO.
|
||||
placeholderWarningEnabled: boolean;
|
||||
// Draft autosave debounce, milliseconds.
|
||||
autoSaveDraftInterval: number;
|
||||
// Character separating user from tag (e.g. "user+tag@"), RFC 5233.
|
||||
@@ -362,6 +364,7 @@ const DEFAULT_PERSISTED: PersistedSettings = {
|
||||
signatureSeparatorEnabled: true,
|
||||
requestReadReceiptDefault: false,
|
||||
emptySubjectWarningEnabled: true,
|
||||
placeholderWarningEnabled: true,
|
||||
autoSaveDraftInterval: 60000,
|
||||
subAddressDelimiter: '+',
|
||||
preferredIdentityIds: {},
|
||||
|
||||
Reference in New Issue
Block a user