diff --git a/src/AutomationPeers/TableViewAutomationPeer.cs b/src/AutomationPeers/TableViewAutomationPeer.cs index 84e46213..7bd5b0c9 100644 --- a/src/AutomationPeers/TableViewAutomationPeer.cs +++ b/src/AutomationPeers/TableViewAutomationPeer.cs @@ -37,7 +37,7 @@ protected override AutomationControlType GetAutomationControlTypeCore() /// protected override string GetLocalizedControlTypeCore() { - return "table view"; + return TableViewLocalizedStrings.TableViewControlType; } /// diff --git a/src/AutomationPeers/TableViewCellAutomationPeer.cs b/src/AutomationPeers/TableViewCellAutomationPeer.cs index b4e2b7f6..5beae9fc 100644 --- a/src/AutomationPeers/TableViewCellAutomationPeer.cs +++ b/src/AutomationPeers/TableViewCellAutomationPeer.cs @@ -42,7 +42,7 @@ protected override AutomationControlType GetAutomationControlTypeCore() /// protected override string GetLocalizedControlTypeCore() { - return "cell"; + return TableViewLocalizedStrings.CellControlType; } /// @@ -71,7 +71,7 @@ protected override string GetNameCore() : string.Empty; var rowIndex = _owner.Row?.Index ?? -1; - var rowDisplay = rowIndex >= 0 ? $"Row {rowIndex + 1}" : string.Empty; + var rowDisplay = rowIndex >= 0 ? TableViewLocalizedStrings.FormatRowNumber(rowIndex + 1) : string.Empty; var cellValue = _owner.Column is TableViewTemplateColumn ? string.Empty diff --git a/src/AutomationPeers/TableViewColumnHeaderAutomationPeer.cs b/src/AutomationPeers/TableViewColumnHeaderAutomationPeer.cs index 4b13c231..0ec8fe6e 100644 --- a/src/AutomationPeers/TableViewColumnHeaderAutomationPeer.cs +++ b/src/AutomationPeers/TableViewColumnHeaderAutomationPeer.cs @@ -38,7 +38,7 @@ protected override AutomationControlType GetAutomationControlTypeCore() /// protected override string GetLocalizedControlTypeCore() { - return "column header"; + return TableViewLocalizedStrings.ColumnHeaderControlType; } /// diff --git a/src/AutomationPeers/TableViewRowAutomationPeer.cs b/src/AutomationPeers/TableViewRowAutomationPeer.cs index 6a0b95f5..9662aa62 100644 --- a/src/AutomationPeers/TableViewRowAutomationPeer.cs +++ b/src/AutomationPeers/TableViewRowAutomationPeer.cs @@ -44,7 +44,7 @@ protected override string GetNameCore() } var index = _owner.Index; - return index >= 0 ? $"Row {index + 1}" : "Row"; + return index >= 0 ? TableViewLocalizedStrings.FormatRowNumber(index + 1) : TableViewLocalizedStrings.Row; } /// diff --git a/src/AutomationPeers/TableViewRowHeaderAutomationPeer.cs b/src/AutomationPeers/TableViewRowHeaderAutomationPeer.cs index aed2b94f..6e13188c 100644 --- a/src/AutomationPeers/TableViewRowHeaderAutomationPeer.cs +++ b/src/AutomationPeers/TableViewRowHeaderAutomationPeer.cs @@ -35,7 +35,7 @@ protected override AutomationControlType GetAutomationControlTypeCore() /// protected override string GetLocalizedControlTypeCore() { - return "row header"; + return TableViewLocalizedStrings.RowHeaderControlType; } /// @@ -77,6 +77,6 @@ protected override string GetNameCore() // Fall back to the row index var rowIndex = _owner.TableViewRow?.Index ?? -1; - return rowIndex >= 0 ? $"Row {rowIndex + 1}" : base.GetNameCore(); + return rowIndex >= 0 ? TableViewLocalizedStrings.FormatRowNumber(rowIndex + 1) : base.GetNameCore(); } } diff --git a/src/Strings/de-DE/WinUI.TableView.resw b/src/Strings/de-DE/WinUI.TableView.resw index d868a18a..c3a87287 100644 --- a/src/Strings/de-DE/WinUI.TableView.resw +++ b/src/Strings/de-DE/WinUI.TableView.resw @@ -204,4 +204,28 @@ Gruppen nach Wert sortieren + + Tabellenansicht + UI Automation localized control type of the TableView. + + + Spaltenkopf + UI Automation localized control type of a column header. + + + Zeilenkopf + UI Automation localized control type of a row header. + + + Zelle + UI Automation localized control type of a cell. + + + Zeile {0} + UI Automation name of a row; {0} is the one-based row number. + + + Zeile + UI Automation name of a row whose index is not known. + diff --git a/src/Strings/en-US/WinUI.TableView.resw b/src/Strings/en-US/WinUI.TableView.resw index aee6f2e4..9434ffd5 100644 --- a/src/Strings/en-US/WinUI.TableView.resw +++ b/src/Strings/en-US/WinUI.TableView.resw @@ -204,4 +204,28 @@ Sort Groups by Value + + table view + UI Automation localized control type of the TableView. + + + column header + UI Automation localized control type of a column header. + + + row header + UI Automation localized control type of a row header. + + + cell + UI Automation localized control type of a cell. + + + Row {0} + UI Automation name of a row; {0} is the one-based row number. + + + Row + UI Automation name of a row whose index is not known. + \ No newline at end of file diff --git a/src/Strings/es-ES/WinUI.TableView.resw b/src/Strings/es-ES/WinUI.TableView.resw index 01fc0e8b..68708bf3 100644 --- a/src/Strings/es-ES/WinUI.TableView.resw +++ b/src/Strings/es-ES/WinUI.TableView.resw @@ -204,4 +204,28 @@ Ordenar grupos por valor + + vista de tabla + UI Automation localized control type of the TableView. + + + encabezado de columna + UI Automation localized control type of a column header. + + + encabezado de fila + UI Automation localized control type of a row header. + + + celda + UI Automation localized control type of a cell. + + + Fila {0} + UI Automation name of a row; {0} is the one-based row number. + + + Fila + UI Automation name of a row whose index is not known. + diff --git a/src/Strings/fa-IR/WinUI.TableView.resw b/src/Strings/fa-IR/WinUI.TableView.resw index 47a5b9d2..eeced4ce 100644 --- a/src/Strings/fa-IR/WinUI.TableView.resw +++ b/src/Strings/fa-IR/WinUI.TableView.resw @@ -204,4 +204,28 @@ مرتب‌ سازی گروه‌ ها بر اساس مقدار + + نمای جدول + UI Automation localized control type of the TableView. + + + سرستون + UI Automation localized control type of a column header. + + + سرسطر + UI Automation localized control type of a row header. + + + خانه + UI Automation localized control type of a cell. + + + سطر {0} + UI Automation name of a row; {0} is the one-based row number. + + + سطر + UI Automation name of a row whose index is not known. + \ No newline at end of file diff --git a/src/Strings/ja-JP/WinUI.TableView.resw b/src/Strings/ja-JP/WinUI.TableView.resw index d3d55d16..2d73c1e1 100644 --- a/src/Strings/ja-JP/WinUI.TableView.resw +++ b/src/Strings/ja-JP/WinUI.TableView.resw @@ -214,4 +214,28 @@ グループを値で並べ替え + + テーブル ビュー + UI Automation localized control type of the TableView. + + + 列ヘッダー + UI Automation localized control type of a column header. + + + 行ヘッダー + UI Automation localized control type of a row header. + + + セル + UI Automation localized control type of a cell. + + + 行 {0} + UI Automation name of a row; {0} is the one-based row number. + + + 行 + UI Automation name of a row whose index is not known. + diff --git a/src/Strings/pl-PL/WinUI.TableView.resw b/src/Strings/pl-PL/WinUI.TableView.resw index 4b5e09cd..98c899fe 100644 --- a/src/Strings/pl-PL/WinUI.TableView.resw +++ b/src/Strings/pl-PL/WinUI.TableView.resw @@ -204,5 +204,29 @@ Sortuj grupy według wartości + + widok tabeli + UI Automation localized control type of the TableView. + + + nagłówek kolumny + UI Automation localized control type of a column header. + + + nagłówek wiersza + UI Automation localized control type of a row header. + + + komórka + UI Automation localized control type of a cell. + + + Wiersz {0} + UI Automation name of a row; {0} is the one-based row number. + + + Wiersz + UI Automation name of a row whose index is not known. + diff --git a/src/Strings/pt-BR/WinUI.TableView.resw b/src/Strings/pt-BR/WinUI.TableView.resw index eb574cd7..f318d064 100644 --- a/src/Strings/pt-BR/WinUI.TableView.resw +++ b/src/Strings/pt-BR/WinUI.TableView.resw @@ -204,4 +204,28 @@ Ordenar grupos por valor + + exibição de tabela + UI Automation localized control type of the TableView. + + + cabeçalho de coluna + UI Automation localized control type of a column header. + + + cabeçalho de linha + UI Automation localized control type of a row header. + + + célula + UI Automation localized control type of a cell. + + + Linha {0} + UI Automation name of a row; {0} is the one-based row number. + + + Linha + UI Automation name of a row whose index is not known. + diff --git a/src/Strings/ru-RU/WinUI.TableView.resw b/src/Strings/ru-RU/WinUI.TableView.resw index 545c0450..55bc1056 100644 --- a/src/Strings/ru-RU/WinUI.TableView.resw +++ b/src/Strings/ru-RU/WinUI.TableView.resw @@ -204,4 +204,28 @@ Сортировать группы по значению + + представление таблицы + UI Automation localized control type of the TableView. + + + заголовок столбца + UI Automation localized control type of a column header. + + + заголовок строки + UI Automation localized control type of a row header. + + + ячейка + UI Automation localized control type of a cell. + + + Строка {0} + UI Automation name of a row; {0} is the one-based row number. + + + Строка + UI Automation name of a row whose index is not known. + diff --git a/src/Strings/sk-SK/WinUI.TableView.resw b/src/Strings/sk-SK/WinUI.TableView.resw index b4623e9b..2104ab4c 100644 --- a/src/Strings/sk-SK/WinUI.TableView.resw +++ b/src/Strings/sk-SK/WinUI.TableView.resw @@ -204,4 +204,28 @@ Zoradiť Skupiny Podľa Hodnoty + + zobrazenie tabuľky + UI Automation localized control type of the TableView. + + + hlavička stĺpca + UI Automation localized control type of a column header. + + + hlavička riadka + UI Automation localized control type of a row header. + + + bunka + UI Automation localized control type of a cell. + + + Riadok {0} + UI Automation name of a row; {0} is the one-based row number. + + + Riadok + UI Automation name of a row whose index is not known. + diff --git a/src/Strings/zh-CN/WinUI.TableView.resw b/src/Strings/zh-CN/WinUI.TableView.resw index 80cc95a9..d21158c8 100644 --- a/src/Strings/zh-CN/WinUI.TableView.resw +++ b/src/Strings/zh-CN/WinUI.TableView.resw @@ -204,4 +204,28 @@ 按值排序分组 + + 表格视图 + UI Automation localized control type of the TableView. + + + 列标题 + UI Automation localized control type of a column header. + + + 行标题 + UI Automation localized control type of a row header. + + + 单元格 + UI Automation localized control type of a cell. + + + 第 {0} 行 + UI Automation name of a row; {0} is the one-based row number. + + + 行 + UI Automation name of a row whose index is not known. + diff --git a/src/Strings/zh-TW/WinUI.TableView.resw b/src/Strings/zh-TW/WinUI.TableView.resw index 591cbf65..25269a14 100644 --- a/src/Strings/zh-TW/WinUI.TableView.resw +++ b/src/Strings/zh-TW/WinUI.TableView.resw @@ -204,4 +204,28 @@ 依值排序群組 + + 表格檢視 + UI Automation localized control type of the TableView. + + + 欄標題 + UI Automation localized control type of a column header. + + + 列標題 + UI Automation localized control type of a row header. + + + 儲存格 + UI Automation localized control type of a cell. + + + 第 {0} 列 + UI Automation name of a row; {0} is the one-based row number. + + + 列 + UI Automation name of a row whose index is not known. + diff --git a/src/TableViewLocalization.cs b/src/TableViewLocalization.cs new file mode 100644 index 00000000..0391d57e --- /dev/null +++ b/src/TableViewLocalization.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +namespace WinUI.TableView; + +/// +/// Lets the host application supply the TableView's user-facing strings from its own resources, +/// in place of the ones this library ships. +/// +/// +/// The library ships its strings in WinUI.TableView.resw for a fixed set of languages. An +/// application that supports a language the library does not, or that wants its own wording (for +/// example the UI Automation control types and row names a screen reader announces), sets +/// . It is asked for a key on every read, so an application that changes +/// language while running is followed. +/// The keys are the resource names in WinUI.TableView.resw (see ). The +/// resolver returns or an empty string to keep the library's own value. A +/// resolver that throws is treated as returning : these strings are read inside +/// UI Automation calls, which must not fail. RowNumber is a composite format with one +/// placeholder, {0}, for the one-based row number; a value that is not a valid format falls +/// back to the library's own. +/// +public static class TableViewLocalization +{ + /// + /// Gets or sets the function the TableView asks for each user-facing string, by resource key. + /// (the default) uses the library's own resources only. + /// + public static Func? StringResolver { get; set; } + + /// + /// Gets every key the TableView asks for. + /// + /// + /// Listed here by name rather than read off , so asking for the keys + /// does not load the library's resources (which needs a running app). + /// + public static IReadOnlyList Keys { get; } = Array.AsReadOnly(new[] + { + nameof(TableViewLocalizedStrings.BlankFilterValue), + nameof(TableViewLocalizedStrings.Cancel), + nameof(TableViewLocalizedStrings.ClearFilter), + nameof(TableViewLocalizedStrings.ClearSorting), + nameof(TableViewLocalizedStrings.Copy), + nameof(TableViewLocalizedStrings.CopyCommandDescription), + nameof(TableViewLocalizedStrings.Paste), + nameof(TableViewLocalizedStrings.PasteCommandDescription), + nameof(TableViewLocalizedStrings.CopyWithHeaders), + nameof(TableViewLocalizedStrings.CopyWithHeadersCommandDescription), + nameof(TableViewLocalizedStrings.DatePickerPlaceholder), + nameof(TableViewLocalizedStrings.DeselectAll), + nameof(TableViewLocalizedStrings.DeselectAllCommandDescription), + nameof(TableViewLocalizedStrings.ExportAll), + nameof(TableViewLocalizedStrings.ExportSelected), + nameof(TableViewLocalizedStrings.Ok), + nameof(TableViewLocalizedStrings.SearchBoxPlaceholder), + nameof(TableViewLocalizedStrings.SelectAll), + nameof(TableViewLocalizedStrings.SelectAllCommandDescription), + nameof(TableViewLocalizedStrings.SelectAllParenthesized), + nameof(TableViewLocalizedStrings.SortAscending), + nameof(TableViewLocalizedStrings.SortDescending), + nameof(TableViewLocalizedStrings.TimePickerPlaceholder), + nameof(TableViewLocalizedStrings.Filtered), + nameof(TableViewLocalizedStrings.Group), + nameof(TableViewLocalizedStrings.Ungroup), + nameof(TableViewLocalizedStrings.UngroupAll), + nameof(TableViewLocalizedStrings.SortGroupsByCount), + nameof(TableViewLocalizedStrings.SortGroupsByValue), + nameof(TableViewLocalizedStrings.TableViewControlType), + nameof(TableViewLocalizedStrings.ColumnHeaderControlType), + nameof(TableViewLocalizedStrings.RowHeaderControlType), + nameof(TableViewLocalizedStrings.CellControlType), + nameof(TableViewLocalizedStrings.RowNumber), + nameof(TableViewLocalizedStrings.Row), + }); + + /// + /// The host's value for , or when there is no resolver, + /// it supplies nothing, or it throws. + /// + internal static string? TryResolve(string key) + { + var resolver = StringResolver; + if (resolver is null) + { + return null; + } + + try + { + return resolver(key); + } + catch (Exception) + { + return null; + } + } +} diff --git a/src/TableViewLocalizedStrings.cs b/src/TableViewLocalizedStrings.cs index a4925678..8098b4bf 100644 --- a/src/TableViewLocalizedStrings.cs +++ b/src/TableViewLocalizedStrings.cs @@ -1,5 +1,6 @@ using Microsoft.Windows.ApplicationModel.Resources; using System; +using System.Globalization; namespace WinUI.TableView; @@ -47,6 +48,13 @@ static TableViewLocalizedStrings() UngroupAll = GetValue(nameof(UngroupAll)); SortGroupsByCount = GetValue(nameof(SortGroupsByCount)); SortGroupsByValue = GetValue(nameof(SortGroupsByValue)); + TableViewControlType = GetValue(nameof(TableViewControlType)); + ColumnHeaderControlType = GetValue(nameof(ColumnHeaderControlType)); + RowHeaderControlType = GetValue(nameof(RowHeaderControlType)); + CellControlType = GetValue(nameof(CellControlType)); + RowNumber = GetValue(nameof(RowNumber)); + _resourceRowNumber = GetValue(nameof(RowNumber)); + Row = GetValue(nameof(Row)); } private static string GetValue(string name) @@ -72,33 +80,69 @@ private static string GetValue(string name) #endif } - public static string BlankFilterValue { get; set; } - public static string Cancel { get; set; } - public static string ClearFilter { get; set; } - public static string ClearSorting { get; set; } - public static string Copy { get; set; } - public static string CopyCommandDescription { get; set; } - public static string Paste { get; set; } - public static string PasteCommandDescription { get; set; } - public static string CopyWithHeaders { get; set; } - public static string CopyWithHeadersCommandDescription { get; set; } - public static string DatePickerPlaceholder { get; set; } - public static string DeselectAll { get; set; } - public static string DeselectAllCommandDescription { get; set; } - public static string ExportAll { get; set; } - public static string ExportSelected { get; set; } - public static string Ok { get; set; } - public static string SearchBoxPlaceholder { get; set; } - public static string SelectAll { get; set; } - public static string SelectAllCommandDescription { get; set; } - public static string SelectAllParenthesized { get; set; } - public static string SortAscending { get; set; } - public static string SortDescending { get; set; } - public static string TimePickerPlaceholder { get; set; } - public static string Filtered { get; set; } - public static string Group { get; set; } - public static string Ungroup { get; set; } - public static string UngroupAll { get; set; } - public static string SortGroupsByCount { get; set; } - public static string SortGroupsByValue { get; set; } + public static string BlankFilterValue { get => Resolve(nameof(BlankFilterValue), field); set; } + public static string Cancel { get => Resolve(nameof(Cancel), field); set; } + public static string ClearFilter { get => Resolve(nameof(ClearFilter), field); set; } + public static string ClearSorting { get => Resolve(nameof(ClearSorting), field); set; } + public static string Copy { get => Resolve(nameof(Copy), field); set; } + public static string CopyCommandDescription { get => Resolve(nameof(CopyCommandDescription), field); set; } + public static string Paste { get => Resolve(nameof(Paste), field); set; } + public static string PasteCommandDescription { get => Resolve(nameof(PasteCommandDescription), field); set; } + public static string CopyWithHeaders { get => Resolve(nameof(CopyWithHeaders), field); set; } + public static string CopyWithHeadersCommandDescription { get => Resolve(nameof(CopyWithHeadersCommandDescription), field); set; } + public static string DatePickerPlaceholder { get => Resolve(nameof(DatePickerPlaceholder), field); set; } + public static string DeselectAll { get => Resolve(nameof(DeselectAll), field); set; } + public static string DeselectAllCommandDescription { get => Resolve(nameof(DeselectAllCommandDescription), field); set; } + public static string ExportAll { get => Resolve(nameof(ExportAll), field); set; } + public static string ExportSelected { get => Resolve(nameof(ExportSelected), field); set; } + public static string Ok { get => Resolve(nameof(Ok), field); set; } + public static string SearchBoxPlaceholder { get => Resolve(nameof(SearchBoxPlaceholder), field); set; } + public static string SelectAll { get => Resolve(nameof(SelectAll), field); set; } + public static string SelectAllCommandDescription { get => Resolve(nameof(SelectAllCommandDescription), field); set; } + public static string SelectAllParenthesized { get => Resolve(nameof(SelectAllParenthesized), field); set; } + public static string SortAscending { get => Resolve(nameof(SortAscending), field); set; } + public static string SortDescending { get => Resolve(nameof(SortDescending), field); set; } + public static string TimePickerPlaceholder { get => Resolve(nameof(TimePickerPlaceholder), field); set; } + public static string Filtered { get => Resolve(nameof(Filtered), field); set; } + public static string Group { get => Resolve(nameof(Group), field); set; } + public static string Ungroup { get => Resolve(nameof(Ungroup), field); set; } + public static string UngroupAll { get => Resolve(nameof(UngroupAll), field); set; } + public static string SortGroupsByCount { get => Resolve(nameof(SortGroupsByCount), field); set; } + public static string SortGroupsByValue { get => Resolve(nameof(SortGroupsByValue), field); set; } + public static string TableViewControlType { get => Resolve(nameof(TableViewControlType), field); set; } + public static string ColumnHeaderControlType { get => Resolve(nameof(ColumnHeaderControlType), field); set; } + public static string RowHeaderControlType { get => Resolve(nameof(RowHeaderControlType), field); set; } + public static string CellControlType { get => Resolve(nameof(CellControlType), field); set; } + public static string RowNumber { get => Resolve(nameof(RowNumber), field); set; } + public static string Row { get => Resolve(nameof(Row), field); set; } + + /// + /// Formats the UI Automation name of a row from its one-based number, e.g. "Row 3". + /// + public static string FormatRowNumber(int oneBasedRowNumber) + { + try + { + return string.Format(CultureInfo.CurrentCulture, RowNumber, oneBasedRowNumber); + } + catch (FormatException) + { + // A host-supplied format that is not a valid composite format must not take down a UI + // Automation call; fall back to this library's own resource. + return string.Format(CultureInfo.CurrentCulture, _resourceRowNumber, oneBasedRowNumber); + } + } + + private static string _resourceRowNumber = "Row {0}"; + + /// + /// The host application's string for when it supplies one through + /// ; otherwise , + /// this library's own resource. Read on every access, so a host that changes language at run time + /// is followed without restarting. + /// + private static string Resolve(string key, string resourceValue) + { + return TableViewLocalization.TryResolve(key) is { Length: > 0 } hostValue ? hostValue : resourceValue; + } } diff --git a/tests/TableViewAutomationPeerTests.cs b/tests/TableViewAutomationPeerTests.cs index 150185b0..59fc938f 100644 --- a/tests/TableViewAutomationPeerTests.cs +++ b/tests/TableViewAutomationPeerTests.cs @@ -1,9 +1,12 @@ +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Automation.Provider; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using System.Threading.Tasks; using WinUI.TableView.AutomationPeers; namespace WinUI.TableView.Tests; @@ -423,4 +426,168 @@ public void TableViewRowHeader_OnCreateAutomationPeer_ReturnsTableViewRowHeaderA Assert.IsInstanceOfType(peer, typeof(TableViewRowHeaderAutomationPeer)); } + + // ─── Localized automation text ────────────────────────────────────────── + // Each test swaps the resource value for a sentinel, so it fails on a peer that returns an + // English literal even when the test runs under en-US, where the literal and the resource agree. + + [UITestMethod] + public void TableViewAutomationPeer_LocalizedControlType_ComesFromResources() + { + AssertUsesResource(v => TableViewLocalizedStrings.TableViewControlType = v, () => TableViewLocalizedStrings.TableViewControlType, + () => new TableViewAutomationPeer(new TableView()).GetLocalizedControlType()); + } + + [UITestMethod] + public void TableViewColumnHeaderAutomationPeer_LocalizedControlType_ComesFromResources() + { + AssertUsesResource(v => TableViewLocalizedStrings.ColumnHeaderControlType = v, () => TableViewLocalizedStrings.ColumnHeaderControlType, + () => new TableViewColumnHeaderAutomationPeer(new TableViewColumnHeader()).GetLocalizedControlType()); + } + + [UITestMethod] + public void TableViewRowHeaderAutomationPeer_LocalizedControlType_ComesFromResources() + { + AssertUsesResource(v => TableViewLocalizedStrings.RowHeaderControlType = v, () => TableViewLocalizedStrings.RowHeaderControlType, + () => new TableViewRowHeaderAutomationPeer(new TableViewRowHeader()).GetLocalizedControlType()); + } + + [UITestMethod] + public void TableViewCellAutomationPeer_LocalizedControlType_ComesFromResources() + { + AssertUsesResource(v => TableViewLocalizedStrings.CellControlType = v, () => TableViewLocalizedStrings.CellControlType, + () => new TableViewCellAutomationPeer(new TableViewCell()).GetLocalizedControlType()); + } + + [UITestMethod] + public void TableViewRowAutomationPeer_NameWithoutIndex_ComesFromResources() + { + // Row.Index returns -1 when not in a list, so the name is the bare "Row" resource. + AssertUsesResource(v => TableViewLocalizedStrings.Row = v, () => TableViewLocalizedStrings.Row, + () => new TableViewRowAutomationPeer(new TableViewRow()).GetName()); + } + + [UITestMethod] + public void FormatRowNumber_UsesTheRowNumberResource() + { + var original = TableViewLocalizedStrings.RowNumber; + try + { + TableViewLocalizedStrings.RowNumber = "R#{0}"; + Assert.AreEqual("R#3", TableViewLocalizedStrings.FormatRowNumber(3)); + } + finally + { + TableViewLocalizedStrings.RowNumber = original; + } + + StringAssert.Contains(TableViewLocalizedStrings.FormatRowNumber(3), "3"); + } + + [UITestMethod] + public void AutomationResources_AreNotEmpty() + { + Assert.IsFalse(string.IsNullOrEmpty(TableViewLocalizedStrings.TableViewControlType)); + Assert.IsFalse(string.IsNullOrEmpty(TableViewLocalizedStrings.ColumnHeaderControlType)); + Assert.IsFalse(string.IsNullOrEmpty(TableViewLocalizedStrings.RowHeaderControlType)); + Assert.IsFalse(string.IsNullOrEmpty(TableViewLocalizedStrings.CellControlType)); + Assert.IsFalse(string.IsNullOrEmpty(TableViewLocalizedStrings.Row)); + StringAssert.Contains(TableViewLocalizedStrings.RowNumber, "{0}"); + } + + // The indexed names ("Row {n}") are only composed for a row that has an index, i.e. one realized in a + // loaded TableView. Each test swaps RowNumber for a sentinel and reads the peer, so it fails on a peer + // that composes the name from an English literal instead of FormatRowNumber. + + [UITestMethod] + public async Task TableViewRowAutomationPeer_IndexedName_ComesFromResources() + { + var row = await RealizeSecondRowAsync(); + + AssertUsesRowNumberResource(() => new TableViewRowAutomationPeer(row).GetName(), expected: "R#2"); + } + + [UITestMethod] + public async Task TableViewRowHeaderAutomationPeer_IndexedName_ComesFromResources() + { + var row = await RealizeSecondRowAsync(); + var rowHeader = new TableViewRowHeader { TableViewRow = row }; + + AssertUsesRowNumberResource(() => new TableViewRowHeaderAutomationPeer(rowHeader).GetName(), expected: "R#2"); + } + + [UITestMethod] + public async Task TableViewCellAutomationPeer_IndexedName_ComesFromResources() + { + var row = await RealizeSecondRowAsync(); + Assert.IsTrue(row.Cells.Count > 0, "the realized row has no cells"); + var cell = row.Cells[0]; + + // The cell's name also carries the column header and the value; only its row part is under test. + AssertUsesRowNumberResource(() => new TableViewCellAutomationPeer(cell).GetName(), expected: "R#2", whole: false); + } + + private static void AssertUsesRowNumberResource(Func read, string expected, bool whole = true) + { + var original = TableViewLocalizedStrings.RowNumber; + try + { + TableViewLocalizedStrings.RowNumber = "R#{0}"; + var name = read(); + if (whole) + { + Assert.AreEqual(expected, name); + } + else + { + StringAssert.Contains(name, expected); + } + } + finally + { + TableViewLocalizedStrings.RowNumber = original; + } + } + + private static async Task RealizeSecondRowAsync() + { + var tableView = new TableView { AutoGenerateColumns = false }; + tableView.Columns.Add(new TableViewTextColumn + { + Header = "Name", + Binding = new Binding { Path = new PropertyPath(nameof(IndexedNameItem.Name)) } + }); + tableView.ItemsSource = new[] + { + new IndexedNameItem { Name = "A" }, + new IndexedNameItem { Name = "B" }, + }; + + await UnitTestApp.Current.MainWindow.LoadTestContentAsync(tableView); + + var row = tableView.ContainerFromIndex(1) as TableViewRow; + Assert.IsNotNull(row, "the second row was not realized"); + Assert.AreEqual(1, row.Index, "the realized row reports no index, so no indexed name would be composed"); + return row; + } + + private sealed class IndexedNameItem + { + public string Name { get; set; } = string.Empty; + } + + private static void AssertUsesResource(Action set, Func get, Func read) + { + const string sentinel = "localized-sentinel"; + var original = get(); + try + { + set(sentinel); + Assert.AreEqual(sentinel, read()); + } + finally + { + set(original); + } + } } diff --git a/tests/TableViewLocalizationTests.cs b/tests/TableViewLocalizationTests.cs new file mode 100644 index 00000000..8948f0c7 --- /dev/null +++ b/tests/TableViewLocalizationTests.cs @@ -0,0 +1,115 @@ +using Microsoft.UI.Xaml.Automation.Peers; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using System; +using System.Linq; +using System.Reflection; +using WinUI.TableView.AutomationPeers; + +namespace WinUI.TableView.Tests; + +/// +/// DH-1944 — : the host application supplies the +/// TableView's strings (the automation control types and row names among them) from its own resources. +/// +[TestClass] +public class TableViewLocalizationTests +{ + [TestCleanup] + public void ClearResolver() => TableViewLocalization.StringResolver = null; + + [UITestMethod] + public void Keys_AreExactlyTheLocalizedStringProperties() + { + var properties = typeof(TableViewLocalizedStrings) + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .Where(p => p.PropertyType == typeof(string)) + .Select(p => p.Name) + .OrderBy(n => n, StringComparer.Ordinal) + .ToArray(); + + CollectionAssert.AreEqual(properties, TableViewLocalization.Keys.OrderBy(n => n, StringComparer.Ordinal).ToArray(), + "every string the TableView reads must be offered to the host's resolver under its resource name"); + } + + [UITestMethod] + public void EveryKey_IsAskedOfTheResolver_AndItsAnswerIsUsed() + { + TableViewLocalization.StringResolver = key => "host:" + key; + + foreach (var property in typeof(TableViewLocalizedStrings).GetProperties(BindingFlags.Public | BindingFlags.Static) + .Where(p => p.PropertyType == typeof(string))) + { + Assert.AreEqual("host:" + property.Name, property.GetValue(null), property.Name); + } + } + + [UITestMethod] + public void A_null_or_empty_answer_keeps_the_librarys_own_value() + { + var own = TableViewLocalizedStrings.SortAscending; + + TableViewLocalization.StringResolver = _ => null; + Assert.AreEqual(own, TableViewLocalizedStrings.SortAscending); + + TableViewLocalization.StringResolver = _ => string.Empty; + Assert.AreEqual(own, TableViewLocalizedStrings.SortAscending); + } + + [UITestMethod] + public void A_resolver_that_throws_keeps_the_librarys_own_value() + { + var own = TableViewLocalizedStrings.CellControlType; + + TableViewLocalization.StringResolver = _ => throw new InvalidOperationException("host failure"); + + Assert.AreEqual(own, TableViewLocalizedStrings.CellControlType); + } + + [UITestMethod] + public void The_resolver_is_asked_on_every_read_so_a_language_change_is_followed() + { + var language = "de"; + TableViewLocalization.StringResolver = key => key == nameof(TableViewLocalizedStrings.Row) ? "Zeile:" + language : null; + Assert.AreEqual("Zeile:de", TableViewLocalizedStrings.Row); + + language = "fr"; + Assert.AreEqual("Zeile:fr", TableViewLocalizedStrings.Row); + } + + [UITestMethod] + public void FormatRowNumber_uses_the_hosts_format() + { + TableViewLocalization.StringResolver = key => key == nameof(TableViewLocalizedStrings.RowNumber) ? "Ligne {0}" : null; + + Assert.AreEqual("Ligne 3", TableViewLocalizedStrings.FormatRowNumber(3)); + } + + [UITestMethod] + public void FormatRowNumber_falls_back_when_the_hosts_format_is_malformed() + { + var own = TableViewLocalizedStrings.FormatRowNumber(3); + TableViewLocalization.StringResolver = key => key == nameof(TableViewLocalizedStrings.RowNumber) ? "Row {0" : null; + + Assert.AreEqual(own, TableViewLocalizedStrings.FormatRowNumber(3)); + } + + [UITestMethod] + public void The_column_headers_localized_control_type_comes_from_the_resolver() + { + TableViewLocalization.StringResolver = key => key == nameof(TableViewLocalizedStrings.ColumnHeaderControlType) ? "Spaltenkopf" : null; + + var peer = FrameworkElementAutomationPeer.CreatePeerForElement(new TableViewColumnHeader()); + + Assert.IsInstanceOfType(peer, typeof(TableViewColumnHeaderAutomationPeer)); + Assert.AreEqual("Spaltenkopf", peer.GetLocalizedControlType()); + } + + [UITestMethod] + public void A_row_peers_name_comes_from_the_resolver() + { + TableViewLocalization.StringResolver = key => key == nameof(TableViewLocalizedStrings.Row) ? "Baris" : null; + + Assert.AreEqual("Baris", new TableViewRowAutomationPeer(new TableViewRow()).GetName()); + } +}