From 5e557266d184494b99ddaed9e0d3a6baee4a11b4 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 24 Sep 2026 21:37:10 +0200 Subject: [PATCH] fix: count the files selection and delete prompt with plurals The files screen filled `files.selected_count` and `files.delete_items_title` with its own `{name}` substitution, a leftover from before t() took arguments, so they could not be plurals. Both are ICU plurals now and every message on the screen gets its values through t(). The upload progress line is one message instead of "Uploading..." followed by the numbers. --- locales/rn/en.json | 7 ++++--- src/screens/FilesScreen.tsx | 26 ++++++++++++-------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/locales/rn/en.json b/locales/rn/en.json index c711f846..439b081f 100644 --- a/locales/rn/en.json +++ b/locales/rn/en.json @@ -415,7 +415,7 @@ "files": { "delete_cascade_hint": "Folders are deleted along with everything inside them. This can't be undone.", "delete_item_title": "Delete this item?", - "delete_items_title": "Delete {count} items?", + "delete_items_title": "{count, plural, one {Delete # item?} other {Delete # items?}}", "done": "Done", "folder_empty": "This folder is empty", "invalid_name": "Invalid name", @@ -424,7 +424,7 @@ "open_error": "Could not open the file", "open_with": "Open with…", "preview_too_large": "This file is too large to preview.", - "selected_count": "{count} selected", + "selected_count": "{count, plural, one {# selected} other {# selected}}", "share_add_people": "Add people", "share_empty": "Not shared with anyone yet", "share_error": "Failed to update sharing", @@ -438,7 +438,8 @@ "share_unavailable": "Sharing is not available on this device", "shared_with": "Shared with", "sharing_access": "Sharing & access", - "upload_unavailable": "The file picker is not installed in this build. Rebuild the app to enable uploads." + "upload_unavailable": "The file picker is not installed in this build. Rebuild the app to enable uploads.", + "uploading_progress": "Uploading {current}/{total} · {name}" }, "identities": { "badge": { diff --git a/src/screens/FilesScreen.tsx b/src/screens/FilesScreen.tsx index eb297636..4d83da7d 100644 --- a/src/screens/FilesScreen.tsx +++ b/src/screens/FilesScreen.tsx @@ -61,7 +61,7 @@ import { useSettingsStore, type FilesViewMode, type FilesSortKey, type FilesSortDir, } from '../stores/settings-store'; import { useAuthStore } from '../stores/auth-store'; -import { useLocaleStore } from '../stores/locale-store'; +import { useLocaleStore, type TranslateFn } from '../stores/locale-store'; import Dialog from '../components/Dialog'; import ShareSheet from '../components/files/ShareSheet'; import { FilePreviewModal, canPreviewInApp } from '../components/files/FilePreviewModal'; @@ -71,13 +71,7 @@ interface FileRow extends FileNode { displayName: string; } -type Translate = (key: string, fallback?: string) => string; - -// The locale strings use `{name}`-style placeholders; the RN `t` does no -// interpolation, so substitute here. -function fill(text: string, vars: Record): string { - return text.replace(/\{(\w+)\}/g, (_, k: string) => (k in vars ? String(vars[k]) : `{${k}}`)); -} +type Translate = TranslateFn; const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp']; @@ -591,7 +585,7 @@ export default function FilesScreen() { if (maxSize > 0 && size != null && size > maxSize) { Alert.alert( t('files.upload_error', 'Failed to upload file'), - fill(t('files.file_too_large', '"{name}" exceeds the maximum file size ({max})'), { + t('files.file_too_large', '"{name}" exceeds the maximum file size ({max})', { name: asset.name, max: formatFileSize(maxSize), }), @@ -683,7 +677,7 @@ export default function FilesScreen() { - {fill(t('files.selected_count', '{count} selected'), { count: selection.size })} + {t('files.selected_count', '{count, plural, one {# selected} other {# selected}}', { count: selection.size })} - {t('files.uploading', 'Uploading...')} {uploadProgress.current}/{uploadProgress.total} · {uploadProgress.name} + {t('files.uploading_progress', 'Uploading {current}/{total} · {name}', { + current: uploadProgress.current, + total: uploadProgress.total, + name: uploadProgress.name, + })} @@ -881,7 +879,7 @@ export default function FilesScreen() { {item.isShared && item.accountName ? ( - {fill(t('files.shared_by', 'Shared by {name}'), { name: item.accountName })} + {t('files.shared_by', 'Shared by {name}', { name: item.accountName })} ) : null} {item.size != null && !isDir ? ( @@ -940,7 +938,7 @@ export default function FilesScreen() { {item.isShared && item.accountName ? ( - {fill(t('files.shared_by', 'Shared by {name}'), { name: item.accountName })} + {t('files.shared_by', 'Shared by {name}', { name: item.accountName })} ) : item.size != null && !isDir ? ( @@ -1120,7 +1118,7 @@ export default function FilesScreen() { visible={confirmDelete != null} title={ confirmDelete && confirmDelete.rows.length > 1 - ? fill(t('files.delete_items_title', 'Delete {count} items?'), { count: confirmDelete.rows.length }) + ? t('files.delete_items_title', '{count, plural, one {Delete # item?} other {Delete # items?}}', { count: confirmDelete.rows.length }) : t('files.delete_item_title', 'Delete this item?') } message={t('files.delete_cascade_hint', "Folders are deleted along with everything inside them. This can't be undone.")}