diff --git a/README.md b/README.md index 5a260531..a29724b4 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ React Native (Expo SDK 54) client for [Bulwark Webmail](https://github.com/bulwa ## What's missing or rough - iOS builds, but push notifications and client certificates are Android-only so far -- Filters & rules, S/MIME, plugins, themes, file storage - UI stubs only +- S/MIME, plugins, themes - UI stubs only (filters & rules, the vacation responder and file storage are real implementations) - Calendar editing is partial; contacts editing is basic - No Play Store distribution yet (sideload APK from Releases); iOS ships via TestFlight diff --git a/locales/en/common.json b/locales/en/common.json index 72c55b54..266f0940 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -303,9 +303,7 @@ "move": "Move", "print": "Print", "view_source": "View source", - "fullscreen": "Fullscreen", - "exit_fullscreen": "Exit fullscreen", "export_email": "Export as .eml", "forward_as_attachment": "Forward as attachment", @@ -1666,7 +1664,7 @@ "disable_tooltip": "Disable encryption", "enabled_success": "Encryption at rest enabled" }, - "public_keys":{ + "public_keys": { "name_label": "Public Key name", "name_placeholder": "e.g. My Key", "key_label": "Public Key (S/MIME or PGP ASCII-armored)", @@ -1843,7 +1841,7 @@ }, "refresh_cache": { "label": "Refresh cached data", - "description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions \u2014 fixes a stale or wrong view without signing out.", + "description": "Reload contacts, calendars, and folders from the server. Keeps your accounts and sessions — fixes a stale or wrong view without signing out.", "button": "Refresh" }, "reset_settings": { @@ -3340,7 +3338,31 @@ "disabled_description": "Large file uploads via WebDAV can cause Stalwart/RocksDB instability, including out-of-memory crashes and unrecoverable disk usage. Deleted files may not be immediately purged from blob storage. This feature is not recommended for production environments.", "stability_warning": "Large file uploads can cause server instability. Deleted files may not be immediately purged from storage. Use with caution.", "migration_title": "Updating your files…", - "migration_description": "Organising folders and files into their proper structure. This happens only once." + "migration_description": "Organising folders and files into their proper structure. This happens only once.", + "move_to": "Move to…", + "sharing_access": "Sharing & access", + "shared_with": "Shared with", + "share_add_people": "Add people", + "share_search_placeholder": "Search by name or email", + "share_no_users": "No other users found", + "share_no_matches": "No matches", + "share_folder_hint": "Sharing a folder shares everything inside it.", + "share_error": "Failed to update sharing", + "share_remove": "Remove access", + "share_empty": "Not shared with anyone yet", + "share_role_viewer": "Viewer", + "share_role_editor": "Editor", + "share_role_manager": "Manager", + "share_role_custom": "Custom", + "done": "Done", + "invalid_name": "Invalid name", + "invalid_name_slash": "Names cannot contain \"/\".", + "upload_unavailable": "The file picker is not installed in this build. Rebuild the app to enable uploads.", + "selected_count": "{count} selected", + "folder_empty": "This folder is empty", + "delete_item_title": "Delete this item?", + "delete_items_title": "Delete {count} items?", + "delete_cascade_hint": "Folders are deleted along with everything inside them. This can't be undone." }, "smime": { "your_certificates": "Your Certificates", diff --git a/src/components/files/ShareSheet.tsx b/src/components/files/ShareSheet.tsx index 4e76d273..4f726119 100644 --- a/src/components/files/ShareSheet.tsx +++ b/src/components/files/ShareSheet.tsx @@ -17,6 +17,7 @@ import { getPrincipals, isFolder, ownPrincipalId, setFileNodeShare } from '../.. import type { FileNode, FileNodeRights, Principal } from '../../api/types'; import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens'; import { useColors } from '../../theme/colors'; +import { useLocaleStore } from '../../stores/locale-store'; type RolePreset = 'read' | 'readWrite' | 'manager'; @@ -38,10 +39,10 @@ const FILE_PRESETS: Record = { }, }; -const PRESET_LABELS: Record = { - read: 'Viewer', - readWrite: 'Editor', - manager: 'Manager', +const PRESET_LABEL_KEYS: Record = { + read: ['files.share_role_viewer', 'Viewer'], + readWrite: ['files.share_role_editor', 'Editor'], + manager: ['files.share_role_manager', 'Manager'], }; const PRESET_ORDER: RolePreset[] = ['read', 'readWrite', 'manager']; @@ -65,6 +66,7 @@ interface ShareSheetProps { export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps) { const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); + const t = useLocaleStore((s) => s.t); const [principals, setPrincipals] = useState([]); const [loadingPrincipals, setLoadingPrincipals] = useState(false); @@ -121,12 +123,12 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps }); onChanged(); } catch (e) { - Alert.alert('Sharing failed', e instanceof Error ? e.message : String(e)); + Alert.alert(t('files.share_error', 'Failed to update sharing'), e instanceof Error ? e.message : String(e)); } finally { setSavingId(null); } }, - [node, savingId, onChanged], + [node, savingId, onChanged, t], ); if (!node) return null; @@ -157,12 +159,12 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps - Share “{node.name}” + {t('files.share', 'Share')} “{node.name}” {isFolder(node) ? ( - Sharing a folder shares everything inside it. + {t('files.share_folder_hint', 'Sharing a folder shares everything inside it.')} ) : null} @@ -172,7 +174,7 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps > {sharedEntries.length > 0 ? ( - Shared with + {t('files.shared_with', 'Shared with')} {sharedEntries.map(([principalId, rights]) => { const preset = detectPreset(rights); const busy = savingId === principalId; @@ -190,12 +192,12 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps - {PRESET_LABELS[p]} + {t(PRESET_LABEL_KEYS[p][0], PRESET_LABEL_KEYS[p][1])} ))} {preset === 'custom' ? ( - Custom + {t('files.share_role_custom', 'Custom')} ) : null} {busy ? ( @@ -204,7 +206,7 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps void applyShare(principalId, null)} hitSlop={8} - accessibilityLabel="Remove access" + accessibilityLabel={t('files.share_remove', 'Remove access')} > @@ -216,16 +218,16 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps ) : ( - Not shared with anyone yet + {t('files.share_empty', 'Not shared with anyone yet')} )} - Add people + {t('files.share_add_people', 'Add people')} ) : candidates.length === 0 ? ( - {principals.length === 0 ? 'No other users found' : 'No matches'} + {principals.length === 0 + ? t('files.share_no_users', 'No other users found') + : t('files.share_no_matches', 'No matches')} ) : ( candidates.map((p) => ( @@ -265,7 +269,7 @@ export default function ShareSheet({ node, onClose, onChanged }: ShareSheetProps - Done + {t('files.done', 'Done')} diff --git a/src/components/settings/FilesSettings.tsx b/src/components/settings/FilesSettings.tsx index 56b5fab8..d00a68c0 100644 --- a/src/components/settings/FilesSettings.tsx +++ b/src/components/settings/FilesSettings.tsx @@ -8,14 +8,13 @@ import { spacing, radius, typography, type ThemePalette } from '../../theme/toke import { useColors } from '../../theme/colors'; import { useSettingsStore, - type FilesFolderLayout, type FilesViewMode, type FilesSortKey, type FilesSortDir, } from '../../stores/settings-store'; +import { useLocaleStore } from '../../stores/locale-store'; interface FilesPrefs { - folderLayout: FilesFolderLayout; defaultViewMode: FilesViewMode; defaultSortKey: FilesSortKey; defaultSortDir: FilesSortDir; @@ -69,6 +68,7 @@ function getIcon(c: ThemePalette, file: SampleFile, colored: boolean, sz: number function FilesPreview({ prefs }: { prefs: FilesPrefs }) { const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); + const t = useLocaleStore((s) => s.t); const files = SAMPLE.filter((f) => { if (!prefs.showHiddenFiles && f.hidden) return false; return true; @@ -99,9 +99,9 @@ function FilesPreview({ prefs }: { prefs: FilesPrefs }) { return ( - Name - Size - Modified + {t('files.name', 'Name')} + {t('files.size', 'Size')} + {t('files.modified', 'Modified')} {files.map((f) => ( @@ -126,8 +126,8 @@ export function FilesSettings() { const hydrated = useSettingsStore((s) => s.hydrated); const hydrate = useSettingsStore((s) => s.hydrate); const set = useSettingsStore((s) => s.updateSetting); + const t = useLocaleStore((s) => s.t); - const folderLayout = useSettingsStore((s) => s.filesFolderLayout); const defaultViewMode = useSettingsStore((s) => s.filesDefaultViewMode); const defaultSortKey = useSettingsStore((s) => s.filesDefaultSortKey); const defaultSortDir = useSettingsStore((s) => s.filesDefaultSortDir); @@ -141,7 +141,6 @@ export function FilesSettings() { }, [hydrated, hydrate]); const prefs: FilesPrefs = { - folderLayout, defaultViewMode, defaultSortKey, defaultSortDir, @@ -154,69 +153,76 @@ export function FilesSettings() { return ( - Preview + {t('files.preview', 'Preview')} - - - set('filesFolderLayout', v as FilesFolderLayout)} - options={[ - { value: 'inline', label: 'Inline' }, - { value: 'sidebar', label: 'Sidebar' }, - ]} - /> - - - + + set('filesDefaultViewMode', v as FilesViewMode)} options={[ - { value: 'list', label: 'List' }, - { value: 'grid', label: 'Grid' }, + { value: 'list', label: t('files.list_view', 'List') }, + { value: 'grid', label: t('files.grid_view', 'Grid') }, ]} /> - + set('filesDefaultSortKey', v as FilesSortKey)} options={[ - { value: 'name', label: 'Name' }, - { value: 'size', label: 'Size' }, - { value: 'modified', label: 'Modified' }, + { value: 'name', label: t('files.name', 'Name') }, + { value: 'size', label: t('files.size', 'Size') }, + { value: 'modified', label: t('files.modified', 'Modified') }, ]} /> - + set('filesDefaultSortDir', v as FilesSortDir)} options={[ - { value: 'asc', label: 'Ascending' }, - { value: 'desc', label: 'Descending' }, + { value: 'asc', label: t('files.settings_ascending', 'Ascending') }, + { value: 'desc', label: t('files.settings_descending', 'Descending') }, ]} /> - - + + set('filesShowIcons', v)} /> - + set('filesColoredIcons', v)} disabled={!showIcons} /> - + set('filesShowThumbnails', v)} @@ -224,8 +230,11 @@ export function FilesSettings() { - - + +