From be5fc12622f9e91faffa93a33c9d57a8d7ae24e9 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:03:41 +0100 Subject: [PATCH] feat: let each translation list its own placeholder fill-in words --- .../__tests__/placeholder-warning.test.tsx | 47 ++++++ components/email/email-composer.tsx | 8 +- lib/__tests__/placeholder-check.test.ts | 61 ++++++-- lib/placeholder-check.ts | 145 ++++++++---------- locales/ar/common.json | 5 + locales/ca/common.json | 5 + locales/cs/common.json | 5 + locales/da/common.json | 5 + locales/de/common.json | 5 + locales/en/common.json | 5 + locales/es/common.json | 5 + locales/et/common.json | 5 + locales/fa/common.json | 5 + locales/fr/common.json | 5 + locales/he/common.json | 5 + locales/hu/common.json | 5 + locales/it/common.json | 5 + locales/ja/common.json | 5 + locales/ko/common.json | 5 + locales/lv/common.json | 5 + locales/mn/common.json | 5 + locales/nb/common.json | 5 + locales/nl/common.json | 5 + locales/pl/common.json | 5 + locales/pt/common.json | 5 + locales/ro/common.json | 5 + locales/ru/common.json | 5 + locales/sk/common.json | 5 + locales/tr/common.json | 5 + locales/uk/common.json | 5 + locales/zh-TW/common.json | 5 + locales/zh/common.json | 5 + 32 files changed, 307 insertions(+), 94 deletions(-) diff --git a/components/email/__tests__/placeholder-warning.test.tsx b/components/email/__tests__/placeholder-warning.test.tsx index 0cda741ea..d73fc79c9 100644 --- a/components/email/__tests__/placeholder-warning.test.tsx +++ b/components/email/__tests__/placeholder-warning.test.tsx @@ -4,6 +4,29 @@ import React from 'react'; import { EmailComposer } from '../email-composer'; import { useSettingsStore } from '@/stores/settings-store'; +// The UI language whose fill-in words t.raw hands the composer. +const ui = vi.hoisted(() => ({ locale: 'en' })); + +vi.mock('next-intl', async () => { + const fs = await import('fs'); + const path = await import('path'); + const messages = (locale: string) => + JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../../locales', locale, 'common.json'), 'utf-8')); + return { + useTranslations: (namespace?: string) => { + const t = (key: string) => key; + t.raw = (key: string) => (namespace ? messages(ui.locale)[namespace] : messages(ui.locale))[key]; + return t; + }, + useLocale: () => ui.locale, + useFormatter: () => ({ + dateTime: (d: Date | string) => String(d), + relativeTime: (d: Date | string) => String(d), + number: (n: number) => String(n), + }), + }; +}); + // ─── Heavy component mocks (mirrors empty-subject-warning.test.tsx) ────────── vi.mock('@/components/email/rich-text-editor', () => ({ @@ -207,6 +230,7 @@ describe('composer placeholder warning', () => { }); afterEach(() => { + ui.locale = 'en'; useSettingsStore.setState({ placeholderWarningEnabled: true, attachmentReminderEnabled: false, @@ -319,6 +343,29 @@ describe('composer placeholder warning', () => { expect(screen.queryByText('placeholder_warning.title')).not.toBeInTheDocument(); }); + it('uses the fill-in words of the UI language', async () => { + ui.locale = 'de'; + const onSend = vi.fn(); + render(Viele Grüße, [Ihr Name]

' }} onSend={onSend} />); + + fireEvent.click(sendButton()); + + const dialog = await screen.findByRole('alertdialog'); + expect(within(dialog).getByText('[Ihr Name]')).toBeInTheDocument(); + expect(onSend).not.toHaveBeenCalled(); + }); + + it('keeps the English fill-in words in another UI language', async () => { + ui.locale = 'de'; + const onSend = vi.fn(); + render(Thanks, [Your Name]

