From 4ec1c128f6d45cc8bd426486433b074fe2829ff6 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 06:36:09 +0100 Subject: [PATCH] fix: warn about template fill-ins like [Name] before sending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A lone capitalised word in brackets now counts as a fill-in, so two cases of ordinary text changed: the cross-language case uses lowercase "[votre] [ваше]", and the broken word-list case uses "[Your name]", so both still test what they were written for. --- lib/__tests__/placeholder-check.test.ts | 20 ++++++++++++++++++-- lib/placeholder-check.ts | 7 ++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/__tests__/placeholder-check.test.ts b/lib/__tests__/placeholder-check.test.ts index 9e3d52e3a..8cd351235 100644 --- a/lib/__tests__/placeholder-check.test.ts +++ b/lib/__tests__/placeholder-check.test.ts @@ -31,6 +31,22 @@ describe('findUnfilledPlaceholders', () => { expect(findUnfilledPlaceholders('', 'Treffpunkt: [ÜBERGABEORT]')).toEqual(['[ÜBERGABEORT]']); }); + // A template's "Hi [Name]" went out unchanged: only capitals and + // "your/insert" prompts counted. + it('finds capitalised words in brackets that a template left to fill in', () => { + expect(findUnfilledPlaceholders('', ['Hi [Name],', '[Step] is done. Regards, [First Name] at [Company]. [Date]'].join('\n'), EN)).toEqual([ + '[Name]', + '[Step]', + '[First Name]', + '[Company]', + '[Date]', + ]); + }); + + it('leaves longer bracketed phrases and links alone', () => { + expect(findUnfilledPlaceholders('', 'See [Chapter Four Part Two] and [Name](https://example.com).', EN)).toEqual([]); + }); + 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]', @@ -94,7 +110,7 @@ describe('findUnfilledPlaceholders', () => { 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]']); + expect(findUnfilledPlaceholders('', '[Your name] [TIME]', broken)).toEqual(['[TIME]']); }); it('finds the circles that stand in for a name in Japanese and Korean', () => { @@ -104,7 +120,7 @@ describe('findUnfilledPlaceholders', () => { it('leaves bracketed words and headings in other languages alone', () => { const text = [ '【重要】お知らせ 【通知】', - '[Votre] [Ваше] [您的]', + '[votre] [ваше] [您的]', 'Le [ton] juste. Vote [tu].', 'Siehe [Anlage 2] und [Kapitel 3].', 'Termin: ○ und ◯.', diff --git a/lib/placeholder-check.ts b/lib/placeholder-check.ts index a428ef62a..87f8a0f11 100644 --- a/lib/placeholder-check.ts +++ b/lib/placeholder-check.ts @@ -10,6 +10,11 @@ const TEMPLATE_PLACEHOLDER = /\{\{\s*[\w.-]+\s*\}\}/g; // Markdown link. const CAPS_BRACKET = /\[[\p{Lu}\d][\p{Lu}\d _./-]{0,28}[\p{Lu}\d]\](?!\()/gu; +// [Name], [Company], [First Name]: one to three capitalised words, the way +// templates name a fill-in. A lowercase word ([Yours truly]) or a number +// ([Anlage 2]) makes it ordinary text. +const TITLE_BRACKET = /\[\p{Lu}[\p{Ll}'’-]+(?: \p{Lu}[\p{Ll}'’-]+){0,2}\](?!\()/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 @@ -113,7 +118,7 @@ function collect(text: string, patterns: RegExp[]): Match[] { * to check (see PlaceholderWords). */ export function findUnfilledPlaceholders(subject: string, bodyText: string, words: PlaceholderWords[] = []): string[] { - const bodyPatterns = [TEMPLATE_PLACEHOLDER, CAPS_BRACKET, CIRCLE_MARKER]; + const bodyPatterns = [TEMPLATE_PLACEHOLDER, CAPS_BRACKET, TITLE_BRACKET, CIRCLE_MARKER]; const prompt = promptBracketPattern(words); const unfinished = unfinishedWordPattern(words); if (prompt) bodyPatterns.push(prompt);