Skip to content

Commit f78ef22

Browse files
authored
[otel] Log UI language; [i18n] Fix RTL bug (#4062)
1 parent dfe76ac commit f78ef22

File tree

4 files changed

+145
-5
lines changed

4 files changed

+145
-5
lines changed

Backend/Controllers/UserController.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ public async Task<IActionResult> Authenticate([FromBody, BindRequired] Credentia
9595
}
9696
}
9797

98+
/// <summary> Logs the current UI language. </summary>
99+
[HttpPost("uilanguage", Name = "UiLanguage")]
100+
[ProducesResponseType(StatusCodes.Status200OK)]
101+
public IActionResult UiLanguage([FromBody, BindRequired] string uilang)
102+
{
103+
using var activity = OtelService.StartActivityWithTag(otelTagName, "logging current UI language");
104+
activity?.SetTag("ui_language", uilang);
105+
return Ok();
106+
}
107+
98108
/// <summary> Gets the current user. </summary>
99109
[HttpGet("currentuser", Name = "GetCurrentUser")]
100110
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(User))]

src/api/api/user-api.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,55 @@ export const UserApiAxiosParamCreator = function (
497497
options: localVarRequestOptions,
498498
};
499499
},
500+
/**
501+
*
502+
* @param {string} body
503+
* @param {*} [options] Override http request option.
504+
* @throws {RequiredError}
505+
*/
506+
uiLanguage: async (
507+
body: string,
508+
options: any = {}
509+
): Promise<RequestArgs> => {
510+
// verify required parameter 'body' is not null or undefined
511+
assertParamExists("uiLanguage", "body", body);
512+
const localVarPath = `/v1/users/uilanguage`;
513+
// use dummy base URL string because the URL constructor only accepts absolute URLs.
514+
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
515+
let baseOptions;
516+
if (configuration) {
517+
baseOptions = configuration.baseOptions;
518+
}
519+
520+
const localVarRequestOptions = {
521+
method: "POST",
522+
...baseOptions,
523+
...options,
524+
};
525+
const localVarHeaderParameter = {} as any;
526+
const localVarQueryParameter = {} as any;
527+
528+
localVarHeaderParameter["Content-Type"] = "application/json";
529+
530+
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
531+
let headersFromBaseOptions =
532+
baseOptions && baseOptions.headers ? baseOptions.headers : {};
533+
localVarRequestOptions.headers = {
534+
...localVarHeaderParameter,
535+
...headersFromBaseOptions,
536+
...options.headers,
537+
};
538+
localVarRequestOptions.data = serializeDataIfNeeded(
539+
body,
540+
localVarRequestOptions,
541+
configuration
542+
);
543+
544+
return {
545+
url: toPathString(localVarUrlObj),
546+
options: localVarRequestOptions,
547+
};
548+
},
500549
/**
501550
*
502551
* @param {string} userId
@@ -836,6 +885,29 @@ export const UserApiFp = function (configuration?: Configuration) {
836885
configuration
837886
);
838887
},
888+
/**
889+
*
890+
* @param {string} body
891+
* @param {*} [options] Override http request option.
892+
* @throws {RequiredError}
893+
*/
894+
async uiLanguage(
895+
body: string,
896+
options?: any
897+
): Promise<
898+
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>
899+
> {
900+
const localVarAxiosArgs = await localVarAxiosParamCreator.uiLanguage(
901+
body,
902+
options
903+
);
904+
return createRequestFunction(
905+
localVarAxiosArgs,
906+
globalAxios,
907+
BASE_PATH,
908+
configuration
909+
);
910+
},
839911
/**
840912
*
841913
* @param {string} userId
@@ -1017,6 +1089,17 @@ export const UserApiFactory = function (
10171089
.isEmailOrUsernameAvailable(body, options)
10181090
.then((request) => request(axios, basePath));
10191091
},
1092+
/**
1093+
*
1094+
* @param {string} body
1095+
* @param {*} [options] Override http request option.
1096+
* @throws {RequiredError}
1097+
*/
1098+
uiLanguage(body: string, options?: any): AxiosPromise<void> {
1099+
return localVarFp
1100+
.uiLanguage(body, options)
1101+
.then((request) => request(axios, basePath));
1102+
},
10201103
/**
10211104
*
10221105
* @param {string} userId
@@ -1155,6 +1238,20 @@ export interface UserApiIsEmailOrUsernameAvailableRequest {
11551238
readonly body: string;
11561239
}
11571240

1241+
/**
1242+
* Request parameters for uiLanguage operation in UserApi.
1243+
* @export
1244+
* @interface UserApiUiLanguageRequest
1245+
*/
1246+
export interface UserApiUiLanguageRequest {
1247+
/**
1248+
*
1249+
* @type {string}
1250+
* @memberof UserApiUiLanguage
1251+
*/
1252+
readonly body: string;
1253+
}
1254+
11581255
/**
11591256
* Request parameters for updateUser operation in UserApi.
11601257
* @export
@@ -1346,6 +1443,22 @@ export class UserApi extends BaseAPI {
13461443
.then((request) => request(this.axios, this.basePath));
13471444
}
13481445

1446+
/**
1447+
*
1448+
* @param {UserApiUiLanguageRequest} requestParameters Request parameters.
1449+
* @param {*} [options] Override http request option.
1450+
* @throws {RequiredError}
1451+
* @memberof UserApi
1452+
*/
1453+
public uiLanguage(
1454+
requestParameters: UserApiUiLanguageRequest,
1455+
options?: any
1456+
) {
1457+
return UserApiFp(this.configuration)
1458+
.uiLanguage(requestParameters.body, options)
1459+
.then((request) => request(this.axios, this.basePath));
1460+
}
1461+
13491462
/**
13501463
*
13511464
* @param {UserApiUpdateUserRequest} requestParameters Request parameters.

src/backend/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,14 @@ const authenticationUrls = [
5454
];
5555

5656
/** A list of URL patterns for which the frontend explicitly handles errors
57-
* and the blanket error pop ups should be suppressed.*/
57+
* and the blanket error pop-ups should be suppressed.*/
5858
const whiteListedErrorUrls = [
5959
"/merge/retrievedups",
6060
"/speakers/create/",
6161
"/speakers/update/",
6262
"/users/authenticate",
6363
"/users/captcha/",
64+
"/users/uilanguage",
6465
];
6566

