From 977c763059aac6928d811d66ef2d81b84905dbf7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 12:36:32 +0800 Subject: [PATCH 1/2] Localize the automation peers' control types and row names The automation peers returned English literals for their localized control types ("table view", "column header", "row header", "cell") and composed row names as "Row {n}", so a screen reader on a German, Japanese or Chinese UI announced those in English while every other TableView string already went through TableViewLocalizedStrings. Move them to the .resw files in every shipped language, with FormatRowNumber composing "Row {0}" through the current culture. The tests swap each resource for a sentinel, so they fail on a peer that returns a literal even under en-US. --- .../TableViewAutomationPeer.cs | 2 +- .../TableViewCellAutomationPeer.cs | 4 +- .../TableViewColumnHeaderAutomationPeer.cs | 2 +- .../TableViewRowAutomationPeer.cs | 2 +- .../TableViewRowHeaderAutomationPeer.cs | 4 +- src/Strings/de-DE/WinUI.TableView.resw | 24 ++++++ src/Strings/en-US/WinUI.TableView.resw | 24 ++++++ src/Strings/es-ES/WinUI.TableView.resw | 24 ++++++ src/Strings/fa-IR/WinUI.TableView.resw | 24 ++++++ src/Strings/ja-JP/WinUI.TableView.resw | 24 ++++++ src/Strings/pl-PL/WinUI.TableView.resw | 24 ++++++ src/Strings/pt-BR/WinUI.TableView.resw | 24 ++++++ src/Strings/ru-RU/WinUI.TableView.resw | 24 ++++++ src/Strings/sk-SK/WinUI.TableView.resw | 24 ++++++ src/Strings/zh-CN/WinUI.TableView.resw | 24 ++++++ src/Strings/zh-TW/WinUI.TableView.resw | 24 ++++++ src/TableViewLocalizedStrings.cs | 21 +++++ tests/TableViewAutomationPeerTests.cs | 83 +++++++++++++++++++ 18 files changed, 375 insertions(+), 7 deletions(-) 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/TableViewLocalizedStrings.cs b/src/TableViewLocalizedStrings.cs index a4925678..133423e6 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,12 @@ 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)); + Row = GetValue(nameof(Row)); } private static string GetValue(string name) @@ -101,4 +108,18 @@ private static string GetValue(string name) public static string UngroupAll { get; set; } public static string SortGroupsByCount { get; set; } public static string SortGroupsByValue { get; set; } + public static string TableViewControlType { get; set; } + public static string ColumnHeaderControlType { get; set; } + public static string RowHeaderControlType { get; set; } + public static string CellControlType { get; set; } + public static string RowNumber { get; set; } + public static string Row { get; set; } + + /// + /// Formats the UI Automation name of a row from its one-based number, e.g. "Row 3". + /// + public static string FormatRowNumber(int oneBasedRowNumber) + { + return string.Format(CultureInfo.CurrentCulture, RowNumber, oneBasedRowNumber); + } } diff --git a/tests/TableViewAutomationPeerTests.cs b/tests/TableViewAutomationPeerTests.cs index 150185b0..ca4769fe 100644 --- a/tests/TableViewAutomationPeerTests.cs +++ b/tests/TableViewAutomationPeerTests.cs @@ -423,4 +423,87 @@ 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}"); + } + + 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); + } + } } From ad90bffc79bcc78df8fa05735f7a8de8d0aa4cc9 Mon Sep 17 00:00:00 2001 From: Phil Wade <38622251+SolidRockProgrammer@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:14:53 +0800 Subject: [PATCH 2/2] Test the indexed row names through the row, row-header and cell peers The FormatRowNumber test proved the formatter reads RowNumber, but not that the three peers call it: restoring their English literals left every test green. Each new test realizes a row in a loaded TableView, swaps RowNumber for a sentinel and reads the peer's name. --- tests/TableViewAutomationPeerTests.cs | 84 +++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/TableViewAutomationPeerTests.cs b/tests/TableViewAutomationPeerTests.cs index ca4769fe..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; @@ -492,6 +495,87 @@ public void AutomationResources_AreNotEmpty() 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";