diff --git a/components/filters/__tests__/filter-rule-modal-all-messages.test.tsx b/components/filters/__tests__/filter-rule-modal-all-messages.test.tsx new file mode 100644 index 000000000..56629969d --- /dev/null +++ b/components/filters/__tests__/filter-rule-modal-all-messages.test.tsx @@ -0,0 +1,93 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import React from 'react'; +import type { FilterRule } from '@/lib/jmap/sieve-types'; +import { FilterRuleModal } from '../filter-rule-modal'; + +vi.mock('@/stores/toast-store', () => ({ toast: { error: vi.fn(), success: vi.fn(), info: vi.fn() } })); + +const rule: FilterRule = { + id: 'r1', + name: 'Alles markieren', + enabled: true, + matchType: 'all', + conditions: [{ field: 'from', comparator: 'address_is', value: 'anna@acme.com' }], + actions: [{ type: 'mark_read' }], + stopProcessing: false, +}; + +function openModal(props: Partial> = {}) { + const onSave = vi.fn(); + render(); + return { onSave, saved: () => onSave.mock.calls[0][0] as FilterRule }; +} + +const fieldSelect = () => screen.getByLabelText('conditions') as HTMLSelectElement; +const comparatorSelect = () => screen.queryByLabelText('comparators.contains') as HTMLSelectElement | null; +const valueInput = () => screen.queryByPlaceholderText('value_placeholder_multi') as HTMLInputElement | null; + +describe('the "all messages" condition', () => { + it('is offered, and asks for neither a comparator nor a value', () => { + const { onSave, saved } = openModal(); + expect([...fieldSelect().options].map((o) => o.value)).toContain('all'); + fireEvent.change(fieldSelect(), { target: { value: 'all' } }); + expect(comparatorSelect()).toBeNull(); + expect(valueInput()).toBeNull(); + + fireEvent.click(screen.getByText('save')); + expect(onSave).toHaveBeenCalledTimes(1); + expect(saved().conditions).toEqual([{ field: 'all', comparator: 'any', value: '' }]); + }); + + it('turns back into a regular condition when another field is chosen', () => { + openModal(); + fireEvent.change(fieldSelect(), { target: { value: 'all' } }); + fireEvent.change(fieldSelect(), { target: { value: 'subject' } }); + expect(comparatorSelect()!.value).toBe('contains'); + expect(valueInput()!.value).toBe(''); + }); + + it('opens as it was saved, and keeps itself next to a blank row', () => { + const { saved } = openModal({ initialRule: { ...rule, conditions: [{ field: 'all', comparator: 'any', value: '' }] } }); + expect(fieldSelect().value).toBe('all'); + expect(comparatorSelect()).toBeNull(); + expect(valueInput()).toBeNull(); + // A second row left blank is dropped on save; this one is not. + fireEvent.click(screen.getByText('add_condition')); + fireEvent.click(screen.getByText('save')); + expect(saved().conditions).toEqual([{ field: 'all', comparator: 'any', value: '' }]); + }); + + it('drops the header name of a custom header condition it replaces', () => { + const { saved } = openModal({ + initialRule: { ...rule, conditions: [{ field: 'header', comparator: 'contains', value: 'bar', headerName: 'X-Foo' }] }, + }); + fireEvent.change(fieldSelect(), { target: { value: 'all' } }); + fireEvent.click(screen.getByText('save')); + expect(saved().conditions).toEqual([{ field: 'all', comparator: 'any', value: '' }]); + expect(saved().conditions[0]).not.toHaveProperty('headerName'); + }); + + it('stays when a suggestion is added next to it', () => { + const { saved } = openModal({ + initialRule: { ...rule, conditions: [{ field: 'all', comparator: 'any', value: '' }] }, + suggestions: [{ id: 'subject', label: 'Betreff Rechnung', condition: { field: 'subject', comparator: 'contains', value: 'Rechnung' } }], + }); + // Unlike a blank row, it takes no value and is not made way for. + fireEvent.click(screen.getByText('Betreff Rechnung')); + fireEvent.click(screen.getByText('save')); + expect(saved().conditions).toEqual([ + { field: 'all', comparator: 'any', value: '' }, + { field: 'subject', comparator: 'contains', value: 'Rechnung' }, + ]); + }); + + it('is never run on the messages already in the folder', () => { + openModal({ offerApplyToExisting: true }); + const applyExisting = screen.getByLabelText('apply_existing') as HTMLInputElement; + expect(applyExisting.disabled).toBe(false); + fireEvent.change(fieldSelect(), { target: { value: 'all' } }); + expect(fieldSelect().value).toBe('all'); + expect(applyExisting.disabled).toBe(true); + }); +}); diff --git a/components/filters/filter-rule-modal.tsx b/components/filters/filter-rule-modal.tsx index 25d15ee10..bf4703127 100644 --- a/components/filters/filter-rule-modal.tsx +++ b/components/filters/filter-rule-modal.tsx @@ -49,7 +49,7 @@ interface FilterRuleModalProps { } const ALL_FIELDS: FilterConditionField[] = [ - "from", "to", "cc", "subject", "header", "size", "body", "attachment", + "from", "to", "cc", "subject", "header", "size", "body", "attachment", "all", ]; const TEXT_COMPARATORS: FilterComparator[] = [ @@ -63,9 +63,13 @@ const SIZE_COMPARATORS: FilterComparator[] = ["greater_than", "less_than"]; const ATTACHMENT_COMPARATORS: FilterComparator[] = ["has_any", "has_type"]; +// "All messages" has nothing to choose: its one comparator is not shown. +const ALL_MESSAGES_COMPARATORS: FilterComparator[] = ["any"]; + function comparatorsFor(field: FilterConditionField): FilterComparator[] { if (field === "size") return SIZE_COMPARATORS; if (field === "attachment") return ATTACHMENT_COMPARATORS; + if (field === "all") return ALL_MESSAGES_COMPARATORS; if (field === "from" || field === "to" || field === "cc") return ADDRESS_COMPARATORS; return TEXT_COMPARATORS; } @@ -102,6 +106,11 @@ function isConditionValueEmpty(v: string | string[]): boolean { return !v.trim(); } +// Conditions that test without a value: "has an attachment" and "all messages". +function takesNoValue(c: Pick): boolean { + return c.field === "all" || (c.field === "attachment" && c.comparator === "has_any"); +} + function makeEmptyAction(): FilterAction { return { type: "move", value: "" }; } @@ -181,11 +190,11 @@ export function FilterRuleModal({ // comma the user just typed mid-edit. const validConditions = useMemo(() => conditions .filter((c) => { - if (c.field === "attachment" && c.comparator === "has_any") return true; + if (takesNoValue(c)) return true; return !isConditionValueEmpty(c.value); }) .map((c) => { - if (c.field === "attachment" && c.comparator === "has_any") return c; + if (takesNoValue(c)) return c; if (c.field === "size") return c; // numeric, single-value only if (typeof c.value !== "string") return c; // already structured const parsed = inputStringToValue(c.value); @@ -246,7 +255,7 @@ export function FilterRuleModal({ const applySuggestion = (suggestion: RuleSuggestion) => { setConditions((prev) => { // The blank row a rule starts with makes way for the suggestion. - const kept = prev.filter((c) => !(c.field !== "attachment" && isConditionValueEmpty(c.value))); + const kept = prev.filter((c) => !(c.field !== "attachment" && c.field !== "all" && isConditionValueEmpty(c.value))); const replaceAt = suggestion.replaces ? kept.findIndex(suggestion.replaces) : -1; if (replaceAt === -1) return [...kept, suggestion.condition]; return kept.map((c, i) => (i === replaceAt ? suggestion.condition : c)); @@ -271,9 +280,9 @@ export function FilterRuleModal({ if (updates.field && updates.field !== "header") { delete updated.headerName; } - // has_any takes no value; clear it so we don't leak old text into - // the generated Sieve. - if (updated.field === "attachment" && updated.comparator === "has_any") { + // has_any and "all messages" take no value; clear it so we don't + // leak old text into the generated Sieve. + if (takesNoValue(updated)) { updated.value = ""; } // Size is numeric, single value only - collapse any list to scalar. @@ -437,24 +446,27 @@ export function FilterRuleModal({ /> )} - + {condition.field !== "all" && ( + + )} - {/* has_any takes no value; render a stub so the row layout - stays consistent but no input is editable. */} - {condition.field === "attachment" && condition.comparator === "has_any" ? ( + {/* has_any and "all messages" take no value; render a stub + so the row layout stays consistent but no input is + editable. */} + {takesNoValue(condition) ? (
) : ( { + const t = (key: string) => (key === 'comparators.starts_with' ? '' : key); + return { useTranslations: () => t, useLocale: () => 'en' }; +}); + const rule = (overrides: Partial): FilterRule => ({ id: 'r1', name: 'Rule', @@ -67,4 +74,25 @@ describe('filter rule summary (expanded view)', () => { 'comparators.has_any', ]); }); + + it('shows the value where a locale leaves the comparator empty', () => { + render(); + expect(valuesOf('subject').values).toEqual(['“Rechnung”']); + }); + + it('shows "all messages" on its own, without a comparator or a value', () => { + const all = rule({ + conditions: [{ field: 'all', comparator: 'any', value: '' }], + actions: [{ type: 'mark_read' }], + }); + const { unmount } = render(); + const chip = screen.getByText('condition_fields.all').parentElement!; + expect([...chip.children].map((el) => el.textContent)).toEqual(['condition_fields.all']); + unmount(); + + const { container } = render(); + expect(container.textContent).toBe('condition_fields.all→action_types.mark_read'); + }); }); diff --git a/components/settings/filter-settings.tsx b/components/settings/filter-settings.tsx index 8993be414..855515c61 100644 --- a/components/settings/filter-settings.tsx +++ b/components/settings/filter-settings.tsx @@ -33,11 +33,13 @@ function isReadonlyRule(r: FilterRule): boolean { return r.origin === "external" || r.origin === "opaque"; } -function RuleSummary({ rule }: { rule: FilterRule }) { +export function RuleSummary({ rule }: { rule: FilterRule }) { const t = useTranslations("settings.filters"); const conditions = rule.conditions.slice(0, 2).map((c) => { const field = t(`condition_fields.${c.field}`); + // "All messages" says it all: no comparator, no value. + if (c.field === "all") return field; const comparator = t(`comparators.${c.comparator}`); // has_any is a no-value test ("attachment is present"); appending // `""` would look broken in the summary line. @@ -101,7 +103,9 @@ export function VisualRuleSummary({ rule }: { rule: FilterRule }) { {rule.conditions.map((c, i) => { const field = t(`condition_fields.${c.field}`); - const comparator = t(`comparators.${c.comparator}`); + // "All messages" says it all: no comparator, no value. + const everyMessage = c.field === "all"; + const comparator = everyMessage ? null : t(`comparators.${c.comparator}`); return ( {i > 0 && ( @@ -109,8 +113,8 @@ export function VisualRuleSummary({ rule }: { rule: FilterRule }) { )} {field} - {comparator} - {!(c.field === "attachment" && c.comparator === "has_any") && ( + {!everyMessage && {comparator}} + {!everyMessage && !(c.field === "attachment" && c.comparator === "has_any") && ( // The values of one condition stay in its chip, where "or" // cannot be mistaken for the joiner between conditions. They // wrap between each other under the first value, the "or" diff --git a/lib/filters/__tests__/retroactive.test.ts b/lib/filters/__tests__/retroactive.test.ts index a20b93fa2..daecf9176 100644 --- a/lib/filters/__tests__/retroactive.test.ts +++ b/lib/filters/__tests__/retroactive.test.ts @@ -155,6 +155,13 @@ describe('retroactiveSupport', () => { } }); + it('never runs a rule for all messages on old mail', () => { + // It would act on everything in the folder, a "delete" on the whole inbox. + const all = rule([{ field: 'all', comparator: 'any', value: '' }], [{ type: 'mark_read' }]); + expect(retroactiveSupport(all)).toEqual({ ok: false, reason: 'condition' }); + expect(planRetroactive(all, [message()])).toEqual({ ids: [], steps: [] }); + }); + it('refuses a folder known only by its path, and keep', () => { expect(retroactiveSupport(rule([{ field: 'from', comparator: 'address_is', value: 'a@b.c' }], [{ type: 'move', value: 'News' }])).ok).toBe(false); expect(retroactiveSupport(rule([{ field: 'from', comparator: 'address_is', value: 'a@b.c' }], [{ type: 'keep' }])).ok).toBe(false); diff --git a/lib/jmap/sieve-types.ts b/lib/jmap/sieve-types.ts index d80853f02..53ea76e1b 100644 --- a/lib/jmap/sieve-types.ts +++ b/lib/jmap/sieve-types.ts @@ -17,7 +17,9 @@ export interface SieveCapabilities { export type FilterConditionField = | 'from' | 'to' | 'cc' | 'subject' | 'header' | 'size' | 'body' - | 'attachment'; + | 'attachment' + // Every message (Sieve `true`): no value, and always the comparator 'any'. + | 'all'; export type FilterComparator = | 'contains' | 'not_contains' @@ -38,7 +40,9 @@ export type FilterComparator = // sub.acme.com nor acme.com.evil) // Older Bulwark versions read these as `header :contains`, so a script // they save keeps working, only less strictly. - | 'address_is' | 'domain_is'; + | 'address_is' | 'domain_is' + // For field === 'all': any message at all. + | 'any'; export type FilterActionType = | 'move' | 'copy' | 'forward' diff --git a/lib/sieve/__tests__/generator.test.ts b/lib/sieve/__tests__/generator.test.ts index 1fb0c51c1..c633936cb 100644 --- a/lib/sieve/__tests__/generator.test.ts +++ b/lib/sieve/__tests__/generator.test.ts @@ -225,6 +225,45 @@ describe('generateScript', () => { }); }); + describe('all messages', () => { + const ALL = { field: 'all', comparator: 'any', value: '' } as const; + + it('matches every message', () => { + const script = generateScript([makeRule({ conditions: [ALL], actions: [{ type: 'mark_read' }] })]); + expect(script).toContain('if true {\n addflag "\\\\Seen";\n}'); + // Nothing to require for it. + expect(script).toMatch(/^require \["imap4flags"\];$/m); + }); + + it('still leaves spam out of a move to a folder', () => { + const script = generateScript( + [makeRule({ conditions: [ALL], actions: [{ type: 'move', value: 'Archive' }] })], + undefined, + { extensions: ['fileinto', 'spamtestplus', 'relational'] }, + ); + expect(script).toContain('if allof(true, not spamtest :percent :value "ge" :comparator "i;ascii-numeric" "50") {'); + }); + + it('stands next to other conditions', () => { + const subject = { field: 'subject', comparator: 'contains', value: 'Rechnung' } as const; + expect(generateScript([makeRule({ conditions: [ALL, subject] })])) + .toContain('if allof(true, header :contains "Subject" "Rechnung") {'); + expect(generateScript([makeRule({ matchType: 'any', conditions: [subject, ALL] })])) + .toContain('if anyof(header :contains "Subject" "Rechnung", true) {'); + // Any of them, and still no spam into a folder. + expect(generateScript( + [makeRule({ matchType: 'any', conditions: [subject, ALL] })], + undefined, + { extensions: ['fileinto', 'spamtestplus', 'relational'] }, + )).toContain('if allof(anyof(header :contains "Subject" "Rechnung", true), not spamtest :percent :value "ge" :comparator "i;ascii-numeric" "50") {'); + }); + + it('reads back as written', () => { + const rules = [makeRule({ conditions: [ALL], actions: [{ type: 'mark_read' }] })]; + expect(parseScript(generateScript(rules)).rules).toEqual(rules); + }); + }); + describe('stopProcessing', () => { it('appends stop when stopProcessing is true', () => { const script = generateScript([makeRule({ stopProcessing: true })]); diff --git a/lib/sieve/generator.ts b/lib/sieve/generator.ts index f1dae3d5b..ebe02b016 100644 --- a/lib/sieve/generator.ts +++ b/lib/sieve/generator.ts @@ -36,6 +36,9 @@ function formatStringArg(values: string[], transform: (s: string) => string = (s function generateCondition(condition: FilterCondition): string { const { field, comparator, value } = condition; + // Every message: there is nothing to compare. + if (field === 'all') return 'true'; + if (field === 'size') { // Size is numeric, single value only. It is written unquoted, so // anything but a number (with an optional K/M/G quantifier) would be diff --git a/locales/ar/common.json b/locales/ar/common.json index 3f0e6749c..b860972e0 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -2124,7 +2124,8 @@ "subject": "الموضوع", "header": "ترويسة مخصصة", "size": "الحجم", - "body": "النص" + "body": "النص", + "all": "جميع الرسائل" }, "comparators": { "has_any": "موجود", diff --git a/locales/ca/common.json b/locales/ca/common.json index 670d0e7c3..78e1d366f 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -2124,7 +2124,8 @@ "subject": "Assumpte", "header": "Capçalera personalitzada", "size": "Mida", - "body": "Cos" + "body": "Cos", + "all": "Tots els missatges" }, "comparators": { "has_any": "és present", diff --git a/locales/cs/common.json b/locales/cs/common.json index 3b50ffa25..979bb3eee 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -2117,7 +2117,8 @@ "subject": "Předmět", "header": "Vlastní hlavička", "size": "Velikost", - "body": "Tělo zprávy" + "body": "Tělo zprávy", + "all": "Všechny zprávy" }, "comparators": { "has_any": "je přítomna", diff --git a/locales/da/common.json b/locales/da/common.json index 15d368faa..0b5741706 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -2120,7 +2120,8 @@ "subject": "Emne", "header": "Brugerdefineret header", "size": "Størrelse", - "body": "Brødtekst" + "body": "Brødtekst", + "all": "Alle beskeder" }, "comparators": { "has_any": "er til stede", diff --git a/locales/de/common.json b/locales/de/common.json index 1d8eaeb42..c7640c7af 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -2117,7 +2117,8 @@ "subject": "Betreff", "header": "Benutzerdefinierter Header", "size": "Größe", - "body": "Nachrichtentext" + "body": "Nachrichtentext", + "all": "Alle Nachrichten" }, "comparators": { "has_any": "vorhanden", diff --git a/locales/en/common.json b/locales/en/common.json index 5be6e2466..4bfc0b6b2 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -2124,7 +2124,8 @@ "subject": "Subject", "header": "Custom Header", "size": "Size", - "body": "Body" + "body": "Body", + "all": "All messages" }, "comparators": { "has_any": "is present", diff --git a/locales/es/common.json b/locales/es/common.json index 58e8f8be6..8050c077d 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -2117,7 +2117,8 @@ "subject": "Asunto", "header": "Encabezado personalizado", "size": "Tamaño", - "body": "Cuerpo" + "body": "Cuerpo", + "all": "Todos los mensajes" }, "comparators": { "has_any": "está presente", diff --git a/locales/fa/common.json b/locales/fa/common.json index fa0c9abf2..cdc1f272f 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -2124,7 +2124,8 @@ "subject": "موضوع", "header": "هدر سفارشی", "size": "حجم", - "body": "بدنه" + "body": "بدنه", + "all": "همه پیام‌ها" }, "comparators": { "has_any": "وجود دارد", diff --git a/locales/fr/common.json b/locales/fr/common.json index e9468e9ae..c79f451cf 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -2117,7 +2117,8 @@ "subject": "Objet", "header": "En-tête personnalisé", "size": "Taille", - "body": "Corps" + "body": "Corps", + "all": "Tous les messages" }, "comparators": { "has_any": "est présent", diff --git a/locales/he/common.json b/locales/he/common.json index deb56c0aa..c5c2f176d 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -2037,7 +2037,8 @@ "header": "כותרת מותאמת אישית", "size": "גודל", "body": "גוף", - "attachment": "קובץ מצורף" + "attachment": "קובץ מצורף", + "all": "כל ההודעות" }, "comparators": { "contains": "מכיל", diff --git a/locales/hu/common.json b/locales/hu/common.json index fc568be71..b34ad9baf 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -2120,7 +2120,8 @@ "subject": "Tárgy", "header": "Egyéni fejléc", "size": "Méret", - "body": "Törzs" + "body": "Törzs", + "all": "Minden üzenet" }, "comparators": { "has_any": "jelen van", diff --git a/locales/it/common.json b/locales/it/common.json index 8af92ffc4..3c67aa209 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -2117,7 +2117,8 @@ "subject": "Oggetto", "header": "Intestazione personalizzata", "size": "Dimensione", - "body": "Corpo" + "body": "Corpo", + "all": "Tutti i messaggi" }, "comparators": { "has_any": "è presente", diff --git a/locales/ja/common.json b/locales/ja/common.json index 058a448cf..cc6c642e9 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -2117,7 +2117,8 @@ "subject": "件名", "header": "カスタムヘッダー", "size": "サイズ", - "body": "本文" + "body": "本文", + "all": "すべてのメール" }, "comparators": { "has_any": "あり", diff --git a/locales/ko/common.json b/locales/ko/common.json index 68ac1065d..556db7782 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -2117,7 +2117,8 @@ "subject": "제목", "header": "사용자 지정 헤더", "size": "크기", - "body": "본문" + "body": "본문", + "all": "모든 메일" }, "comparators": { "has_any": "있음", diff --git a/locales/lv/common.json b/locales/lv/common.json index 7c3c8f9ce..1a4aa2198 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -2117,7 +2117,8 @@ "subject": "Temats", "header": "Pielāgota galvene", "size": "Izmērs", - "body": "Teksts" + "body": "Teksts", + "all": "Visas vēstules" }, "comparators": { "has_any": "ir klāt", diff --git a/locales/mn/common.json b/locales/mn/common.json index 71ee7f043..ec01d22e6 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -2124,7 +2124,8 @@ "subject": "Сэдэв", "header": "Захиалгат толгой хэсэг", "size": "Хэмжээ", - "body": "Бие" + "body": "Бие", + "all": "Бүх захиа" }, "comparators": { "has_any": " байгаа", diff --git a/locales/nb/common.json b/locales/nb/common.json index 7593f4441..31bbc71b0 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -2124,7 +2124,8 @@ "subject": "Emne", "header": "Egendefinert meldingshode", "size": "Størrelse", - "body": "Meldingstekst" + "body": "Meldingstekst", + "all": "Alle meldinger" }, "comparators": { "has_any": "finnes", diff --git a/locales/nl/common.json b/locales/nl/common.json index 584620f58..8af80bc4f 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -2117,7 +2117,8 @@ "subject": "Onderwerp", "header": "Aangepaste header", "size": "Grootte", - "body": "Inhoud" + "body": "Inhoud", + "all": "Alle berichten" }, "comparators": { "has_any": "aanwezig", diff --git a/locales/pl/common.json b/locales/pl/common.json index 34284534e..6ff261ad9 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -2117,7 +2117,8 @@ "subject": "Temat", "header": "Niestandardowy nagłówek", "size": "Rozmiar", - "body": "Treść" + "body": "Treść", + "all": "Wszystkie wiadomości" }, "comparators": { "has_any": "jest obecny", diff --git a/locales/pt/common.json b/locales/pt/common.json index 13dc88684..9b7b00c4a 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -2117,7 +2117,8 @@ "subject": "Assunto", "header": "Cabeçalho personalizado", "size": "Tamanho", - "body": "Corpo" + "body": "Corpo", + "all": "Todas as mensagens" }, "comparators": { "has_any": "está presente", diff --git a/locales/ro/common.json b/locales/ro/common.json index e41cc7af7..5513f03a0 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -2124,7 +2124,8 @@ "subject": "Subiect", "header": "Antet personalizat", "size": "Dimensiune", - "body": "Corpul mesajului" + "body": "Corpul mesajului", + "all": "Toate mesajele" }, "comparators": { "has_any": "este prezent", diff --git a/locales/ru/common.json b/locales/ru/common.json index df02b292d..dd81c7518 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -2117,7 +2117,8 @@ "subject": "Тема", "header": "Пользовательский заголовок", "size": "Размер", - "body": "Тело" + "body": "Тело", + "all": "Все письма" }, "comparators": { "has_any": "присутствует", diff --git a/locales/sk/common.json b/locales/sk/common.json index b456da2ad..0233d1ca4 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -2124,7 +2124,8 @@ "subject": "Predmet", "header": "Vlastná hlavička", "size": "Veľkosť", - "body": "Telo správy" + "body": "Telo správy", + "all": "Všetky správy" }, "comparators": { "has_any": "je prítomná", diff --git a/locales/tr/common.json b/locales/tr/common.json index b7f633357..71bf24e8b 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -2117,7 +2117,8 @@ "subject": "Konu", "header": "Özel Başlık", "size": "Boyut", - "body": "Gövde" + "body": "Gövde", + "all": "Tüm iletiler" }, "comparators": { "has_any": "mevcut", diff --git a/locales/uk/common.json b/locales/uk/common.json index 44bde0e2c..6809d2614 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -2117,7 +2117,8 @@ "subject": "Тема", "header": "Спеціальний заголовок", "size": "Розмір", - "body": "Тіло" + "body": "Тіло", + "all": "Усі листи" }, "comparators": { "has_any": "присутнє", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index d89feafc2..a88d3af54 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -2124,7 +2124,8 @@ "subject": "主題", "header": "自訂標頭", "size": "大小", - "body": "內文" + "body": "內文", + "all": "所有郵件" }, "comparators": { "has_any": "存在", diff --git a/locales/zh/common.json b/locales/zh/common.json index 976e3a0f5..3d6f528f4 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -2117,7 +2117,8 @@ "subject": "主题", "header": "自定义标头", "size": "大小", - "body": "正文" + "body": "正文", + "all": "所有邮件" }, "comparators": { "has_any": "存在",