fix: label the new folder parent picker "Parent folder" instead of "Inside {name}"

This commit is contained in:
Linus Rath
2026-10-31 13:22:56 +01:00
parent 980243469c
commit e4eda0a840
4 changed files with 80 additions and 1 deletions
+1
View File
@@ -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",
+1
View File
@@ -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",
@@ -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<string, unknown>, prefix = '', out = new Map<string, string>()): Map<string, string> {
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === 'object') flatten(v as Record<string, unknown>, 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 = /(?<!\{)\{\s*\w+\s*[,}]/;
describe('t() calls without values', () => {
it('use catalog texts that have no placeholders', () => {
const en = flatten(getDictionary('en') as Record<string, unknown>);
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([]);
});
});
+2 -1
View File
@@ -352,7 +352,8 @@ export function FolderSettings() {
<Text style={styles.fieldLabel}>
{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')}
</Text>
<Select
value={draftParent}