' }} onSend={onSend} />); + + fireEvent.click(sendButton()); + + const dialog = await screen.findByRole('alertdialog'); + expect(within(dialog).getByText('[Your Name]')).toBeInTheDocument(); + }); + it('sends without asking when the warning is off', async () => { useSettingsStore.setState({ placeholderWarningEnabled: false } as never); const onSend = vi.fn(); diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 1b538ec45..409d942ad 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -90,6 +90,7 @@ import { SubAddressHelper } from "@/components/identity/sub-address-helper"; import { generateSubAddress } from "@/lib/sub-addressing"; import { substitutePlaceholders, spliceTemplateAboveSignature, composeBodyHasUserContent } from "@/lib/template-utils"; import { findUnfilledPlaceholders, stripEmbeddedSignatureHtml } from "@/lib/placeholder-check"; +import enMessages from "@/locales/en/common.json"; import { TemplatePicker } from "@/components/templates/template-picker"; import { TemplateForm } from "@/components/templates/template-form"; import type { EmailTemplate } from "@/lib/template-types"; @@ -3237,7 +3238,12 @@ export function EmailComposer({ plainTextMode, forwardedSeparator: tQuote('forwarded_separator'), }); - const found = findUnfilledPlaceholders(subject, bodyText); + // The fill-in words of the UI language, and of English: AI drafts and + // templates are often English whatever the UI language is. + const found = findUnfilledPlaceholders(subject, bodyText, [ + enMessages.email_composer.placeholder_words, + t.raw('placeholder_words'), + ]); if (found.some(p => !confirmedPlaceholdersRef.current.has(p))) { setPlaceholderWarningMatches(found); setPlaceholderDontAskAgain(false); diff --git a/lib/__tests__/placeholder-check.test.ts b/lib/__tests__/placeholder-check.test.ts index 6d205af98..9e3d52e3a 100644 --- a/lib/__tests__/placeholder-check.test.ts +++ b/lib/__tests__/placeholder-check.test.ts @@ -1,5 +1,20 @@ +import fs from 'fs'; +import path from 'path'; import { describe, it, expect } from 'vitest'; -import { findUnfilledPlaceholders, stripEmbeddedSignatureHtml } from '../placeholder-check'; +import { findUnfilledPlaceholders, stripEmbeddedSignatureHtml, type PlaceholderWords } from '../placeholder-check'; + +const localesDir = path.resolve(__dirname, '../../locales'); +const locales = fs.readdirSync(localesDir).filter((l) => fs.existsSync(path.join(localesDir, l, 'common.json'))); + +function wordsOf(locale: string): PlaceholderWords { + const messages = JSON.parse(fs.readFileSync(path.join(localesDir, locale, 'common.json'), 'utf-8')); + return messages.email_composer.placeholder_words; +} + +// What the composer passes: English and the UI language. +const withUi = (locale: string) => [wordsOf('en'), wordsOf(locale)]; +const EN = withUi('en'); +const ALL = locales.map(wordsOf); describe('findUnfilledPlaceholders', () => { it('finds template placeholders in the subject and the body', () => { @@ -17,11 +32,11 @@ describe('findUnfilledPlaceholders', () => { }); it('finds fill-ins that ask the sender to write something', () => { - expect(findUnfilledPlaceholders('', 'Best regards,\n[Your Name]\n[Insert date]')).toEqual([ + 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]')).toEqual([ + expect(findUnfilledPlaceholders('', 'Viele Grüße\n[Ihr Name], [Datum einfügen]', withUi('de'))).toEqual([ '[Ihr Name]', '[Datum einfügen]', ]); @@ -64,8 +79,22 @@ describe('findUnfilledPlaceholders', () => { ['zh', '[您的姓名]'], ['zh', '【插入日期】'], ['zh-TW', '[填寫公司名稱]'], - ])('finds a %s fill-in: %s', (_locale, fillIn) => { - expect(findUnfilledPlaceholders('', `Text ${fillIn} text`)).toEqual([fillIn]); + ])('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', () => { @@ -80,11 +109,11 @@ describe('findUnfilledPlaceholders', () => { 'Siehe [Anlage 2] und [Kapitel 3].', 'Termin: ○ und ◯.', ].join('\n'); - expect(findUnfilledPlaceholders('', text)).toEqual([]); + expect(findUnfilledPlaceholders('', text, ALL)).toEqual([]); }); it('finds unfinished-text markers', () => { - expect(findUnfilledPlaceholders('', 'The venue is TBD. TODO: add the agenda. Room XXX')).toEqual([ + expect(findUnfilledPlaceholders('', 'The venue is TBD. TODO: add the agenda. Room XXX', EN)).toEqual([ 'TBD', 'TODO', 'XXX', @@ -92,7 +121,7 @@ describe('findUnfilledPlaceholders', () => { }); it('reports a marker in brackets once', () => { - expect(findUnfilledPlaceholders('', 'Agenda: [TODO]')).toEqual(['[TODO]']); + expect(findUnfilledPlaceholders('', 'Agenda: [TODO]', EN)).toEqual(['[TODO]']); }); it('lists each placeholder once', () => { @@ -112,11 +141,23 @@ describe('findUnfilledPlaceholders', () => { 'Signed, [Yours truly].', 'A {single} brace and {{}} are not placeholders.', ].join('\n'); - expect(findUnfilledPlaceholders('', text)).toEqual([]); + expect(findUnfilledPlaceholders('', text, ALL)).toEqual([]); }); it('ignores bracket tags and markers in the subject', () => { - expect(findUnfilledPlaceholders('Re: [EXTERNAL] Venue TBD', 'Thanks!')).toEqual([]); + 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; + 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); + } + } }); }); diff --git a/lib/placeholder-check.ts b/lib/placeholder-check.ts index 0b78d6efa..a428ef62a 100644 --- a/lib/placeholder-check.ts +++ b/lib/placeholder-check.ts @@ -10,97 +10,70 @@ const TEMPLATE_PLACEHOLDER = /\{\{\s*[\w.-]+\s*\}\}/g; // Markdown link. const CAPS_BRACKET = /\[[\p{Lu}\d][\p{Lu}\d _./-]{0,28}[\p{Lu}\d]\](?!\()/gu; -// Fill-ins that ask the sender for something: [Your Name], [Insert date], -// [Votre nom], [Ваше имя], [您的姓名]. Matched by the word that opens or -// closes them, for the languages the app is translated into. "Your" and -// "insert/enter/add" are what AI drafts and templates use. -const PROMPT_START_WORDS = [ - // English - 'your', 'insert', 'enter', 'add', - // German - 'ihr', 'ihre', 'ihren', 'dein', 'deine', 'deinen', - // French - 'votre', 'vos', 'ton', 'ta', 'tes', 'insérer', 'insérez', 'ajouter', 'ajoutez', 'saisir', 'saisissez', 'indiquer', 'indiquez', - // Spanish - 'tu', 'tus', 'su', 'sus', 'insertar', 'inserte', 'inserta', 'añadir', 'añade', 'agregar', 'agrega', 'escribe', 'escriba', 'indica', 'indique', - // Portuguese - 'seu', 'sua', 'seus', 'suas', 'teu', 'tua', 'inserir', 'insira', 'adicionar', 'adicione', 'digite', 'informe', - // Italian - 'il tuo', 'la tua', 'i tuoi', 'le tue', 'il suo', 'la sua', 'inserisci', 'inserire', 'aggiungi', 'aggiungere', 'indica', - // Catalan - 'el teu', 'la teva', 'el vostre', 'la vostra', 'insereix', 'inseriu', 'afegeix', 'afegiu', 'escriviu', - // Dutch - 'je', 'jouw', 'uw', 'voeg', 'vul', - // Danish, Norwegian - 'din', 'dit', 'ditt', 'dine', 'indsæt', 'tilføj', 'sett inn', 'legg til', 'skriv', - // Polish - 'twoje', 'twój', 'twoja', 'pana', 'pani', 'wstaw', 'wpisz', 'dodaj', 'podaj', - // Czech, Slovak - 'vaše', 'váš', 'vaši', 'vaša', 'tvoje', 'tvůj', 'vložte', 'doplňte', 'zadejte', 'zadajte', 'uveďte', - // Russian, Ukrainian - 'ваше', 'ваш', 'ваша', 'ваши', 'ваші', 'твоё', 'твое', 'твой', 'твоя', 'твоє', 'твій', - 'вставьте', 'укажите', 'введите', 'добавьте', 'вставте', 'вкажіть', 'введіть', 'додайте', - // Romanian - 'introduceți', 'inserați', 'adăugați', 'completați', - // Hungarian - 'az ön', 'a te', 'írja be', 'illessze be', 'adja meg', - // Estonian, Latvian - 'teie', 'sinu', 'sisesta', 'sisestage', 'lisa', 'lisage', - 'jūsu', 'tavs', 'tava', 'ievadiet', 'ierakstiet', 'ievietojiet', 'pievienojiet', 'norādiet', - // Turkish - 'şirketinizin', 'buraya', - // Mongolian - 'таны', 'чиний', - // Korean - '귀하의', '당신의', '고객님의', - // Arabic, Hebrew - 'أدخل', 'ادخل', 'أضف', 'اكتب', 'הכנס', 'הכניסו', 'הזן', 'הוסף', -]; - -// Languages that put the verb or "your" last: [Datum einfügen], -// [Numele tău], [נושא שלך], [نام شما], [이름 입력]. -const PROMPT_END_WORDS = [ - 'einfügen', 'eintragen', 'ergänzen', - 'invoegen', 'invullen', 'toevoegen', - 'tău', 'ta', 'dvs.', 'dumneavoastră', - 'ekleyin', 'girin', 'yazın', - 'оруулна уу', - 'שלך', 'שלכם', - 'شما', 'وارد کنید', 'اضافه کنید', - '입력', '삽입', '추가', -]; - -// Japanese and Chinese have no spaces between words: [あなたの名前], -// [名前を入力], [您的姓名], [插入日期]. -const PROMPT_START_UNSPACED = ['あなたの', '貴社の', '御社の', '您的', '你的', '贵公司', '貴公司', '插入', '输入', '輸入', '填写', '填寫', '添加', '新增', '在此']; -const PROMPT_END_UNSPACED = ['を入力', 'を記入', 'を挿入', 'を追加', 'を入れる']; - -// Turkish possessive "your": [Adınız], [Şirket adınız]. -const TURKISH_YOUR = '\\p{L}*[ıiuü]n[ıiuü]z'; +/** + * 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(); + 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]'; -const PROMPT_BRACKET = new RegExp( - `${OPEN}(?:` + [ - `(?:${alternation(PROMPT_START_WORDS)})\\s${INNER}{1,40}`, - `${INNER}{1,40}\\s(?:${alternation(PROMPT_END_WORDS)})`, - `(?:${alternation(PROMPT_START_UNSPACED)})${INNER}{1,40}`, - `${INNER}{1,40}(?:${alternation(PROMPT_END_UNSPACED)})`, - `(?:${INNER}{0,40}\\s)?${TURKISH_YOUR}`, - ].join('|') + `)${CLOSE}(?!\\()`, - 'giu', -); +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'); +} -// Upper case only: "todo" or "tba" in a sentence is usually meant. -const UNFINISHED_WORD = /(? m.text); return Array.from(new Set(found)); } diff --git a/locales/ar/common.json b/locales/ar/common.json index 5b78532fb..20ffb5739 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -810,6 +810,11 @@ "send_anyway": "الإرسال على أي حال", "back": "العودة إلى التعديل" }, + "placeholder_words": { + "before": ["أدخل", "ادخل", "أضف", "اكتب"], + "after": [], + "unfinished": [] + }, "schedule_send": "جدولة الإرسال", "schedule_send_description": "اختر متى يجب على الخادم إرسال هذه الرسالة.", "schedule_preset": { diff --git a/locales/ca/common.json b/locales/ca/common.json index f5eacf827..3351973ef 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -810,6 +810,11 @@ "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": [] + }, "schedule_send": "Programa l'enviament", "schedule_send_description": "Trieu quan ha d'alliberar el servidor aquest missatge.", "schedule_preset": { diff --git a/locales/cs/common.json b/locales/cs/common.json index dc4f120db..30dd9973e 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -807,6 +807,11 @@ "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": [] + }, "add_link": "Přidat odkaz", "link_url_prompt": "Zadejte URL", "sending": "Odesílání...", diff --git a/locales/da/common.json b/locales/da/common.json index 9eba96c6a..be6b29715 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -809,6 +809,11 @@ "send_anyway": "Send alligevel", "back": "Tilbage til redigering" }, + "placeholder_words": { + "before": ["din", "dit", "dine", "indsæt", "tilføj", "skriv"], + "after": [], + "unfinished": [] + }, "link_url_prompt": "Indtast URL'en", "schedule_send": "Planlæg afsendelse", "schedule_send_description": "Vælg hvornår serveren skal frigive denne besked.", diff --git a/locales/de/common.json b/locales/de/common.json index 6f104c905..a1bce6f33 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -807,6 +807,11 @@ "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": [] + }, "add_link": "Link hinzufügen", "link_url_prompt": "URL eingeben", "sending": "Wird gesendet...", diff --git a/locales/en/common.json b/locales/en/common.json index a2e7d98b7..7bb0bd233 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -812,6 +812,11 @@ "send_anyway": "Send anyway", "back": "Back to editing" }, + "placeholder_words": { + "before": ["your", "insert", "enter", "add"], + "after": [], + "unfinished": ["TODO", "TBD", "TBA", "TBC", "FIXME", "XXX"] + }, "schedule_send": "Schedule send", "schedule_send_description": "Choose when the server should release this message.", "schedule_preset": { diff --git a/locales/es/common.json b/locales/es/common.json index 34fa4f082..24c6ef43f 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -807,6 +807,11 @@ "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": [] + }, "add_link": "Añadir enlace", "link_url_prompt": "Introduce la URL", "sending": "Enviando...", diff --git a/locales/et/common.json b/locales/et/common.json index a05cf6d40..8169d98ce 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -810,6 +810,11 @@ "send_anyway": "Saada ikkagi", "back": "Tagasi muutmise juurde" }, + "placeholder_words": { + "before": ["teie", "sinu", "sisesta", "sisestage", "lisa", "lisage"], + "after": [], + "unfinished": [] + }, "schedule_send": "Ajasta saatmine", "schedule_send_description": "Vali, millal server peaks selle sõnumi välja saatma.", "schedule_preset": { diff --git a/locales/fa/common.json b/locales/fa/common.json index efcd98c12..2cf8f0db4 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -810,6 +810,11 @@ "send_anyway": "ارسال به هر حال", "back": "بازگشت به ویرایش" }, + "placeholder_words": { + "before": [], + "after": ["شما", "وارد کنید", "اضافه کنید"], + "unfinished": [] + }, "schedule_send": "زمان‌بندی ارسال", "schedule_send_description": "زمان انتشار پیام توسط سرور را انتخاب کنید.", "schedule_preset": { diff --git a/locales/fr/common.json b/locales/fr/common.json index 0ed2bf3fe..6b32667b3 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -807,6 +807,11 @@ "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": [] + }, "add_link": "Ajouter un lien", "link_url_prompt": "Saisissez l'URL", "sending": "Envoi...", diff --git a/locales/he/common.json b/locales/he/common.json index c57fa817e..11e706f09 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -775,6 +775,11 @@ "send_anyway": "שלח בכל זאת", "back": "חזור לעריכה" }, + "placeholder_words": { + "before": ["הכנס", "הכניסו", "הזן", "הוסף"], + "after": ["שלך", "שלכם"], + "unfinished": [] + }, "schedule_send": "תזמן שליחה", "schedule_send_description": "בחר מתי השרת צריך לשחרר הודעה זו.", "schedule_preset": { diff --git a/locales/hu/common.json b/locales/hu/common.json index c0ff83e60..fe474d12e 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -810,6 +810,11 @@ "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": [] + }, "schedule_send": "Késleltetett küldés", "schedule_send_description": "Válaszd ki, mikor küldje el a szerver az üzenetet.", "schedule_preset": { diff --git a/locales/it/common.json b/locales/it/common.json index 210261726..0f8fa996c 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -807,6 +807,11 @@ "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": [] + }, "add_link": "Aggiungi link", "link_url_prompt": "Inserisci l'URL", "sending": "Invio in corso...", diff --git a/locales/ja/common.json b/locales/ja/common.json index 690cff5f1..c07fbdfb2 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -807,6 +807,11 @@ "send_anyway": "そのまま送信", "back": "編集に戻る" }, + "placeholder_words": { + "before": ["あなたの-", "貴社の-", "御社の-"], + "after": ["-を入力", "-を記入", "-を挿入", "-を追加", "-を入れる"], + "unfinished": [] + }, "add_link": "リンクを追加", "link_url_prompt": "URLを入力してください", "sending": "送信中...", diff --git a/locales/ko/common.json b/locales/ko/common.json index 418e19f3f..1ec0a9a80 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -807,6 +807,11 @@ "send_anyway": "그래도 보내기", "back": "편집으로 돌아가기" }, + "placeholder_words": { + "before": ["귀하의", "당신의", "고객님의"], + "after": ["입력", "삽입", "추가"], + "unfinished": [] + }, "add_link": "링크 추가", "link_url_prompt": "URL을 입력하세요", "sending": "전송 중...", diff --git a/locales/lv/common.json b/locales/lv/common.json index 6a7b554a5..4b06f6852 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -807,6 +807,11 @@ "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": [] + }, "add_link": "Pievienot saiti", "link_url_prompt": "Ievadiet URL", "sending": "Sūta...", diff --git a/locales/mn/common.json b/locales/mn/common.json index d76788bf6..cad57666e 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -810,6 +810,11 @@ "send_anyway": "Ямар ч байсан илгээнэ үү", "back": "Засварлах руу буцах" }, + "placeholder_words": { + "before": ["таны", "чиний"], + "after": ["оруулна уу"], + "unfinished": [] + }, "schedule_send": "Хуваарь илгээх", "schedule_send_description": "Сервер энэ мессежийг хэзээ гаргахыг сонгоно уу.", "schedule_preset": { diff --git a/locales/nb/common.json b/locales/nb/common.json index 216585cc9..3cdfc2b38 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -810,6 +810,11 @@ "send_anyway": "Send likevel", "back": "Tilbake til redigering" }, + "placeholder_words": { + "before": ["din", "ditt", "dine", "sett inn", "legg til", "skriv"], + "after": [], + "unfinished": [] + }, "schedule_send": "Planlegg sending", "schedule_send_description": "Velg når serveren skal sende denne meldingen.", "schedule_preset": { diff --git a/locales/nl/common.json b/locales/nl/common.json index 4b21d06b2..1875c6628 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -807,6 +807,11 @@ "send_anyway": "Toch verzenden", "back": "Terug naar bewerken" }, + "placeholder_words": { + "before": ["je", "jouw", "uw", "voeg", "vul"], + "after": ["invoegen", "invullen", "toevoegen"], + "unfinished": [] + }, "add_link": "Link toevoegen", "link_url_prompt": "Voer de URL in", "sending": "Bezig met verzenden...", diff --git a/locales/pl/common.json b/locales/pl/common.json index 3ff68ab0d..2f6f33bff 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -807,6 +807,11 @@ "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": [] + }, "add_link": "Dodaj link", "link_url_prompt": "Wprowadź adres URL", "sending": "Wysyłanie...", diff --git a/locales/pt/common.json b/locales/pt/common.json index 44dbc0644..ea29f35ec 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -807,6 +807,11 @@ "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": [] + }, "add_link": "Adicionar link", "link_url_prompt": "Insira a URL", "sending": "Enviando...", diff --git a/locales/ro/common.json b/locales/ro/common.json index 5714b10a7..7db304194 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -810,6 +810,11 @@ "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": [] + }, "schedule_send": "Programează trimiterea", "schedule_send_description": "Alegeți momentul în care serverul ar trebui să transmită acest mesaj.", "schedule_preset": { diff --git a/locales/ru/common.json b/locales/ru/common.json index 9c4ddb903..8ab2e7114 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -807,6 +807,11 @@ "send_anyway": "Отправить всё равно", "back": "Вернуться к редактированию" }, + "placeholder_words": { + "before": ["ваше", "ваш", "ваша", "ваши", "твоё", "твое", "твой", "твоя", "вставьте", "укажите", "введите", "добавьте"], + "after": [], + "unfinished": [] + }, "add_link": "Добавить ссылку", "link_url_prompt": "Введите URL", "sending": "Отправка...", diff --git a/locales/sk/common.json b/locales/sk/common.json index 7fa7346a3..f15bfddd3 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -810,6 +810,11 @@ "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": [] + }, "schedule_send": "Naplánovať odoslanie", "schedule_send_description": "Vyberte, kedy má server túto správu odoslať.", "schedule_preset": { diff --git a/locales/tr/common.json b/locales/tr/common.json index 50c8d8f59..30ed3616c 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -810,6 +810,11 @@ "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": [] + }, "schedule_send": "Göndermeyi zamanla", "schedule_send_description": "Sunucunun bu iletiyi ne zaman göndereceğini seçin.", "schedule_preset": { diff --git a/locales/uk/common.json b/locales/uk/common.json index acffee115..273e3c804 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -807,6 +807,11 @@ "send_anyway": "Все одно надішліть", "back": "Назад до редагування" }, + "placeholder_words": { + "before": ["ваше", "ваш", "ваша", "ваші", "твоє", "твій", "твоя", "вставте", "вкажіть", "введіть", "додайте"], + "after": [], + "unfinished": [] + }, "add_link": "Додати посилання", "link_url_prompt": "Введіть URL", "sending": "Надсилання...", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 226070e7e..9749d48ad 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -810,6 +810,11 @@ "send_anyway": "仍然傳送", "back": "繼續編輯" }, + "placeholder_words": { + "before": ["您的-", "你的-", "貴公司-", "插入-", "輸入-", "填寫-", "新增-", "在此-"], + "after": [], + "unfinished": [] + }, "schedule_send": "排程寄送", "schedule_send_description": "選擇伺服器要在何時送出這封郵件。", "schedule_preset": { diff --git a/locales/zh/common.json b/locales/zh/common.json index df5122617..9267dc715 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -807,6 +807,11 @@ "send_anyway": "仍然发送", "back": "返回编辑" }, + "placeholder_words": { + "before": ["您的-", "你的-", "贵公司-", "插入-", "输入-", "填写-", "添加-", "在此-"], + "after": [], + "unfinished": [] + }, "add_link": "添加链接", "link_url_prompt": "输入 URL", "sending": "发送中...",