From 283835adccf303184db536b10d98a2486be1a62c Mon Sep 17 00:00:00 2001 From: d2dyno <53011783+d2dyno1@users.noreply.github.com> Date: Wed, 6 May 2026 22:51:45 +0200 Subject: [PATCH 1/5] Added pluralization --- .../Assets/Licenses/SmartFormat LICENSE | 43 +++++++++++++++++++ .../Extensions/LocalizationExtensions.cs | 5 ++- .../SecureFolderFS.Sdk.csproj | 1 + 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 src/Platforms/SecureFolderFS.UI/Assets/Licenses/SmartFormat LICENSE diff --git a/src/Platforms/SecureFolderFS.UI/Assets/Licenses/SmartFormat LICENSE b/src/Platforms/SecureFolderFS.UI/Assets/Licenses/SmartFormat LICENSE new file mode 100644 index 000000000..fbb8d6860 --- /dev/null +++ b/src/Platforms/SecureFolderFS.UI/Assets/Licenses/SmartFormat LICENSE @@ -0,0 +1,43 @@ +SmartFormat +https://github.com/axuno/SmartFormat/blob/main/LICENSE.md +MIT +https://github.com/axuno/SmartFormat + +The MIT License (MIT) +===================== + +Copyright � 2011-2022 SmartFormat Project maintainers and contributors. + +All rights reserved. + +* Project founder: Scott Rippey +* Project owner: axuno gGmbH + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the �Software�), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED �AS IS�, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +--- + +The bundled project *SmartFormat.ZString* is Copyright � Cysharp, Inc. [Their software](https://github.com/Cysharp/ZString) +is licensed under [The MIT License (MIT)](src/SmartFormat.ZString/repo/LICENSE). +Their conversion methods under the `ZString/Number` directory +is Copyright � .NET Foundation and Contributors und is licensed +under [The MIT License (MIT)](https://github.com/dotnet/runtime/blob/master/LICENSE.TXT). diff --git a/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationExtensions.cs b/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationExtensions.cs index 2b4adf3a2..6985792eb 100644 --- a/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationExtensions.cs +++ b/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationExtensions.cs @@ -3,6 +3,7 @@ using SecureFolderFS.Sdk.Services; using SecureFolderFS.Shared; using SecureFolderFS.Shared.Helpers; +using SmartFormat; namespace SecureFolderFS.Sdk.Extensions { @@ -43,7 +44,7 @@ public static string ToLocalized(this string resourceKey, ILocalizationService? public static string ToLocalized(this string resourceKey, params object?[] interpolate) { var localized = ToLocalized(resourceKey); - return SafetyHelpers.NoFailureResult(() => string.Format(localized, interpolate)) ?? localized; + return SafetyHelpers.NoFailureResult(() => Smart.Format(localized, interpolate)) ?? localized; } /// @@ -57,7 +58,7 @@ public static string ToLocalized(this string resourceKey, params object?[] inter public static string ToLocalized(this string resourceKey, ILocalizationService localizationService, params object?[] interpolate) { var localized = ToLocalized(resourceKey, localizationService); - return SafetyHelpers.NoFailureResult(() => string.Format(localized, interpolate)) ?? localized; + return SafetyHelpers.NoFailureResult(() => Smart.Format(localized, interpolate)) ?? localized; } } } diff --git a/src/Sdk/SecureFolderFS.Sdk/SecureFolderFS.Sdk.csproj b/src/Sdk/SecureFolderFS.Sdk/SecureFolderFS.Sdk.csproj index 7eb10636e..6be007f7b 100644 --- a/src/Sdk/SecureFolderFS.Sdk/SecureFolderFS.Sdk.csproj +++ b/src/Sdk/SecureFolderFS.Sdk/SecureFolderFS.Sdk.csproj @@ -10,6 +10,7 @@ + From cb49e212be20030e0bc62ec0b99695058d6f3775 Mon Sep 17 00:00:00 2001 From: d2dyno <53011783+d2dyno1@users.noreply.github.com> Date: Wed, 6 May 2026 23:36:48 +0200 Subject: [PATCH 2/5] Updated strings and usages --- .../VaultForegroundService.cs | 2 +- .../Views/Vault/BrowserPage.xaml | 4 +- .../Strings/en-US/Resources.resx | 64 +++++++++---------- .../BaseDateTimeToStringConverter.cs | 3 +- .../Health/HealthFileDataIssueViewModel.cs | 2 +- .../Views/Vault/VaultHealthPage.xaml | 2 +- .../{Progress.cs => TotalProgress.cs} | 3 +- .../LocalizationServiceExtensions.cs | 5 +- .../Extensions/TransferExtensions.cs | 8 +-- .../Storage/Browser/BrowserItemViewModel.cs | 4 +- .../Controls/Transfer/TransferViewModel.cs | 8 +-- .../Views/Vault/VaultHealthViewModel.cs | 2 +- .../MockLocalizationService.cs | 4 +- 13 files changed, 53 insertions(+), 58 deletions(-) rename src/Sdk/SecureFolderFS.Sdk/AppModels/{Progress.cs => TotalProgress.cs} (78%) diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/VaultForegroundService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/VaultForegroundService.cs index ea866bdfb..6f15a92f6 100644 --- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/VaultForegroundService.cs +++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/VaultForegroundService.cs @@ -104,7 +104,7 @@ private void UpdateNotification() { 0 => "VaultUnlocked".ToLocalized(), 1 => "OneVaultIsUnlocked".ToLocalized(), - _ => "MultipleVaultsAreUnlocked".ToLocalized(count) + _ => "VaultsUnlockedPlural".ToLocalized(count) }; // Tapping the notification brings the app back to foreground diff --git a/src/Platforms/SecureFolderFS.Maui/Views/Vault/BrowserPage.xaml b/src/Platforms/SecureFolderFS.Maui/Views/Vault/BrowserPage.xaml index f8d08999f..5a56737f4 100644 --- a/src/Platforms/SecureFolderFS.Maui/Views/Vault/BrowserPage.xaml +++ b/src/Platforms/SecureFolderFS.Maui/Views/Vault/BrowserPage.xaml @@ -50,7 +50,7 @@ FontSize="18" HorizontalOptions="Center" IsVisible="{Binding ViewModel.IsSelecting, Mode=OneWay}" - Text="{Binding ViewModel.CurrentFolder.SelectedItems.Count, Mode=OneWay, Converter={StaticResource CountToStringConverter}, ConverterParameter='(0:LOCALIZE|SelectItems)|(>0:LOCALIZE|ItemsSelected)'}" + Text="{Binding ViewModel.CurrentFolder.SelectedItems.Count, Mode=OneWay, Converter={StaticResource CountToStringConverter}, ConverterParameter='(0:LOCALIZE|SelectItems)|(>0:LOCALIZE|ItemsSelectedPlural)'}" TextColor="#FFFFFF" VerticalOptions="Center" /> @@ -62,7 +62,7 @@ diff --git a/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx b/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx index 7ad9cfa48..c9ecc5154 100644 --- a/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx +++ b/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx @@ -620,9 +620,6 @@ Today, {0} - - {0} ago - An error occurred @@ -1043,11 +1040,11 @@ Yesterday, {0} - - Last week + + {0:plural:Last week|{} weeks ago} - - {0} days ago + + {0:plural:{} day ago|{} days ago} Vault is unlocked @@ -1085,8 +1082,8 @@ Parts of the file are corrupted and will be reset - - {0} corrupted region(s) + + {0:plural:{} corrupted region|{} corrupted regions} Data loss unpreventable @@ -1178,14 +1175,14 @@ Clear selection - - Copying {0} item(s) + + Copying {0:plural:{1}|{} items} - - Moving {0} item(s) + + Moving {0:plural:{1}|{} items} - - Deleting {0} item(s) + + Deleting {0:plural:{1}|{} items} Not enough space @@ -1193,11 +1190,11 @@ Deleting item(s) - - Are you sure you want to permanently delete {0} item(s)? + + Are you sure you want to permanently delete {0:plural:one item|{} items}? - - The deleted items exceed the available space in the recycle bin. Do you want to permanently delete {0} item(s) instead? + + The deleted {0:plural:item exceeds|{} items exceed} the available space in the recycle bin. Do you want to permanently delete {0:plural:this item|{} items} instead? By continuing, you agree to our @@ -1229,14 +1226,14 @@ Enable Device Link - - {0} item(s) selected + + {0:plural:One item|{} items} selected - - {0} element(s) + + {0:plural:{} element|{} elements} - - Found {0} issue(s) + + Found {0:plural:one issue|{} issues} Available widgets @@ -1256,8 +1253,8 @@ This archive format is not supported for extraction - - Extracting {0} items + + Extracting {0:plural:{1}|{} items} Extracting... @@ -1332,7 +1329,7 @@ Manual certificate fingerprint (SHA-256) - Search in: {0} + Search in: '{0}' Root folder @@ -1346,8 +1343,8 @@ Collecting items ({0}) - - Collected {0} items + + Collected {0:plural:{} item|{} items} Scan completed @@ -1421,11 +1418,8 @@ Tap to lock all vaults - - One vault is unlocked - - - {0} vaults are unlocked + + {0:plural:One vault is|{} vaults are} unlocked View in app diff --git a/src/Platforms/SecureFolderFS.UI/ValueConverters/BaseDateTimeToStringConverter.cs b/src/Platforms/SecureFolderFS.UI/ValueConverters/BaseDateTimeToStringConverter.cs index e585edbdf..358cd6a59 100644 --- a/src/Platforms/SecureFolderFS.UI/ValueConverters/BaseDateTimeToStringConverter.cs +++ b/src/Platforms/SecureFolderFS.UI/ValueConverters/BaseDateTimeToStringConverter.cs @@ -2,7 +2,6 @@ using SecureFolderFS.Sdk.Extensions; using SecureFolderFS.Sdk.Services; using SecureFolderFS.Shared; -using SecureFolderFS.Shared.Helpers; namespace SecureFolderFS.UI.ValueConverters { @@ -19,7 +18,7 @@ public abstract class BaseDateTimeToStringConverter : BaseConverter if (parameter is string formatString) { var split = formatString.Split('|'); - if (split[0] == "LOCALIZE") + if (split[0].Equals("localize", StringComparison.OrdinalIgnoreCase)) return string.Format(split[1].ToLocalized(), dateString); else return string.Format(split[1], dateString); diff --git a/src/Platforms/SecureFolderFS.UI/ViewModels/Health/HealthFileDataIssueViewModel.cs b/src/Platforms/SecureFolderFS.UI/ViewModels/Health/HealthFileDataIssueViewModel.cs index e23060a5f..aa289ed9e 100644 --- a/src/Platforms/SecureFolderFS.UI/ViewModels/Health/HealthFileDataIssueViewModel.cs +++ b/src/Platforms/SecureFolderFS.UI/ViewModels/Health/HealthFileDataIssueViewModel.cs @@ -35,7 +35,7 @@ public HealthFileDataIssueViewModel(IStorableChild storable, IResult? result, st { Severity = Severity.Critical; CorruptedChunks = corruptedChunks ?? []; - CorruptedChunksText = !isRecoverable ? string.Empty : "CorruptedChunks".ToLocalized(CorruptedChunks.Count); + CorruptedChunksText = !isRecoverable ? string.Empty : "CorruptedChunksPlural".ToLocalized(CorruptedChunks.Count); IsRecoverable = isRecoverable; } } diff --git a/src/Platforms/SecureFolderFS.Uno/Views/Vault/VaultHealthPage.xaml b/src/Platforms/SecureFolderFS.Uno/Views/Vault/VaultHealthPage.xaml index 759d859d6..4a3a73f71 100644 --- a/src/Platforms/SecureFolderFS.Uno/Views/Vault/VaultHealthPage.xaml +++ b/src/Platforms/SecureFolderFS.Uno/Views/Vault/VaultHealthPage.xaml @@ -264,7 +264,7 @@ /// The current achieved progress so far. /// The total progress to achieve. - public readonly record struct TotalProgress(int Achieved, int Total); + /// The additional state of the progress. + public readonly record struct TotalProgress(int Achieved, int Total, object? State); } diff --git a/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationServiceExtensions.cs b/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationServiceExtensions.cs index 2969937c3..61219466c 100644 --- a/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationServiceExtensions.cs +++ b/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationServiceExtensions.cs @@ -15,13 +15,14 @@ public static string LocalizeDate(this ILocalizationService localizationService, { var cultureInfo = localizationService.CurrentCulture; var daysAgo = (DateTime.Today - dateTime.Date).Days; + var weeksAgo = daysAgo / 7; var dateString = dateTime switch { _ when dateTime.Year == 1 => "Unspecified", _ when dateTime.Date == DateTime.Today => "DateToday".ToLocalized(localizationService, interpolate: dateTime.ToString("t", cultureInfo)), _ when daysAgo == 1 => "DateYesterday".ToLocalized(localizationService, interpolate: dateTime.ToString("t", cultureInfo)), - _ when daysAgo is >= 2 and <= 6 => "DateDaysAgo".ToLocalized(localizationService, interpolate: daysAgo.ToString()), - _ when daysAgo is >= 7 and < 14 => "DateWeekAgo".ToLocalized(localizationService), + _ when daysAgo is >= 2 and <= 6 => "DateDaysAgoPlural".ToLocalized(localizationService, interpolate: daysAgo.ToString()), + _ when daysAgo is >= 7 and < 14 => "DateWeekAgoPlural".ToLocalized(localizationService, interpolate: weeksAgo.ToString()), _ => null }; diff --git a/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs b/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs index 117a9a151..cc078ac3f 100644 --- a/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs +++ b/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs @@ -34,12 +34,12 @@ public static async Task TransferAsync( var collection = items.ToOrAsCollection(); transferViewModel.IsProgressing = true; transferViewModel.IsVisible = true; - transferViewModel.Report(new(0, 0)); + transferViewModel.Report(new(0, 0, 0)); var counter = 0; var reporter = new Progress(_ => { counter++; - transferViewModel.Report(new(counter, 0)); + transferViewModel.Report(new(counter, 0, 1)); }); for (var i = 0; i < collection.Count; i++) @@ -65,7 +65,7 @@ public static async Task TransferAsync( var collection = items.ToOrAsCollection(); transferViewModel.IsProgressing = true; transferViewModel.IsVisible = true; - transferViewModel.Report(new(0, collection.Count)); + transferViewModel.Report(new(0, collection.Count, collection.Count)); for (var i = 0; i < collection.Count; i++) { @@ -73,7 +73,7 @@ public static async Task TransferAsync( var item = collection.ElementAt(i); await callback(item, cancellationToken); - transferViewModel.Report(new((i + 1), collection.Count)); + transferViewModel.Report(new((i + 1), collection.Count, item.Name)); } await Task.Delay(1000, CancellationToken.None); diff --git a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/BrowserItemViewModel.cs b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/BrowserItemViewModel.cs index 14ced089d..d6801df64 100644 --- a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/BrowserItemViewModel.cs +++ b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/BrowserItemViewModel.cs @@ -346,7 +346,7 @@ await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner) var messageOverlay = new MessageOverlayViewModel() { Title = "NotEnoughSpace".ToLocalized(), - Message = "ItemsExceedRecycleBinSize".ToLocalized(items.Length), + Message = "ItemsExceedRecycleBinSizePlural".ToLocalized(items.Length), PrimaryText = "Delete".ToLocalized(), SecondaryText = "Cancel".ToLocalized() }; @@ -377,7 +377,7 @@ await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner) var messageOverlay = new MessageOverlayViewModel() { Title = "ItemDeletionTitle".ToLocalized(), - Message = "ItemDeletionDescription".ToLocalized(items.Length), + Message = "ItemDeletionDescriptionPlural".ToLocalized(items.Length), PrimaryText = "Delete".ToLocalized(), SecondaryText = "Cancel".ToLocalized() }; diff --git a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs index a8d2e220b..ef2c5b27e 100644 --- a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs +++ b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs @@ -44,10 +44,10 @@ public void Report(TotalProgress value) Title = TransferType switch { - TransferType.Copy => "CopyingItems".ToLocalized(GetItemsCount(value)), - TransferType.Move => "MovingItems".ToLocalized(GetItemsCount(value)), - TransferType.Delete => "DeletingItems".ToLocalized(GetItemsCount(value)), - TransferType.Extract => "ExtractingItems".ToLocalized(GetItemsCount(value)), + TransferType.Copy => "CopyingItemsPlural".ToLocalized(GetItemsCount(value), value.State), + TransferType.Move => "MovingItemsPlural".ToLocalized(GetItemsCount(value), value.State), + TransferType.Delete => "DeletingItemsPlural".ToLocalized(GetItemsCount(value), value.State), + TransferType.Extract => "ExtractingItemsPlural".ToLocalized(GetItemsCount(value), value.State), _ => "Loading".ToLocalized() }; diff --git a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.cs b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.cs index ce355bb2d..bdc855610 100644 --- a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.cs +++ b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Vault/VaultHealthViewModel.cs @@ -112,7 +112,7 @@ public void Report(TotalProgress value) StatusTitle = "CollectingItems".ToLocalized(); Subtitle = value.Achieved == 0 ? "CollectingItemsInProgress".ToLocalized() - : "CollectedCountItems".ToLocalized(value.Achieved); + : "CollectedItemsPlural".ToLocalized(value.Achieved); } else { diff --git a/tests/SecureFolderFS.Tests/ServiceImplementation/MockLocalizationService.cs b/tests/SecureFolderFS.Tests/ServiceImplementation/MockLocalizationService.cs index 4ae2d9bf5..a9f59d3b5 100644 --- a/tests/SecureFolderFS.Tests/ServiceImplementation/MockLocalizationService.cs +++ b/tests/SecureFolderFS.Tests/ServiceImplementation/MockLocalizationService.cs @@ -10,8 +10,8 @@ internal sealed class MockLocalizationService : ILocalizationService { { "DateToday", "Today, {0}" }, { "DateYesterday", "Yesterday, {0}" }, - { "DateDaysAgo", "{0} days ago" }, - { "DateWeekAgo", "Last week" } + { "DateDaysAgoPlural", "{0} days ago" }, + { "DateWeekAgoPlural", "Last week" } }; /// From 6f761a4ae28253ef0131671a7305d42d08d39e59 Mon Sep 17 00:00:00 2001 From: d2dyno <53011783+d2dyno1@users.noreply.github.com> Date: Thu, 7 May 2026 01:03:20 +0200 Subject: [PATCH 3/5] More localization fixes --- .../VaultForegroundService.cs | 1 - .../Browser/BrowserControl.DragDrop.xaml.cs | 2 +- .../UserControls/Browser/TransferControl.xaml | 1 + .../Strings/en-US/Resources.resx | 8 ++--- .../AppModels/TotalProgress.cs | 2 +- .../Extensions/LocalizationExtensions.cs | 4 +-- .../LocalizationServiceExtensions.cs | 4 +-- .../Extensions/TransferExtensions.cs | 30 ++++++++++++++++--- .../Storage/Browser/FolderViewModel.cs | 2 +- .../Controls/Transfer/TransferViewModel.cs | 20 +++++-------- .../Views/Vault/BrowserViewModel.cs | 7 +++++ 11 files changed, 53 insertions(+), 28 deletions(-) diff --git a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/VaultForegroundService.cs b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/VaultForegroundService.cs index 6f15a92f6..82d719a43 100644 --- a/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/VaultForegroundService.cs +++ b/src/Platforms/SecureFolderFS.Maui/Platforms/Android/ServiceImplementation/VaultForegroundService.cs @@ -103,7 +103,6 @@ private void UpdateNotification() var title = count switch { 0 => "VaultUnlocked".ToLocalized(), - 1 => "OneVaultIsUnlocked".ToLocalized(), _ => "VaultsUnlockedPlural".ToLocalized(count) }; diff --git a/src/Platforms/SecureFolderFS.Maui/UserControls/Browser/BrowserControl.DragDrop.xaml.cs b/src/Platforms/SecureFolderFS.Maui/UserControls/Browser/BrowserControl.DragDrop.xaml.cs index 308feb3ff..66e262f8f 100644 --- a/src/Platforms/SecureFolderFS.Maui/UserControls/Browser/BrowserControl.DragDrop.xaml.cs +++ b/src/Platforms/SecureFolderFS.Maui/UserControls/Browser/BrowserControl.DragDrop.xaml.cs @@ -461,7 +461,7 @@ await transferViewModel.TransferAsync(itemsToProcess, async (item, reporter, tok new FileViewModel(createdFile, browserViewModel, destinationViewModel), browserViewModel.Layouts.GetSorter()); } - }, cts.Token); + }, x => x.Name, cts.Token); } catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException) { diff --git a/src/Platforms/SecureFolderFS.Maui/UserControls/Browser/TransferControl.xaml b/src/Platforms/SecureFolderFS.Maui/UserControls/Browser/TransferControl.xaml index 4b712c812..56d597636 100644 --- a/src/Platforms/SecureFolderFS.Maui/UserControls/Browser/TransferControl.xaml +++ b/src/Platforms/SecureFolderFS.Maui/UserControls/Browser/TransferControl.xaml @@ -56,6 +56,7 @@ diff --git a/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx b/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx index c9ecc5154..670dd2301 100644 --- a/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx +++ b/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx @@ -1176,13 +1176,13 @@ Clear selection - Copying {0:plural:{1}|{} items} + Copying {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} - Moving {0:plural:{1}|{} items} + Moving {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} - Deleting {0:plural:{1}|{} items} + Deleting {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} Not enough space @@ -1254,7 +1254,7 @@ This archive format is not supported for extraction - Extracting {0:plural:{1}|{} items} + Extracting {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} Extracting... diff --git a/src/Sdk/SecureFolderFS.Sdk/AppModels/TotalProgress.cs b/src/Sdk/SecureFolderFS.Sdk/AppModels/TotalProgress.cs index 45069c30b..9b58a7b65 100644 --- a/src/Sdk/SecureFolderFS.Sdk/AppModels/TotalProgress.cs +++ b/src/Sdk/SecureFolderFS.Sdk/AppModels/TotalProgress.cs @@ -6,5 +6,5 @@ /// The current achieved progress so far. /// The total progress to achieve. /// The additional state of the progress. - public readonly record struct TotalProgress(int Achieved, int Total, object? State); + public readonly record struct TotalProgress(int Achieved, int Total, object? State = null); } diff --git a/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationExtensions.cs b/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationExtensions.cs index 6985792eb..a49446659 100644 --- a/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationExtensions.cs +++ b/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationExtensions.cs @@ -44,7 +44,7 @@ public static string ToLocalized(this string resourceKey, ILocalizationService? public static string ToLocalized(this string resourceKey, params object?[] interpolate) { var localized = ToLocalized(resourceKey); - return SafetyHelpers.NoFailureResult(() => Smart.Format(localized, interpolate)) ?? localized; + return SafetyHelpers.NoFailureResult(() => Smart.Format(localized, interpolate)) ?? $"{{{resourceKey}}}"; } /// @@ -58,7 +58,7 @@ public static string ToLocalized(this string resourceKey, params object?[] inter public static string ToLocalized(this string resourceKey, ILocalizationService localizationService, params object?[] interpolate) { var localized = ToLocalized(resourceKey, localizationService); - return SafetyHelpers.NoFailureResult(() => Smart.Format(localized, interpolate)) ?? localized; + return SafetyHelpers.NoFailureResult(() => Smart.Format(localized, interpolate)) ?? $"{{{resourceKey}}}"; } } } diff --git a/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationServiceExtensions.cs b/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationServiceExtensions.cs index 61219466c..1b0d5bdbf 100644 --- a/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationServiceExtensions.cs +++ b/src/Sdk/SecureFolderFS.Sdk/Extensions/LocalizationServiceExtensions.cs @@ -21,8 +21,8 @@ public static string LocalizeDate(this ILocalizationService localizationService, _ when dateTime.Year == 1 => "Unspecified", _ when dateTime.Date == DateTime.Today => "DateToday".ToLocalized(localizationService, interpolate: dateTime.ToString("t", cultureInfo)), _ when daysAgo == 1 => "DateYesterday".ToLocalized(localizationService, interpolate: dateTime.ToString("t", cultureInfo)), - _ when daysAgo is >= 2 and <= 6 => "DateDaysAgoPlural".ToLocalized(localizationService, interpolate: daysAgo.ToString()), - _ when daysAgo is >= 7 and < 14 => "DateWeekAgoPlural".ToLocalized(localizationService, interpolate: weeksAgo.ToString()), + _ when daysAgo is >= 2 and <= 6 => "DateDaysAgoPlural".ToLocalized(localizationService, interpolate: daysAgo), + _ when daysAgo is >= 7 and < 14 => "DateWeekAgoPlural".ToLocalized(localizationService, interpolate: weeksAgo), _ => null }; diff --git a/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs b/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs index cc078ac3f..33eb92a0e 100644 --- a/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs +++ b/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs @@ -30,16 +30,27 @@ public static async Task TransferAsync( IEnumerable items, Func, CancellationToken, Task> callback, CancellationToken cancellationToken = default) + where TTransferred : IStorable + { + await TransferAsync(transferViewModel, items, callback, x => x.Name, cancellationToken); + } + + public static async Task TransferAsync( + this TransferViewModel transferViewModel, + IEnumerable items, + Func, CancellationToken, Task> callback, + Func itemName, + CancellationToken cancellationToken = default) { var collection = items.ToOrAsCollection(); transferViewModel.IsProgressing = true; transferViewModel.IsVisible = true; - transferViewModel.Report(new(0, 0, 0)); + transferViewModel.Report(new(0, collection.Count, collection.Count)); var counter = 0; - var reporter = new Progress(_ => + var reporter = new Progress(x => { counter++; - transferViewModel.Report(new(counter, 0, 1)); + transferViewModel.Report(new(counter, 0, x.Name)); }); for (var i = 0; i < collection.Count; i++) @@ -47,6 +58,7 @@ public static async Task TransferAsync( cancellationToken.ThrowIfCancellationRequested(); var item = collection.ElementAt(i); + transferViewModel.Report(new(i, collection.Count, itemName(item))); await callback(item, reporter, cancellationToken); } @@ -61,6 +73,16 @@ public static async Task TransferAsync( Func callback, CancellationToken cancellationToken = default) where TTransferred : IStorable + { + await TransferAsync(transferViewModel, items, callback, x => x.Name, cancellationToken); + } + + public static async Task TransferAsync( + this TransferViewModel transferViewModel, + IEnumerable items, + Func callback, + Func itemName, + CancellationToken cancellationToken = default) { var collection = items.ToOrAsCollection(); transferViewModel.IsProgressing = true; @@ -72,8 +94,8 @@ public static async Task TransferAsync( cancellationToken.ThrowIfCancellationRequested(); var item = collection.ElementAt(i); + transferViewModel.Report(new(i, collection.Count, itemName(item))); await callback(item, cancellationToken); - transferViewModel.Report(new((i + 1), collection.Count, item.Name)); } await Task.Delay(1000, CancellationToken.None); diff --git a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FolderViewModel.cs b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FolderViewModel.cs index d5958017f..b87568a33 100644 --- a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FolderViewModel.cs +++ b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FolderViewModel.cs @@ -82,7 +82,7 @@ public async Task ListContentsAsync(CancellationToken cancellationToken = defaul Items.DisposeAll(); Items.Clear(); - var isPickingFolder = BrowserViewModel.TransferViewModel?.IsPickingFolder ?? false; + var isPickingFolder = BrowserViewModel.IsPickingFolder; var items = await Folder.GetItemsAsync(isPickingFolder ? StorableType.Folder : StorableType.All, cancellationToken).ToArrayAsyncImpl(cancellationToken: cancellationToken); BrowserViewModel.Layouts.GetSorter().SortCollection(items.Where(x => !isPickingFolder || x is IFolder).Select(x => (BrowserItemViewModel)(x switch { diff --git a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs index ef2c5b27e..daeffdd17 100644 --- a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs +++ b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Transfer/TransferViewModel.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -44,23 +45,18 @@ public void Report(TotalProgress value) Title = TransferType switch { - TransferType.Copy => "CopyingItemsPlural".ToLocalized(GetItemsCount(value), value.State), - TransferType.Move => "MovingItemsPlural".ToLocalized(GetItemsCount(value), value.State), - TransferType.Delete => "DeletingItemsPlural".ToLocalized(GetItemsCount(value), value.State), - TransferType.Extract => "ExtractingItemsPlural".ToLocalized(GetItemsCount(value), value.State), + TransferType.Copy => "CopyingItemsPlural".ToLocalized(GetInterpolation()), + TransferType.Move => "MovingItemsPlural".ToLocalized(GetInterpolation()), + TransferType.Delete => "DeletingItemsPlural".ToLocalized(GetInterpolation()), + TransferType.Extract => "ExtractingItemsPlural".ToLocalized(GetInterpolation()), _ => "Loading".ToLocalized() }; - return; - static string GetItemsCount(TotalProgress totalProgress) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + object GetInterpolation() { - return totalProgress switch - { - { Achieved: < 0 } => totalProgress.Total.ToString(), - { Total: <= 0 } => totalProgress.Achieved.ToString(), - _ => $"{totalProgress.Achieved}/{totalProgress.Total}" - }; + return new { value.State, value.Achieved, value.Total }; } } diff --git a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Vault/BrowserViewModel.cs b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Vault/BrowserViewModel.cs index aa79fdbbf..ef5b7a51e 100644 --- a/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Vault/BrowserViewModel.cs +++ b/src/Sdk/SecureFolderFS.Sdk/ViewModels/Views/Vault/BrowserViewModel.cs @@ -49,6 +49,11 @@ public partial class BrowserViewModel : BaseDesignationViewModel, IFolderPicker, public INavigator? OuterNavigator { get; } + /// + /// Gets a value indicating whether the folder selection process is currently active. + /// + public bool IsPickingFolder { get; protected set; } + /// /// Gets the thumbnail cache for this browser instance. /// @@ -100,6 +105,7 @@ public override void OnDisappearing() try { + IsPickingFolder = true; TransferViewModel.IsPickingFolder = true; await OuterNavigator.NavigateAsync(this); using var cts = TransferViewModel.GetCancellation(cancellationToken); @@ -111,6 +117,7 @@ public override void OnDisappearing() } finally { + IsPickingFolder = false; TransferViewModel.IsPickingFolder = false; await OuterNavigator.GoBackAsync(); Dispose(); From 0b5544d802156d9ebdcfe5e1dedea1f13cea4f3f Mon Sep 17 00:00:00 2001 From: d2dyno <53011783+d2dyno1@users.noreply.github.com> Date: Thu, 7 May 2026 13:54:37 +0200 Subject: [PATCH 4/5] Apply code review --- .../Assets/Licenses/SmartFormat LICENSE | 10 +++++----- .../Extensions/TransferExtensions.cs | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Platforms/SecureFolderFS.UI/Assets/Licenses/SmartFormat LICENSE b/src/Platforms/SecureFolderFS.UI/Assets/Licenses/SmartFormat LICENSE index fbb8d6860..ca9e216a2 100644 --- a/src/Platforms/SecureFolderFS.UI/Assets/Licenses/SmartFormat LICENSE +++ b/src/Platforms/SecureFolderFS.UI/Assets/Licenses/SmartFormat LICENSE @@ -6,7 +6,7 @@ https://github.com/axuno/SmartFormat The MIT License (MIT) ===================== -Copyright � 2011-2022 SmartFormat Project maintainers and contributors. +Copyright © 2011-2022 SmartFormat Project maintainers and contributors. All rights reserved. @@ -15,7 +15,7 @@ All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation -files (the �Software�), to deal in the Software without +files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the @@ -25,7 +25,7 @@ conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED �AS IS�, WITHOUT WARRANTY OF ANY KIND, +THE SOFTWARE IS PROVIDED ìAS “IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT @@ -36,8 +36,8 @@ OTHER DEALINGS IN THE SOFTWARE. --- -The bundled project *SmartFormat.ZString* is Copyright � Cysharp, Inc. [Their software](https://github.com/Cysharp/ZString) +The bundled project *SmartFormat.ZString* is Copyright © Cysharp, Inc. [Their software](https://github.com/Cysharp/ZString) is licensed under [The MIT License (MIT)](src/SmartFormat.ZString/repo/LICENSE). Their conversion methods under the `ZString/Number` directory -is Copyright � .NET Foundation and Contributors und is licensed +is Copyright © .NET Foundation and Contributors und is licensed under [The MIT License (MIT)](https://github.com/dotnet/runtime/blob/master/LICENSE.TXT). diff --git a/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs b/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs index 33eb92a0e..d9d2f5684 100644 --- a/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs +++ b/src/Sdk/SecureFolderFS.Sdk/Extensions/TransferExtensions.cs @@ -62,7 +62,7 @@ public static async Task TransferAsync( await callback(item, reporter, cancellationToken); } - transferViewModel.Title = "TransferDone".ToLocalized(); + transferViewModel.Report(new(collection.Count, collection.Count, string.Empty)); await Task.Delay(1000, CancellationToken.None); await transferViewModel.HideAsync(); } @@ -98,6 +98,7 @@ public static async Task TransferAsync( await callback(item, cancellationToken); } + transferViewModel.Report(new(collection.Count, collection.Count, string.Empty)); await Task.Delay(1000, CancellationToken.None); await transferViewModel.HideAsync(); } From ef3d8f98e06205d36b6a848cea2fb5b4e3a732c3 Mon Sep 17 00:00:00 2001 From: d2dyno <53011783+d2dyno1@users.noreply.github.com> Date: Thu, 7 May 2026 15:58:06 +0200 Subject: [PATCH 5/5] Update Resources.resx --- .../Strings/en-US/Resources.resx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx b/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx index 670dd2301..cb65a26b8 100644 --- a/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx +++ b/src/Platforms/SecureFolderFS.UI/Strings/en-US/Resources.resx @@ -1041,10 +1041,10 @@ Yesterday, {0} - {0:plural:Last week|{} weeks ago} + {0:plural:Last week|{} weeks ago|{} weeks ago} - {0:plural:{} day ago|{} days ago} + {0:plural:{} day ago|{} days ago|{} days ago} Vault is unlocked @@ -1083,7 +1083,7 @@ Parts of the file are corrupted and will be reset - {0:plural:{} corrupted region|{} corrupted regions} + {0:plural:{} corrupted region|{} corrupted regions|{} corrupted regions} Data loss unpreventable @@ -1176,13 +1176,13 @@ Clear selection - Copying {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} + Copying {Total:choose(0):{Achieved} {Achieved:plural:item|items|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items|items}}} - Moving {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} + Moving {Total:choose(0):{Achieved} {Achieved:plural:item|items|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items|items}}} - Deleting {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} + Deleting {Total:choose(0):{Achieved} {Achieved:plural:item|items|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items|items}}} Not enough space @@ -1191,7 +1191,7 @@ Deleting item(s) - Are you sure you want to permanently delete {0:plural:one item|{} items}? + Are you sure you want to permanently delete {0:plural:one item|{} items|{} items}? The deleted {0:plural:item exceeds|{} items exceed} the available space in the recycle bin. Do you want to permanently delete {0:plural:this item|{} items} instead? @@ -1227,7 +1227,7 @@ Enable Device Link - {0:plural:One item|{} items} selected + {0:plural:One item|{} items|{} items} selected {0:plural:{} element|{} elements} @@ -1254,7 +1254,7 @@ This archive format is not supported for extraction - Extracting {Total:choose(0):{Achieved} {Achieved:plural:item|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items}}} + Extracting {Total:choose(0):{Achieved} {Achieved:plural:item|items|items}|{Total:choose(1):{State}|{Achieved}/{Total} {Total:plural:item|items|items}}} Extracting... @@ -1344,7 +1344,7 @@ Collecting items ({0}) - Collected {0:plural:{} item|{} items} + Collected {0:plural:{} item|{} items|{} items} Scan completed @@ -1419,7 +1419,7 @@ Tap to lock all vaults - {0:plural:One vault is|{} vaults are} unlocked + {0:plural:One vault is|{} vaults are|{} vaults are} unlocked View in app