Skip to content

Commit c31946d

Browse files
authored
Fix UI Culture bug (#1901)
1 parent 3a0788b commit c31946d

File tree

6 files changed

+66
-20
lines changed

6 files changed

+66
-20
lines changed

src/Microsoft.Azure.SignalR.Common/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public static class QueryParameter
7777
public const string OriginalPath = "asrs.op";
7878
public const string ConnectionRequestId = "asrs_request_id";
7979
public const string RequestCulture = "asrs_lang";
80+
public const string RequestUICulture = "asrs_ui_lang";
8081
}
8182

8283
public static class CustomizedPingTimer

src/Microsoft.Azure.SignalR/HubHost/NegotiateHandler.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public async Task<NegotiationResponse> Process(HttpContext context)
8080
var claims = BuildClaims(context);
8181
var request = context.Request;
8282
var cultureName = context.Features.Get<IRequestCultureFeature>()?.RequestCulture.Culture.Name;
83+
var uiCultureName = context.Features.Get<IRequestCultureFeature>()?.RequestCulture.UICulture.Name;
8384
var originalPath = GetOriginalPath(request.Path);
8485
var provider = _endpointManager.GetEndpointProvider(_router.GetNegotiateEndpoint(context, _endpointManager.GetEndpoints(_hubName)));
8586

@@ -88,7 +89,11 @@ public async Task<NegotiationResponse> Process(HttpContext context)
8889
return null;
8990
}
9091

91-
var queryString = GetQueryString(request.QueryString.HasValue ? request.QueryString.Value.Substring(1) : null, cultureName);
92+
var queryString = GetQueryString(
93+
request.QueryString.HasValue ? request.QueryString.Value.Substring(1) : null,
94+
cultureName,
95+
uiCultureName
96+
);
9297

9398
return new NegotiationResponse
9499
{
@@ -99,7 +104,7 @@ public async Task<NegotiationResponse> Process(HttpContext context)
99104
};
100105
}
101106

