Skip to content

Commit 849a438

Browse files
RedthCopilot
andcommitted
[Essentials] Make Locale constructible by external TextToSpeech backends
Microsoft.Maui.Media.Locale only had an internal constructor, so an out-of-tree ITextToSpeech implementation could not implement GetLocalesAsync or populate SpeechOptions.Locale without reflection or forking. Community backends (for example Maui.Tizen) had to mark locale enumeration as unsupported. Make the existing constructor public and normalize null arguments to string.Empty so the non-nullable Language/Country/Name/Id contract that PublicAPI already declares is actually honored. Values are otherwise stored verbatim: not parsed, validated, trimmed, or case normalized. Immutability, reference equality, and the default ToString are unchanged. Because locales can now be created with empty Id/Language, tighten the platform voice-selection guards from `!= null` to string.IsNullOrEmpty so a language-only locale still falls back the way it does today. This also fixes a latent NullReferenceException on Tizen where `options?.Locale.Language` dereferenced a null Locale. Add Essentials.ExternalBackend.UnitTests, a test assembly that has no InternalsVisibleTo access to Microsoft.Maui.Essentials, proving an external backend can implement ITextToSpeech, return Locale values from GetLocalesAsync, and round-trip them through SpeechOptions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent bedd1b1 commit 849a438

19 files changed

Lines changed: 212 additions & 13 deletions

Microsoft.Maui-mac.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"src\\Essentials\\samples\\Samples\\Essentials.Sample.csproj",
4646
"src\\Essentials\\src\\Essentials.csproj",
4747
"src\\Essentials\\test\\DeviceTests\\Essentials.DeviceTests.csproj",
48+
"src\\Essentials\\test\\ExternalBackendTests\\Essentials.ExternalBackend.UnitTests.csproj",
4849
"src\\Essentials\\test\\UnitTests\\Essentials.UnitTests.csproj",
4950
"src\\Graphics\\src\\Graphics.Skia\\Graphics.Skia.csproj",
5051
"src\\Graphics\\src\\Graphics\\Graphics.csproj",

Microsoft.Maui-windows.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"src\\Essentials\\samples\\Samples\\Essentials.Sample.csproj",
5353
"src\\Essentials\\src\\Essentials.csproj",
5454
"src\\Essentials\\test\\DeviceTests\\Essentials.DeviceTests.csproj",
55+
"src\\Essentials\\test\\ExternalBackendTests\\Essentials.ExternalBackend.UnitTests.csproj",
5556
"src\\Essentials\\test\\UnitTests\\Essentials.UnitTests.csproj",
5657
"src\\Graphics\\samples\\GraphicsTester.Android\\GraphicsTester.Android.csproj",
5758
"src\\Graphics\\samples\\GraphicsTester.Portable\\GraphicsTester.Portable.csproj",

