diff --git a/src/Languages/lang_bn.json b/src/Languages/lang_bn.json
index dc73d82fd3..ffec542b62 100644
--- a/src/Languages/lang_bn.json
+++ b/src/Languages/lang_bn.json
@@ -302,7 +302,7 @@
"Local": "স্থানীয়",
"OK": "ঠিক আছে",
"Last checked: {0}": "সর্বশেষ পরীক্ষা করা হয়েছে: {0}",
- "{0} packages were found, {1} of which match the specified filters.": "{1} প্যাকেজ পাওয়া গেছে যার মধ্যে {0}টি নির্দিষ্ট ফিল্টারগুলির সাথে মেলে।",
+ "{0} packages were found, {1} of which match the specified filters.": "{0} প্যাকেজ পাওয়া গেছে যার মধ্যে {1}টি নির্দিষ্ট ফিল্টারগুলির সাথে মেলে।",
"{0} selected": "{0} নির্বাচিত",
"(Last checked: {0})": "(শেষ পরীক্ষা করা হয়েছেঃ {0})",
"All packages selected": "সমস্ত প্যাকেজ নির্বাচিত",
diff --git a/src/Languages/lang_ca.json b/src/Languages/lang_ca.json
index 2621606a48..2b640226ae 100644
--- a/src/Languages/lang_ca.json
+++ b/src/Languages/lang_ca.json
@@ -302,7 +302,7 @@
"Local": "Local",
"OK": "D'acord",
"Last checked: {0}": "Última comprovació: {0}",
- "{0} packages were found, {1} of which match the specified filters.": "S'han trobat {1} paquets, {0} dels quals s'ajusten als filtres establerts.",
+ "{0} packages were found, {1} of which match the specified filters.": "S'han trobat {0} paquets, {1} dels quals s'ajusten als filtres establerts.",
"{0} selected": "{0} seleccionats",
"(Last checked: {0})": "(Comprovat per darrer cop: {0})",
"All packages selected": "Tots els paquets seleccionats",
diff --git a/src/Languages/lang_gu.json b/src/Languages/lang_gu.json
index e7fb08c27b..df80e9eb35 100644
--- a/src/Languages/lang_gu.json
+++ b/src/Languages/lang_gu.json
@@ -302,7 +302,7 @@
"Local": "સ્થાનિક",
"OK": "ઠીક છે",
"Last checked: {0}": "છેલ્લે તપાસ્યું: {0}",
- "{0} packages were found, {1} of which match the specified filters.": "{1} packages મળ્યા, જેમાંથી {0} નિર્દિષ્ટ filters સાથે મેળ ખાતા હતા.",
+ "{0} packages were found, {1} of which match the specified filters.": "{0} packages મળ્યા, જેમાંથી {1} નિર્દિષ્ટ filters સાથે મેળ ખાતા હતા.",
"{0} selected": "{0} પસંદ થયું",
"(Last checked: {0})": "(છેલ્લે ચકાસાયેલ: {0})",
"All packages selected": "બધા પેકેજો પસંદ કરવામાં આવ્યા",
diff --git a/src/Languages/lang_nb.json b/src/Languages/lang_nb.json
index be5c0d6014..30af8334b6 100644
--- a/src/Languages/lang_nb.json
+++ b/src/Languages/lang_nb.json
@@ -302,7 +302,7 @@
"Local": "Lokal",
"OK": "Greit",
"Last checked: {0}": "Sist sjekket: {0}",
- "{0} packages were found, {1} of which match the specified filters.": "{1} pakker ble funnet, hvorav {0} samsvarte med filtrene dine.",
+ "{0} packages were found, {1} of which match the specified filters.": "{0} pakker ble funnet, hvorav {1} samsvarte med filtrene dine.",
"{0} selected": "{0} valgt",
"(Last checked: {0})": "(Sist sjekket: {0})",
"All packages selected": "Alle pakker valgt",
diff --git a/src/Languages/lang_sv.json b/src/Languages/lang_sv.json
index f96f443241..777668dae6 100644
--- a/src/Languages/lang_sv.json
+++ b/src/Languages/lang_sv.json
@@ -302,7 +302,7 @@
"Local": "Lokal",
"OK": "OK",
"Last checked: {0}": "Senast kontrollerad: {0}",
- "{0} packages were found, {1} of which match the specified filters.": "{1} paket hittades, varav {0} matchade filtret",
+ "{0} packages were found, {1} of which match the specified filters.": "{0} paket hittades, varav {1} matchade filtret",
"{0} selected": "{0} valda",
"(Last checked: {0})": "(Kontrollerades senast: {0})",
"All packages selected": "Alla paket valda",
diff --git a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs
index d4cde5ac28..bcd3afa473 100644
--- a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs
+++ b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs
@@ -169,6 +169,46 @@ partial void OnIsFilterPaneOpenChanged(bool value)
[ObservableProperty] private string _pageTitle = "";
[ObservableProperty] private string _pageIconPath = "";
[ObservableProperty] private string _subtitle = "";
+
+ // Shown after the subtitle in a secondary tone: when the list was last checked, then the page suffix.
+ [ObservableProperty] private string _subtitleDetail = "";
+
+ // The whole subtitle as one sentence, for screen readers.
+ [ObservableProperty] private string _subtitleAccessibleText = "";
+
+ /// The subtitle cut around its numbers, so the page can draw them stronger than the words.
+ [ObservableProperty] private IReadOnlyList _subtitleSegments = [];
+
+ // The sentence is formatted with the real integers, so ICU plural selectors keep working (ru, lt, et, sq);
+ // the numbers are then found in the formatted text, wherever the translation placed them.
+ private void SetSubtitle(string text, params int[] values)
+ {
+ var segments = new List();
+ if (values.Length > 0)
+ {
+ string alternatives = string.Join('|', values
+ .Select(v => v.ToString(CultureInfo.CurrentCulture))
+ .Distinct()
+ .OrderByDescending(v => v.Length)
+ .Select(System.Text.RegularExpressions.Regex.Escape));
+ int last = 0;
+ foreach (System.Text.RegularExpressions.Match match in
+ System.Text.RegularExpressions.Regex.Matches(text, $@"(? last) segments.Add(new SubtitleSegment(text[last..match.Index], false));
+ segments.Add(new SubtitleSegment(match.Value, true));
+ last = match.Index + match.Length;
+ }
+ if (last < text.Length) segments.Add(new SubtitleSegment(text[last..], false));
+ }
+ else
+ {
+ segments.Add(new SubtitleSegment(text, false));
+ }
+
+ SubtitleSegments = segments;
+ Subtitle = text;
+ }
[ObservableProperty] private bool _isLoading;
[ObservableProperty] private bool _backgroundTextVisible;
[ObservableProperty] private bool _loadingImageVisible;
@@ -1059,30 +1099,34 @@ public void UpdateSubtitle()
{
if (Loader.IsLoading || (LoadsOnStart && Loader.HasPendingInitialLoad))
{
- Subtitle = _stillLoadingSubtitle;
+ SetSubtitle(_stillLoadingSubtitle);
+ SubtitleDetail = "";
+ SubtitleAccessibleText = Subtitle;
return;
}
+ SubtitleDetail = (_showLastCheckedTime
+ ? " " + CoreTools.Translate("(Last checked: {0})", _lastLoadTime.ToString(CultureInfo.CurrentCulture))
+ : "") + SubtitleSuffix;
+
if (Loader.Any())
{
+ // {0} is everything the managers returned, {1} what the filters keep, as the sentence reads
int selected = FilteredPackages.GetCheckedPackages().Count;
- string r = CoreTools.Translate(
+ int found = _wrappedPackages.Count;
+ int matching = FilteredPackages.Count;
+ SetSubtitle(CoreTools.Translate(
"{0} packages were found, {1} of which match the specified filters.",
- FilteredPackages.Count,
- _wrappedPackages.Count
- ) + " (" + CoreTools.Translate("{0} selected", selected) + ")";
-
- if (_showLastCheckedTime)
- r += " " + CoreTools.Translate("(Last checked: {0})", _lastLoadTime.ToString(CultureInfo.CurrentCulture));
-
- Subtitle = r + SubtitleSuffix;
+ found,
+ matching
+ ) + " (" + CoreTools.Translate("{0} selected", selected) + ")", found, matching, selected);
}
else
{
- Subtitle = _noPackagesSubtitleBase + (_showLastCheckedTime
- ? " " + CoreTools.Translate("(Last checked: {0})", _lastLoadTime.ToString(CultureInfo.CurrentCulture))
- : "") + SubtitleSuffix;
+ SetSubtitle(_noPackagesSubtitleBase);
}
+
+ SubtitleAccessibleText = Subtitle + SubtitleDetail;
}
// ─── Commands ─────────────────────────────────────────────────────────────
@@ -1202,3 +1246,6 @@ public static bool NameOrIdExactMatch(IPackage pkg, string q, ListA piece of the page subtitle; marks a number.
+public readonly record struct SubtitleSegment(string Text, bool IsValue);
diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml
index 17043f8cbf..aeb79c8d64 100644
--- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml
+++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml
@@ -270,14 +270,14 @@
FontFamily="Segoe UI Variable Display"
TextWrapping="Wrap"
Text="{Binding PageTitle}"/>
+
+ Foreground="{DynamicResource TextFillColorSecondaryBrush}"
+ TextWrapping="Wrap"/>
@@ -717,7 +717,7 @@
ItemsSource="{Binding FilteredPackages}"
automation:AutomationProperties.AccessibilityView="Control"
automation:AutomationProperties.Name="{Binding PageTitle}"
- automation:AutomationProperties.HelpText="{Binding Subtitle}"
+ automation:AutomationProperties.HelpText="{Binding SubtitleAccessibleText}"
AutoGenerateColumns="False"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs
index e32752db8c..3bb7b73b58 100644
--- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs
@@ -6,6 +6,7 @@
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls.Documents;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
@@ -84,7 +85,13 @@ or nameof(PackagesPageViewModel.SortAscending))
SyncFiltersButtonName();
UpdateFilterPaneColumn(ViewModel.IsFilterPaneOpen, animate: true);
}
+ // Set last by UpdateSubtitle, so the segments and the detail are both current by then
+ if (args.PropertyName is nameof(PackagesPageViewModel.SubtitleAccessibleText))
+ {
+ UpdateSubtitleInlines();
+ }
};
+ UpdateSubtitleInlines();
SyncFiltersButtonName();
SyncOrderByButtonName();
@@ -411,6 +418,28 @@ private void SyncOrderByButtonName()
CoreTools.Translate("{0}: {1}, {2}", CoreTools.Translate("Order by"), ViewModel.SortFieldName, direction));
}
+ // The numbers are the information that changes: semibold in the body text colour, the words keep the secondary tone
+ private void UpdateSubtitleInlines()
+ {
+ var inlines = new InlineCollection();
+ foreach (SubtitleSegment segment in ViewModel.SubtitleSegments)
+ {
+ var run = new Run(segment.Text);
+ if (segment.IsValue)
+ {
+ run.FontWeight = FontWeight.SemiBold;
+ run.Bind(TextElement.ForegroundProperty, this.GetResourceObservable("TextFillColorPrimaryBrush"));
+ }
+ inlines.Add(run);
+ }
+
+ if (ViewModel.SubtitleDetail.Length > 0)
+ inlines.Add(new Run(ViewModel.SubtitleDetail));
+
+ MainSubtitle.Inlines = inlines;
+ AutomationProperties.SetName(MainSubtitle, ViewModel.SubtitleAccessibleText);
+ }
+
private void UpdateSortMenuChecks()
{
OrderByName_Menu.Icon = Check(ViewModel.SortFieldIndex == 0);