diff --git a/src/stores/__tests__/email-store-toast-params.test.ts b/src/stores/__tests__/email-store-toast-params.test.ts new file mode 100644 index 00000000..5b954d43 --- /dev/null +++ b/src/stores/__tests__/email-store-toast-params.test.ts @@ -0,0 +1,45 @@ +// The toasts the email store raises fill every placeholder their catalog text +// has. Moving several emails to a folder without a name read +// "{count} emails moved": the call passed no count, and the catalog text +// (the webmail's notifications.emails_moved) wins over the fallback. +import { describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +vi.mock('expo-localization', () => ({ getLocales: () => [] })); + +import { getDictionary, translate } from '../../i18n'; + +const SOURCE = path.resolve(__dirname, '../email-store.ts'); + +function flatten(obj: Record, prefix = '', out = new Map()): Map { + for (const [k, v] of Object.entries(obj)) { + const key = prefix ? `${prefix}.${k}` : k; + if (v && typeof v === 'object') flatten(v as Record, key, out); + else if (typeof v === 'string') out.set(key, v); + } + return out; +} + +// t('key', 'fallback') closed right after the fallback: no values passed. +const CALL_WITHOUT_VALUES = /\bt\(\s*'([^']+)'\s*,\s*('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`[^`]*`)\s*,?\s*\)/g; +const PLACEHOLDER = /(? { + it('pass values for every catalog text with a placeholder', () => { + const en = flatten(getDictionary('en') as Record); + const source = fs.readFileSync(SOURCE, 'utf8'); + const offenders = [...source.matchAll(CALL_WITHOUT_VALUES)] + .map((m) => ({ key: m[1], text: en.get(m[1]) })) + .filter(({ text }) => !!text && PLACEHOLDER.test(text)) + .map(({ key, text }) => `${key} = ${text}`); + expect(offenders).toEqual([]); + }); + + it('reads the moved count in the catalog text', () => { + expect(translate('en', 'notifications.emails_moved', 'Emails moved', { count: 3 })).toBe('3 emails moved'); + expect(PLACEHOLDER.test('{count} emails moved')).toBe(true); + expect([..."t('notifications.emails_moved', 'Emails moved')".matchAll(CALL_WITHOUT_VALUES)]).toHaveLength(1); + expect([..."t('notifications.emails_moved', `${count} emails moved`, { count })".matchAll(CALL_WITHOUT_VALUES)]).toHaveLength(0); + }); +}); diff --git a/src/stores/email-store.ts b/src/stores/email-store.ts index a4176993..27160b1a 100644 --- a/src/stores/email-store.ts +++ b/src/stores/email-store.ts @@ -2072,7 +2072,9 @@ export const useEmailStore = create()( ? (targets.length === 1 ? t('notifications.moved_to_mailbox', `Email moved to ${targetName}`, { mailbox: targetName }) : t('email_list.emails_moved_to', `${targets.length} emails moved to ${targetName}`, { count: targets.length, mailbox: targetName })) - : t('notifications.emails_moved', 'Emails moved'), + : (targets.length === 1 + ? t('notifications.email_moved', 'Email moved') + : t('notifications.emails_moved', `${targets.length} emails moved`, { count: targets.length })), createdAt: Date.now(), accountId: source.accountId, items, @@ -2838,7 +2840,9 @@ async function fileAcrossAccounts(action: SpanningAction, targets: Email[], toMa ? (count === 1 ? t('notifications.moved_to_mailbox', `Email moved to ${targetName}`, { mailbox: targetName }) : t('email_list.emails_moved_to', `${count} emails moved to ${targetName}`, { count, mailbox: targetName })) - : t('notifications.emails_moved', 'Emails moved'); + : (count === 1 + ? t('notifications.email_moved', 'Email moved') + : t('notifications.emails_moved', `${count} emails moved`, { count })); set({ emails: get().emails.filter((e) => !goneKeys.has(rowKeyOf(e))), ...(count > 0