diff --git a/locales/rn/de.json b/locales/rn/de.json index 7c8e1686..f810eca3 100644 --- a/locales/rn/de.json +++ b/locales/rn/de.json @@ -952,6 +952,7 @@ "not_empty_message": "\"{name}\" enthält {count} E-Mails. Trotzdem löschen?", "not_empty_title": "Ordner nicht leer", "own_account": "Meine Ordner", + "parent_folder": "Übergeordneter Ordner", "role": "Ordnerrolle", "role_hint": "Weisen Sie diesem Ordner eine Sonderrolle zu (Archiv, Spam, …). Jede Rolle kann nur einem Ordner zugewiesen werden.", "save_failed": "Speichern fehlgeschlagen", diff --git a/locales/rn/en.json b/locales/rn/en.json index fb90bce8..8b53ba9e 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -1237,6 +1237,7 @@ "not_empty_message": "\"{name}\" contains {count} emails. Delete anyway?", "not_empty_title": "Folder not empty", "own_account": "My folders", + "parent_folder": "Parent folder", "role": "Special use", "role_hint": "Assign a special-use role (Archive, Junk, …) to this folder. Each role can be held by one folder.", "save_failed": "Save failed", diff --git a/src/components/__tests__/placeholder-params.test.ts b/src/components/__tests__/placeholder-params.test.ts new file mode 100644 index 00000000..5d39563e --- /dev/null +++ b/src/components/__tests__/placeholder-params.test.ts @@ -0,0 +1,76 @@ +// A t() call whose catalog text has a {placeholder} but which passes no +// values shows the placeholder as it is: the New folder sheet labelled its +// parent picker with settings.folders.subfolder_of, the webmail's +// "Inside {name}" hint, and read "Inside {name}". The English fallback in the +// call ("Parent folder") hid it in review, since the catalog text wins. +import { describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +vi.mock('expo-localization', () => ({ getLocales: () => [] })); + +import { getDictionary } from '../../i18n'; + +const SRC = path.resolve(__dirname, '../..'); + +// The settings, filters, files and sheet code this check covers. +const SCOPE = [ + 'components/settings', + 'components/filters', + 'components/files', + 'components/Dialog.tsx', + 'components/Input.tsx', + 'components/RadioGroup.tsx', + 'components/ToggleSwitch.tsx', + 'components/TagSheet.tsx', + 'components/NewTagRow.tsx', + 'components/MoveSheet.tsx', + 'components/IdentitySheet.tsx', + 'components/TemplateSheet.tsx', + 'screens/SettingsScreen.tsx', + 'screens/FilesScreen.tsx', +]; + +function files(rel: string): string[] { + const p = path.join(SRC, rel); + if (!fs.statSync(p).isDirectory()) return [p]; + return fs.readdirSync(p).flatMap((e) => (e === '__tests__' ? [] : files(path.join(rel, e)))) + .filter((f) => /\.tsx?$/.test(f)); +} + +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') or t('key', "fallback") closed right after the fallback. +const CALL_WITHOUT_VALUES = /\btr?\(\s*'([^']+)'\s*,\s*('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")\s*,?\s*\)/g; +// {name} or {count, plural, ...}; not the {{name}} template tokens. +const PLACEHOLDER = /(? { + it('use catalog texts that have no placeholders', () => { + const en = flatten(getDictionary('en') as Record); + const offenders: string[] = []; + for (const file of SCOPE.flatMap(files)) { + const source = fs.readFileSync(file, 'utf8'); + for (const m of source.matchAll(CALL_WITHOUT_VALUES)) { + const text = en.get(m[1]); + if (text && PLACEHOLDER.test(text)) offenders.push(`${m[1]} = ${text} (${path.relative(SRC, file)})`); + } + } + expect(offenders).toEqual([]); + }); + + it('spots a call that leaves a placeholder unfilled', () => { + const calls = [..."t('settings.folders.subfolder_of', 'Parent folder')".matchAll(CALL_WITHOUT_VALUES)]; + expect(calls.map((m) => m[1])).toEqual(['settings.folders.subfolder_of']); + expect(PLACEHOLDER.test('Inside {name}')).toBe(true); + expect(PLACEHOLDER.test('Placeholders: {{recipient_name}}')).toBe(false); + expect([..."t('a.b', 'x', { name })".matchAll(CALL_WITHOUT_VALUES)]).toEqual([]); + }); +}); diff --git a/src/components/settings/FolderSettings.tsx b/src/components/settings/FolderSettings.tsx index 2268cb5e..f4878192 100644 --- a/src/components/settings/FolderSettings.tsx +++ b/src/components/settings/FolderSettings.tsx @@ -352,7 +352,8 @@ export function FolderSettings() { {editor?.kind === 'edit' ? t('settings.folders.move_under', 'Move under') - : t('settings.folders.subfolder_of', 'Parent folder')} + // Not subfolder_of: that is the webmail's "Inside {name}" hint. + : t('settings.folders.parent_folder', 'Parent folder')}