102-
private string GetQueryString(string originalQueryString, string cultureName)
107+
private string GetQueryString(string originalQueryString, string cultureName, string uiCultureName)
103108
{
104109
var clientRequestId = _connectionRequestIdProvider.GetRequestId();
105110
if (clientRequestId != null)
@@ -112,6 +117,10 @@ private string GetQueryString(string originalQueryString, string cultureName)
112117
{
113118
queryString += $"&{Constants.QueryParameter.RequestCulture}={cultureName}";
114119
}
120+
if (!string.IsNullOrEmpty(uiCultureName))
121+
{
122+
queryString += $"&{Constants.QueryParameter.RequestUICulture}={uiCultureName}";
123+
}
115124

116125
return originalQueryString != null
117126
? $"{originalQueryString}&{queryString}"

src/Microsoft.Azure.SignalR/ServerConnections/ClientConnectionContext.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ private static void ProcessQuery(string queryString, out string originalPath)
265265
{
266266
SetCurrentThreadCulture(culture.FirstOrDefault());
267267
}
268+
if (query.TryGetValue(Constants.QueryParameter.RequestUICulture, out var uiCulture))
269+
{
270+
SetCurrentThreadUiCulture(uiCulture.FirstOrDefault());
271+
}
268272
if (query.TryGetValue(Constants.QueryParameter.OriginalPath, out var path))
269273
{
270274
originalPath = path.FirstOrDefault();
@@ -277,9 +281,22 @@ private static void SetCurrentThreadCulture(string cultureName)
277281
{
278282
try
279283
{
280-
var requestCulture = new RequestCulture(cultureName);
281-
CultureInfo.CurrentCulture = requestCulture.Culture;
282-
CultureInfo.CurrentUICulture = requestCulture.UICulture;
284+
CultureInfo.CurrentCulture = new CultureInfo(cultureName);
285+
}
286+
catch (Exception)
287+
{
288+
// skip invalid culture, normal won't hit.
289+
}
290+
}
291+
}
292+
293+
private static void SetCurrentThreadUiCulture(string uiCultureName)
294+
{
295+
if (!string.IsNullOrEmpty(uiCultureName))
296+
{
297+
try
298+
{
299+
CultureInfo.CurrentUICulture = new CultureInfo(uiCultureName);
283300
}
284301
catch (Exception)
285302
{

test/Microsoft.Azure.SignalR.Tests/Microsoft.Azure.SignalR.Tests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
<DefineConstants>MULTIFRAMEWORK</DefineConstants>
88
</PropertyGroup>
99

10+
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
11+
<DefineConstants>OS_WINDOWS</DefineConstants>
12+
</PropertyGroup>
13+
1014
<ItemGroup>
1115
<ProjectReference Include="..\Microsoft.Azure.SignalR.Tests.Common\Microsoft.Azure.SignalR.Tests.Common.csproj" />
1216
</ItemGroup>

test/Microsoft.Azure.SignalR.Tests/NegotiateHandlerFacts.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ public async Task TestNegotiateHandlerRespectClientRequestCulture()
459459
QueryString = "?endpoint=chosen"
460460
};
461461
features.Set<IHttpRequestFeature>(requestFeature);
462-
var customCulture = new RequestCulture("ar-SA");
462+
var customCulture = new RequestCulture("ar-SA", "en-US");
463463
features.Set<IRequestCultureFeature>(
464464
new RequestCultureFeature(customCulture,
465465
new AcceptLanguageHeaderRequestCultureProvider()));
@@ -470,7 +470,9 @@ public async Task TestNegotiateHandlerRespectClientRequestCulture()
470470
var negotiateResponse = await handler.Process(httpContext);
471471

472472
var queryContainsCulture = negotiateResponse.Url.Contains($"{Constants.QueryParameter.RequestCulture}=ar-SA");
473+
var queryContainsUICulture = negotiateResponse.Url.Contains($"{Constants.QueryParameter.RequestUICulture}=en-US");
473474
Assert.True(queryContainsCulture);
475+
Assert.True(queryContainsUICulture);
474476
}
475477

476478
[Theory]

test/Microsoft.Azure.SignalR.Tests/ServiceContextFacts.cs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,26 +199,39 @@ public void ServiceConnectionContextRemoteIpTest(string xff, bool canBeParsed, s
199199
}
200200

201201
[Theory]
202-
[InlineData("&asrs_lang=ar-SA", true, "ar-SA")]
203-
[InlineData("&asrs_lang=zh-CN", true, "zh-CN")]
204-
[InlineData("", false, null)]
205-
[InlineData("&arsa_lang=", false, null)]
206-
[InlineData("&arsa_lang=123", false, null)] // invalid culture won't change default en-US
207-
public void ServiceConnectionContextCultureTest(string cultureQuery, bool isValid, string result)
202+
// For Linux and Mac, `CultureInfo` ctor doesn't treat invalid culture string as an invalid one. See https://github.com/dotnet/runtime/issues/11590
203+
// If `CultureInfo` ctor treats the culture string as an invalid one, the default culture won't be changed and `parsedCulture` will be ignored.
204+
// Culture / UICulture by default is same as OS, typically en-US. See https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.currentuiculture?view=net-8.0&redirectedfrom=MSDN#remarks
205+
[InlineData("&asrs_lang=ar-SA", true, "ar-SA", false, null)]
206+
[InlineData("&asrs_lang=zh-CN", true, "zh-CN", false, null)]
207+
[InlineData("&asrs_ui_lang=ar-SA", false, null, true, "ar-SA")]
208+
[InlineData("&asrs_ui_lang=zh-CN", false, null, true, "zh-CN")]
209+
[InlineData("&asrs_lang=ar-SA&asrs_ui_lang=zh-CN", true, "ar-SA", true, "zh-CN")]
210+
[InlineData("&asrs_lang=zh-CN&asrs_ui_lang=ar-SA", true, "zh-CN", true, "ar-SA")]
211+
#if OS_WINDOWS
212+
[InlineData("", false, null, false, null)]
213+
[InlineData("&arsa_lang=", false, null, false, null)]
214+
[InlineData("&arsa_lang=123", false, null, false, null)]
215+
[InlineData("&arsa_ui_lang=", false, null, false, null)]
216+
[InlineData("&arsa_ui_lang=123", false, null, false, null)]
217+
[InlineData("&asrs_lang=ar-SA&asrs_ui_lang=", true, "ar-SA", false, null)]
218+
[InlineData("&asrs_lang=ar-SA&asrs_ui_lang=123", true, "ar-SA", false, null)]
219+
[InlineData("&asrs_lang=&asrs_ui_lang=ar-SA", false, null, true, "ar-SA")]
220+
[InlineData("&asrs_lang=123&asrs_ui_lang=ar-SA", false, null, true, "ar-SA")]
221+
#endif
222+
public void ServiceConnectionContextCultureTest(string cultureQuery, bool isCultureValid, string parsedCulture, bool isUiCultureValid, string parsedUiCulture)
208223
{
209224
var queryString = $"?{cultureQuery}";
210225
var originalCulture = CultureInfo.CurrentCulture.Name;
226+
var originalUiCulture = CultureInfo.CurrentUICulture.Name;
211227

212228
_ = new ClientConnectionContext(new OpenConnectionMessage("1", new Claim[0], EmptyHeaders, queryString));
229+
230+
var expectedCulture = isCultureValid ? parsedCulture : originalCulture;
231+
var expectedUiCulture = isUiCultureValid ? parsedUiCulture : originalUiCulture;
213232

214-
if (isValid)
215-
{
216-
Assert.Equal(result, CultureInfo.CurrentCulture.Name);
217-
}
218-
else
219-
{
220-
Assert.Equal(originalCulture, CultureInfo.CurrentCulture.Name);
221-
}
233+
Assert.Equal(expectedCulture, CultureInfo.CurrentCulture.Name);
234+
Assert.Equal(expectedUiCulture, CultureInfo.CurrentUICulture.Name);
222235
}
223236
}
224237
}

0 commit comments

Comments
 (0)