Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 81f29d1

Browse files
authored
Move language settings to 'preferences' (#12723)
* Move language settings to 'preferences' Their new home is in this tab * Update snapshot * Move playwright test code * Add test * tests * Update screenshot
1 parent dcf7643 commit 81f29d1

File tree

9 files changed

+198
-110
lines changed

9 files changed

+198
-110
lines changed

playwright/e2e/settings/general-user-settings-tab.spec.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,24 +73,6 @@ test.describe("General user settings tab", () => {
7373
// Assert that the add button is rendered
7474
await expect(phoneNumbers.getByRole("button", { name: "Add" })).toBeVisible();
7575

76-
// Check language and region setting dropdown
77-
const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput");
78-
await languageInput.scrollIntoViewIfNeeded();
79-
// Check the default value
80-
await expect(languageInput.getByText("English")).toBeVisible();
81-
// Click the button to display the dropdown menu
82-
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
83-
// Assert that the default option is rendered and highlighted
84-
languageInput.getByRole("option", { name: /Albanian/ });
85-
await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass(
86-
/mx_Dropdown_option_highlight/,
87-
);
88-
await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible();
89-
// Click again to close the dropdown
90-
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
91-
// Assert that the default value is rendered again
92-
await expect(languageInput.getByText("English")).toBeVisible();
93-
9476
const setIntegrationManager = uut.locator(".mx_SetIntegrationManager");
9577
await setIntegrationManager.scrollIntoViewIfNeeded();
9678
await expect(

playwright/e2e/settings/preferences-user-settings-tab.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2023 Suguru Hirahara
3+
Copyright 2024 The Matrix.org Foundation C.I.C.
34
45
Licensed under the Apache License, Version 2.0 (the "License");
56
you may not use this file except in compliance with the License.
@@ -19,6 +20,10 @@ import { test, expect } from "../../element-web-test";
1920
test.describe("Preferences user settings tab", () => {
2021
test.use({
2122
displayName: "Bob",
23+
uut: async ({ app, user }, use) => {
24+
const locator = await app.settings.openUserSettings("Preferences");
25+
await use(locator);
26+
},
2227
});
2328

2429
test("should be rendered properly", async ({ app, user }) => {
@@ -28,4 +33,24 @@ test.describe("Preferences user settings tab", () => {
2833
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
2934
await expect(tab).toMatchScreenshot();
3035
});
36+
37+
test("should be able to change the app language", async ({ uut, user }) => {
38+
// Check language and region setting dropdown
39+
const languageInput = uut.locator(".mx_GeneralUserSettingsTab_section_languageInput");
40+
await languageInput.scrollIntoViewIfNeeded();
41+
// Check the default value
42+
await expect(languageInput.getByText("English")).toBeVisible();
43+
// Click the button to display the dropdown menu
44+
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
45+
// Assert that the default option is rendered and highlighted
46+
languageInput.getByRole("option", { name: /Albanian/ });
47+
await expect(languageInput.getByRole("option", { name: /Albanian/ })).toHaveClass(
48+
/mx_Dropdown_option_highlight/,
49+
);
50+
await expect(languageInput.getByRole("option", { name: /Deutsch/ })).toBeVisible();
51+
// Click again to close the dropdown
52+
await languageInput.getByRole("button", { name: "Language Dropdown" }).click();
53+
// Assert that the default value is rendered again
54+
await expect(languageInput.getByText("English")).toBeVisible();
55+
});
3156
});
-3.75 KB
Loading

res/css/views/settings/tabs/user/_GeneralUserSettingsTab.pcss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,8 @@ limitations under the License.
3434
margin-right: $spacing-8;
3535
margin-bottom: 2px;
3636
}
37+
38+
.mx_GeneralUserSettingsTab_section_hint {
39+
font: var(--cpd-font-body-sm-regular);
40+
color: var(--cpd-color-text-secondary);
41+
}

src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,17 @@ import { logger } from "matrix-js-sdk/src/logger";
2222

2323
import { UserFriendlyError, _t } from "../../../../../languageHandler";
2424
import UserProfileSettings from "../../UserProfileSettings";
25-
import * as languageHandler from "../../../../../languageHandler";
2625
import SettingsStore from "../../../../../settings/SettingsStore";
27-
import LanguageDropdown from "../../../elements/LanguageDropdown";
28-
import SpellCheckSettings from "../../SpellCheckSettings";
2926
import AccessibleButton from "../../../elements/AccessibleButton";
3027
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
31-
import PlatformPeg from "../../../../../PlatformPeg";
3228
import Modal from "../../../../../Modal";
33-
import { SettingLevel } from "../../../../../settings/SettingLevel";
3429
import { UIFeature } from "../../../../../settings/UIFeature";
3530
import ErrorDialog, { extractErrorMessageFromError } from "../../../dialogs/ErrorDialog";
3631
import ChangePassword from "../../ChangePassword";
3732
import SetIntegrationManager from "../../SetIntegrationManager";
38-
import ToggleSwitch from "../../../elements/ToggleSwitch";
39-
import { IS_MAC } from "../../../../../Keyboard";
4033
import SettingsTab from "../SettingsTab";
4134
import { SettingsSection } from "../../shared/SettingsSection";
4235
import SettingsSubsection, { SettingsSubsectionText } from "../../shared/SettingsSubsection";
43-
import { SettingsSubsectionHeading } from "../../shared/SettingsSubsectionHeading";
4436
import { SDKContext } from "../../../../../contexts/SDKContext";
4537
import UserPersonalInfoSettings from "../../UserPersonalInfoSettings";
4638

@@ -49,9 +41,6 @@ interface IProps {
4941
}
5042

5143
interface IState {
52-
language: string;
53-
spellCheckEnabled?: boolean;
54-
spellCheckLanguages: string[];
5544
canChangePassword: boolean;
5645
idServerName?: string;
5746
externalAccountManagementUrl?: string;
@@ -69,9 +58,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
6958
this.context = context;
7059

7160
this.state = {
72-
language: languageHandler.getCurrentLanguage(),
73-
spellCheckEnabled: false,
74-
spellCheckLanguages: [],
7561
canChangePassword: false,
7662
canMake3pidChanges: false,
7763
canSetDisplayName: false,
@@ -81,21 +67,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
8167
this.getCapabilities();
8268
}
8369

84-
public async componentDidMount(): Promise<void> {
85-
const plat = PlatformPeg.get();
86-
const [spellCheckEnabled, spellCheckLanguages] = await Promise.all([
87-
plat?.getSpellCheckEnabled(),
88-
plat?.getSpellCheckLanguages(),
89-
]);
90-
91-
if (spellCheckLanguages) {
92-
this.setState({
93-
spellCheckEnabled,
94-
spellCheckLanguages,
95-
});
96-
}
97-
}
98-
9970
private async getCapabilities(): Promise<void> {
10071
const cli = this.context.client!;
10172

@@ -127,28 +98,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
12798
});
12899
}
129100

130-
private onLanguageChange = (newLanguage: string): void => {
131-
if (this.state.language === newLanguage) return;
132-
133-
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
134-
this.setState({ language: newLanguage });
135-
const platform = PlatformPeg.get();
136-
if (platform) {
137-
platform.setLanguage([newLanguage]);
138-
platform.reload();
139-
}
140-
};
141-
142-
private onSpellCheckLanguagesChange = (languages: string[]): void => {
143-
this.setState({ spellCheckLanguages: languages });
144-
PlatformPeg.get()?.setSpellCheckLanguages(languages);
145-
};
146-
147-
private onSpellCheckEnabledChange = (spellCheckEnabled: boolean): void => {
148-
this.setState({ spellCheckEnabled });
149-
PlatformPeg.get()?.setSpellCheckEnabled(spellCheckEnabled);
150-
};
151-
152101
private onPasswordChangeError = (err: Error): void => {
153102
logger.error("Failed to change password: " + err);
154103

@@ -228,37 +177,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
228177
);
229178
}
230179

231-
private renderLanguageSection(): JSX.Element {
232-
// TODO: Convert to new-styled Field
233-
return (
234-
<SettingsSubsection heading={_t("settings|general|language_section")} stretchContent>
235-
<LanguageDropdown
236-
className="mx_GeneralUserSettingsTab_section_languageInput"
237-
onOptionChange={this.onLanguageChange}
238-
value={this.state.language}
239-
/>
240-
</SettingsSubsection>
241-
);
242-
}
243-
244-
private renderSpellCheckSection(): JSX.Element {
245-
const heading = (
246-
<SettingsSubsectionHeading heading={_t("settings|general|spell_check_section")}>
247-
<ToggleSwitch checked={!!this.state.spellCheckEnabled} onChange={this.onSpellCheckEnabledChange} />
248-
</SettingsSubsectionHeading>
249-
);
250-
return (
251-
<SettingsSubsection heading={heading} stretchContent>
252-
{this.state.spellCheckEnabled && !IS_MAC && (
253-
<SpellCheckSettings
254-
languages={this.state.spellCheckLanguages}
255-
onLanguagesChange={this.onSpellCheckLanguagesChange}
256-
/>
257-
)}
258-
</SettingsSubsection>
259-
);
260-
}
261-
262180
private renderManagementSection(): JSX.Element {
263181
// TODO: Improve warning text for account deactivation
264182
return (
@@ -283,9 +201,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
283201
}
284202

285203
public render(): React.ReactNode {
286-
const plaf = PlatformPeg.get();
287-
const supportsMultiLanguageSpellCheck = plaf?.supportsSpellCheckSettings();
288-
289204
let accountManagementSection: JSX.Element | undefined;
290205
const isAccountManagedExternally = !!this.state.externalAccountManagementUrl;
291206
if (SettingsStore.getValue(UIFeature.Deactivate) && !isAccountManagedExternally) {
@@ -302,8 +217,6 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
302217
/>
303218
<UserPersonalInfoSettings canMake3pidChanges={this.state.canMake3pidChanges} />
304219
{this.renderAccountSection()}
305-
{this.renderLanguageSection()}
306-
{supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null}
307220
</SettingsSection>
308221
{this.renderIntegrationManagerSection()}
309222
{accountManagementSection}

src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
import React from "react";
18+
import React, { useCallback, useEffect, useState } from "react";
1919

20-
import { _t } from "../../../../../languageHandler";
20+
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
2121
import { UseCase } from "../../../../../settings/enums/UseCase";
2222
import SettingsStore from "../../../../../settings/SettingsStore";
2323
import Field from "../../../elements/Field";
@@ -33,6 +33,11 @@ import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingP
3333
import SettingsSubsection from "../../shared/SettingsSubsection";
3434
import SettingsTab from "../SettingsTab";
3535
import { SettingsSection } from "../../shared/SettingsSection";
36+
import LanguageDropdown from "../../../elements/LanguageDropdown";
37+
import PlatformPeg from "../../../../../PlatformPeg";
38+
import { IS_MAC } from "../../../../../Keyboard";
39+
import SpellCheckSettings from "../../SpellCheckSettings";
40+
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
3641

3742
interface IProps {
3843
closeSettingsFn(success: boolean): void;
@@ -44,6 +49,79 @@ interface IState {
4449
readMarkerOutOfViewThresholdMs: string;
4550
}
4651

52+
const LanguageSection: React.FC = () => {
53+
const [language, setLanguage] = useState(getCurrentLanguage());
54+
55+
const onLanguageChange = useCallback(
56+
(newLanguage: string) => {
57+
if (language === newLanguage) return;
58+
59+
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage);
60+
setLanguage(newLanguage);
61+
const platform = PlatformPeg.get();
62+
if (platform) {
63+
platform.setLanguage([newLanguage]);
64+
platform.reload();
65+
}
66+
},
67+
[language],
68+
);
69+
70+
return (
71+
<div className="mx_SettingsSubsection_contentStretch">
72+
{_t("settings|general|application_language")}
73+
<LanguageDropdown
74+
className="mx_GeneralUserSettingsTab_section_languageInput"
75+
onOptionChange={onLanguageChange}
76+
value={language}
77+
/>
78+
<div className="mx_GeneralUserSettingsTab_section_hint">
79+
{_t("settings|general|application_language_reload_hint")}
80+
</div>
81+
</div>
82+
);
83+
};
84+
85+
const SpellCheckSection: React.FC = () => {
86+
const [spellCheckEnabled, setSpellCheckEnabled] = useState<boolean | undefined>();
87+
const [spellCheckLanguages, setSpellCheckLanguages] = useState<string[] | undefined>();
88+
89+
useEffect(() => {
90+
(async () => {
91+
const plaf = PlatformPeg.get();
92+
const [enabled, langs] = await Promise.all([plaf?.getSpellCheckEnabled(), plaf?.getSpellCheckLanguages()]);
93+
94+
setSpellCheckEnabled(enabled);
95+
setSpellCheckLanguages(langs || undefined);
96+
})();
97+
}, []);
98+
99+
const onSpellCheckEnabledChange = useCallback((enabled: boolean) => {
100+
setSpellCheckEnabled(enabled);
101+
PlatformPeg.get()?.setSpellCheckEnabled(enabled);
102+
}, []);
103+
104+
const onSpellCheckLanguagesChange = useCallback((languages: string[]): void => {
105+
setSpellCheckLanguages(languages);
106+
PlatformPeg.get()?.setSpellCheckLanguages(languages);
107+
}, []);
108+
109+
if (!PlatformPeg.get()?.supportsSpellCheckSettings()) return null;
110+
111+
return (
112+
<>
113+
<LabelledToggleSwitch
114+
label={_t("settings|general|allow_spellcheck")}
115+
value={Boolean(spellCheckEnabled)}
116+
onChange={onSpellCheckEnabledChange}
117+
/>
118+
{spellCheckEnabled && spellCheckLanguages !== undefined && !IS_MAC && (
119+
<SpellCheckSettings languages={spellCheckLanguages} onLanguagesChange={onSpellCheckLanguagesChange} />
120+
)}
121+
</>
122+
);
123+
};
124+
47125
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
48126
private static ROOM_LIST_SETTINGS = ["breadcrumbs", "FTUE.userOnboardingButton"];
49127

@@ -146,6 +224,12 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
146224
return (
147225
<SettingsTab data-testid="mx_PreferencesUserSettingsTab">
148226
<SettingsSection>
227+
{/* The heading string is still 'general' from where it was moved, but this section should become 'general' */}
228+
<SettingsSubsection heading={_t("settings|general|language_section")}>
229+
<LanguageSection />
230+
<SpellCheckSection />
231+
</SettingsSubsection>
232+
149233
{roomListSettings.length > 0 && (
150234
<SettingsSubsection heading={_t("settings|preferences|room_list_heading")}>
151235
{this.renderGroup(roomListSettings)}

src/i18n/strings/en_EN.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2463,6 +2463,9 @@
24632463
"add_msisdn_dialog_title": "Add Phone Number",
24642464
"add_msisdn_instructions": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
24652465
"add_msisdn_misconfigured": "The add / bind with MSISDN flow is misconfigured",
2466+
"allow_spellcheck": "Allow spell check",
2467+
"application_language": "Application language",
2468+
"application_language_reload_hint": "The app will reload after selecting another language",
24662469
"avatar_remove_progress": "Removing image...",
24672470
"avatar_save_progress": "Uploading image...",
24682471
"avatar_upload_error_text": "The file format is not supported or the image is larger than %(size)s.",
@@ -2516,7 +2519,7 @@
25162519
"identity_server_no_token": "No identity access token found",
25172520
"identity_server_not_set": "Identity server not set",
25182521
"incorrect_msisdn_verification": "Incorrect verification code",
2519-
"language_section": "Language and region",
2522+
"language_section": "Language",
25202523
"msisdn_in_use": "This phone number is already in use",
25212524
"msisdn_label": "Phone Number",
25222525
"msisdn_verification_field_label": "Verification code",
@@ -2532,7 +2535,6 @@
25322535
"remove_email_prompt": "Remove %(email)s?",
25332536
"remove_msisdn_prompt": "Remove %(phone)s?",
25342537
"spell_check_locale_placeholder": "Choose a locale",
2535-
"spell_check_section": "Spell check",
25362538
"unable_to_load_emails": "Unable to load email addresses",
25372539
"unable_to_load_msisdns": "Unable to load phone numbers",
25382540
"username": "Username"

0 commit comments

Comments
 (0)