Microsoft.Maui.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Essentials.DeviceTests", "s
9595
EndProject
9696
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Essentials.UnitTests", "src\Essentials\test\UnitTests\Essentials.UnitTests.csproj", "{A9BAB840-A53E-405A-9458-CA0E02319FB4}"
9797
EndProject
98+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Essentials.ExternalBackend.UnitTests", "src\Essentials\test\ExternalBackendTests\Essentials.ExternalBackend.UnitTests.csproj", "{7C6A0D6E-9B4E-4C0E-9E3C-2F1A5D8B4E71}"
99+
EndProject
98100
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.Maps", "src\Controls\Maps\src\Controls.Maps.csproj", "{5C40FA00-7D48-4F54-898C-01C8942E451D}"
99101
EndProject
100102
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{25D0D27A-C5FE-443D-8B65-D6C987F4A80E}"
@@ -368,6 +370,10 @@ Global
368370
{A9BAB840-A53E-405A-9458-CA0E02319FB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
369371
{A9BAB840-A53E-405A-9458-CA0E02319FB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
370372
{A9BAB840-A53E-405A-9458-CA0E02319FB4}.Release|Any CPU.Build.0 = Release|Any CPU
373+
{7C6A0D6E-9B4E-4C0E-9E3C-2F1A5D8B4E71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
374+
{7C6A0D6E-9B4E-4C0E-9E3C-2F1A5D8B4E71}.Debug|Any CPU.Build.0 = Debug|Any CPU
375+
{7C6A0D6E-9B4E-4C0E-9E3C-2F1A5D8B4E71}.Release|Any CPU.ActiveCfg = Release|Any CPU
376+
{7C6A0D6E-9B4E-4C0E-9E3C-2F1A5D8B4E71}.Release|Any CPU.Build.0 = Release|Any CPU
371377
{5C40FA00-7D48-4F54-898C-01C8942E451D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
372378
{5C40FA00-7D48-4F54-898C-01C8942E451D}.Debug|Any CPU.Build.0 = Debug|Any CPU
373379
{5C40FA00-7D48-4F54-898C-01C8942E451D}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -686,6 +692,7 @@ Global
686692
{420BC8CC-EE70-49A1-964A-CB63414A882D} = {94F3C036-A5F4-4ACC-A028-8506802ADB88}
687693
{66528BE6-FF53-4A3B-8675-46C0B929C83B} = {420BC8CC-EE70-49A1-964A-CB63414A882D}
688694
{A9BAB840-A53E-405A-9458-CA0E02319FB4} = {420BC8CC-EE70-49A1-964A-CB63414A882D}
695+
{7C6A0D6E-9B4E-4C0E-9E3C-2F1A5D8B4E71} = {420BC8CC-EE70-49A1-964A-CB63414A882D}
689696
{5C40FA00-7D48-4F54-898C-01C8942E451D} = {50C758FE-4E10-409A-94F5-A75480960864}
690697
{25D0D27A-C5FE-443D-8B65-D6C987F4A80E} = {459BF674-83CB-46F6-881F-A2D2117DBF4D}
691698
{094FB109-1CBC-4B84-8CFB-072E7B3EC637} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E}

eng/cake/dotnet.cake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ Task("dotnet-test")
276276
"**/Controls.BindingSourceGen.UnitTests.csproj",
277277
"**/Core.UnitTests.csproj",
278278
"**/Essentials.UnitTests.csproj",
279+
"**/Essentials.ExternalBackend.UnitTests.csproj",
279280
"**/Resizetizer.UnitTests.csproj",
280281
"**/Graphics.Tests.csproj",
281282
"**/MauiBlazorWebView.UnitTests.csproj",

eng/helix.proj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
<XUnitProject Include="$(RepoRoot)src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj" />
5252
<XUnitProject Include="$(RepoRoot)src/Core/tests/UnitTests/Core.UnitTests.csproj" />
5353
<XUnitProject Include="$(RepoRoot)src/Essentials/test/UnitTests/Essentials.UnitTests.csproj" />
54+
<XUnitProject Include="$(RepoRoot)src/Essentials/test/ExternalBackendTests/Essentials.ExternalBackend.UnitTests.csproj" />
5455
<XUnitProject Include="$(RepoRoot)src/Graphics/tests/Graphics.Tests/Graphics.Tests.csproj" />
5556
<XUnitProject Include="$(RepoRoot)src/BlazorWebView/tests/MauiBlazorWebView.UnitTests/MauiBlazorWebView.UnitTests.csproj" />
5657
</ItemGroup>

src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,4 @@ static Microsoft.Maui.Authentication.Passkeys.CreateAsync(Microsoft.Maui.Authent
6363
static Microsoft.Maui.Authentication.Passkeys.CreateAsync(string! creationOptionsJson, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Maui.Authentication.PasskeyCreationResponse!>!
6464
static Microsoft.Maui.Authentication.Passkeys.Default.get -> Microsoft.Maui.Authentication.IPasskeys!
6565
static Microsoft.Maui.Authentication.Passkeys.IsSupported.get -> bool
66+
Microsoft.Maui.Media.Locale.Locale(string? language, string? country, string? name, string? id) -> void

src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ static Microsoft.Maui.Authentication.Passkeys.CreateAsync(Microsoft.Maui.Authent
5050
static Microsoft.Maui.Authentication.Passkeys.CreateAsync(string! creationOptionsJson, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Maui.Authentication.PasskeyCreationResponse!>!
5151
static Microsoft.Maui.Authentication.Passkeys.Default.get -> Microsoft.Maui.Authentication.IPasskeys!
5252
static Microsoft.Maui.Authentication.Passkeys.IsSupported.get -> bool
53+
Microsoft.Maui.Media.Locale.Locale(string? language, string? country, string? name, string? id) -> void

src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ static Microsoft.Maui.Authentication.Passkeys.CreateAsync(Microsoft.Maui.Authent
5050
static Microsoft.Maui.Authentication.Passkeys.CreateAsync(string! creationOptionsJson, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Maui.Authentication.PasskeyCreationResponse!>!
5151
static Microsoft.Maui.Authentication.Passkeys.Default.get -> Microsoft.Maui.Authentication.IPasskeys!
5252
static Microsoft.Maui.Authentication.Passkeys.IsSupported.get -> bool
53+
Microsoft.Maui.Media.Locale.Locale(string? language, string? country, string? name, string? id) -> void

src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ static Microsoft.Maui.Authentication.Passkeys.CreateAsync(Microsoft.Maui.Authent
4848
static Microsoft.Maui.Authentication.Passkeys.CreateAsync(string! creationOptionsJson, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Maui.Authentication.PasskeyCreationResponse!>!
4949
static Microsoft.Maui.Authentication.Passkeys.Default.get -> Microsoft.Maui.Authentication.IPasskeys!
5050
static Microsoft.Maui.Authentication.Passkeys.IsSupported.get -> bool
51+
Microsoft.Maui.Media.Locale.Locale(string? language, string? country, string? name, string? id) -> void

src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ static Microsoft.Maui.Authentication.Passkeys.CreateAsync(Microsoft.Maui.Authent
5151
static Microsoft.Maui.Authentication.Passkeys.CreateAsync(string! creationOptionsJson, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<Microsoft.Maui.Authentication.PasskeyCreationResponse!>!
5252
static Microsoft.Maui.Authentication.Passkeys.Default.get -> Microsoft.Maui.Authentication.IPasskeys!
5353
static Microsoft.Maui.Authentication.Passkeys.IsSupported.get -> bool
54+
Microsoft.Maui.Media.Locale.Locale(string? language, string? country, string? name, string? id) -> void

0 commit comments

Comments
 (0)