6667
// Create an axios instance to allow for attaching interceptors to it.
@@ -728,6 +729,10 @@ export async function authenticateUser(
728729
return user;
729730
}
730731

732+
export async function uiLanguage(uilang: string): Promise<void> {
733+
await userApi.uiLanguage({ body: uilang }, defaultOptions());
734+
}
735+
731736
/** Note: Only usable by site admins. */
732737
export async function getAllUsers(): Promise<User[]> {
733738
return (await userApi.getAllUsers(defaultOptions())).data;

src/i18n/index.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import LanguageDetector from "i18next-browser-languagedetector";
44
import Backend, { type HttpBackendOptions } from "i18next-http-backend";
55
import { initReactI18next } from "react-i18next";
66

7+
import { uiLanguage } from "backend";
78
import { getCurrentUser } from "backend/localStorage";
89
import { i18nFallbacks, i18nLangs } from "types/writingSystem";
910

@@ -32,23 +33,34 @@ i18n
3233
fallbackLng: i18nFallbacks,
3334
interpolation: { escapeValue: false },
3435
},
35-
setDir // Callback function to set the direction ("ltr" vs "rtl") after i18n has initialized
36+
setDir // Callback after i18n has initialized
3637
);
3738

39+
/** Sets the text direction ("ltr" or "rtl") based on the current language. */
3840
function setDir(): void {
3941
document.body.dir = i18n.dir();
4042
}
4143

42-
/** Updates `i18n`'s resolved language to the user-specified ui language (if different).
44+
/** Updates `i18n`'s resolved language to the user-specified UI language (if
45+
* different) and logs the UI language.
4346
*
4447
* Returns `boolean` of whether the resolved language was updated. */
4548
export async function updateLangFromUser(): Promise<boolean> {
4649
const uiLang = getCurrentUser()?.uiLang;
50+
let updated = false;
4751
if (uiLang && uiLang !== i18n.resolvedLanguage) {
4852
await i18n.changeLanguage(uiLang, setDir);
49-
return true;
53+
updated = true;
5054
}
51-
return false;
55+
56+
// Log the UI language even if not changed, because we don't log it on i18n
57+
// init or before login (to avoid having languages different from a user's
58+
// preference cluttering the analytics).
59+
uiLanguage(i18n.resolvedLanguage ?? "").catch((err) =>
60+
console.warn("Failed to log UI language:", err)
61+
);
62+
63+
return updated;
5264
}
5365

5466
export default i18n;

0 commit comments

Comments
 (0)