settings: Convert language picker to dropdown widget.

Fixes #35861.
This commit is contained in:
Vector73
2025-11-01 13:46:14 +00:00
committed by Tim Abbott
parent be18d71624
commit 5704abf8b4
16 changed files with 114 additions and 187 deletions

View File

@@ -20,9 +20,9 @@ chat*](/help/general-chat-topic)), or the language of messages you receive.
<FlattenedSteps>
<NavigationSteps target="settings/preferences" />
1. Under **General**, click the button under **Language**.
1. Select a language. Languages are marked as 100% translated only if every
string in the web, desktop, and mobile apps is translated, including
1. Under **General**, select a language from the dropdown under **Language**.
Languages are marked as 100% translated only if every string
in the web, desktop, and mobile apps is translated, including
administrative UI and all error messages that the API can return.
1. Click **Reload**.
</FlattenedSteps>

View File

@@ -354,12 +354,12 @@ async function test_alert_words_section(page: Page): Promise<void> {
}
async function change_language(page: Page, language_data_code: string): Promise<void> {
await page.waitForSelector("#user-preferences .language_selection_button", {
await page.waitForSelector("#default_language_widget", {
visible: true,
});
await page.click("#user-preferences .language_selection_button");
await common.wait_for_micromodal_to_open(page);
const language_selector = `a[data-code="${CSS.escape(language_data_code)}"]`;
await page.click("#default_language_widget");
await page.waitForSelector(".dropdown-list", {visible: true});
const language_selector = `li[data-unique-id="${CSS.escape(language_data_code)}"]`;
await page.click(language_selector);
}
@@ -370,15 +370,12 @@ async function check_language_setting_status(page: Page): Promise<void> {
}
async function assert_language_changed_to_chinese(page: Page): Promise<void> {
await page.waitForSelector("#user-preferences .language_selection_button", {
await page.waitForSelector("#default_language_widget", {
visible: true,
});
const default_language = await common.get_text_from_selector(
page,
"#user-preferences .language_selection_button",
);
const default_language = await common.get_text_from_selector(page, ".dropdown_widget_value");
assert.strictEqual(
default_language,
default_language.slice(0, 4),
"简体中文",
"Default language has not been changed to Chinese.",
);
@@ -401,7 +398,7 @@ async function test_default_language_setting(page: Page): Promise<void> {
// Check that the saved indicator appears
await check_language_setting_status(page);
await page.click(".reload_link");
await page.waitForSelector("#user-preferences .language_selection_button", {
await page.waitForSelector("#default_language_widget", {
visible: true,
});
await assert_language_changed_to_chinese(page);
@@ -420,7 +417,7 @@ async function test_default_language_setting(page: Page): Promise<void> {
await page.waitForSelector("#user-preferences .general-settings-status", {
visible: true,
});
await page.waitForSelector("#user-preferences .language_selection_button", {
await page.waitForSelector("#default_language_widget", {
visible: true,
});
}

View File

@@ -6,7 +6,7 @@ import render_settings_organization_settings_tip from "../templates/settings/org
import * as bot_data from "./bot_data.ts";
import * as demo_organizations_ui from "./demo_organizations_ui.ts";
import {$t, get_language_name, language_list} from "./i18n.ts";
import {$t, language_list} from "./i18n.ts";
import * as information_density from "./information_density.ts";
import {page_params} from "./page_params.ts";
import * as people from "./people.ts";
@@ -181,7 +181,6 @@ export function build_page(): void {
realm.realm_message_edit_history_visibility_policy,
realm_allow_message_editing: realm.realm_allow_message_editing,
language_list,
realm_default_language_name: get_language_name(realm.realm_default_language),
realm_default_language_code: realm.realm_default_language,
realm_waiting_period_threshold: realm.realm_waiting_period_threshold,
realm_new_stream_announcements_stream_id: realm.realm_new_stream_announcements_stream_id,

View File

@@ -30,7 +30,6 @@ import * as reactions from "./reactions.ts";
import * as recent_view_ui from "./recent_view_ui.ts";
import * as rows from "./rows.ts";
import * as settings_panel_menu from "./settings_panel_menu.ts";
import * as settings_preferences from "./settings_preferences.ts";
import * as settings_toggle from "./settings_toggle.ts";
import * as sidebar_ui from "./sidebar_ui.ts";
import * as spectators from "./spectators.ts";
@@ -913,12 +912,6 @@ export function initialize(): void {
this.blur();
});
$("body").on("click", ".language_selection_widget button", (e) => {
e.preventDefault();
e.stopPropagation();
settings_preferences.launch_default_language_setting_modal();
});
$("body").on("click", "#header-container .brand", (e) => {
if (e.metaKey || e.ctrlKey || e.shiftKey) {
return;

View File

@@ -125,7 +125,7 @@ export function initialize(): void {
popover_menus.hide_current_popover_if_visible(instance);
e.preventDefault();
e.stopPropagation();
settings_preferences.launch_default_language_setting_modal();
settings_preferences.launch_default_language_setting_modal_for_spectator();
});
$popper.on("change", "input[name='theme-select']", (e) => {

View File

@@ -952,7 +952,7 @@ export function dispatch_normal_event(event) {
// a reload is fundamentally required because we
// cannot rerender with the new language the strings
// present in the backend/Jinja2 templates.
settings_preferences.set_default_language_name(event.language_name);
settings_preferences.set_default_language(event.value);
}
if (event.property === "web_home_view") {
left_sidebar_navigation_area.handle_home_view_changed(event.value);

View File

@@ -18,7 +18,6 @@ import * as settings_config from "./settings_config.ts";
import * as settings_data from "./settings_data.ts";
import * as settings_org from "./settings_org.ts";
import * as settings_panel_menu from "./settings_panel_menu.ts";
import * as settings_preferences from "./settings_preferences.ts";
import * as settings_sections from "./settings_sections.ts";
import * as settings_toggle from "./settings_toggle.ts";
import {current_user, realm} from "./state_data.ts";
@@ -132,7 +131,6 @@ export function build_page(): void {
user_can_change_avatar: settings_data.user_can_change_avatar(),
user_can_change_email: settings_data.user_can_change_email(),
user_role_text: people.get_user_type(current_user.user_id),
default_language_name: settings_preferences.user_default_language_name,
default_language: user_settings.default_language,
realm_push_notifications_enabled: realm.realm_push_notifications_enabled,
settings_object: user_settings,

View File

@@ -9,7 +9,7 @@ import render_compose_banner from "../templates/compose_banner/compose_banner.hb
import * as blueslip from "./blueslip.ts";
import * as buttons from "./buttons.ts";
import * as compose_banner from "./compose_banner.ts";
import type {DropdownWidget} from "./dropdown_widget.ts";
import type {DropdownWidget, Option} from "./dropdown_widget.ts";
import * as group_permission_settings from "./group_permission_settings.ts";
import type {
AssignedGroupPermission,
@@ -17,7 +17,7 @@ import type {
RealmGroupSettingNameSupportingAnonymousGroups,
} from "./group_permission_settings.ts";
import * as group_setting_pill from "./group_setting_pill.ts";
import {$t} from "./i18n.ts";
import {$t, get_language_list_columns} from "./i18n.ts";
import * as people from "./people.ts";
import {
realm_default_settings_schema,
@@ -495,6 +495,7 @@ const dropdown_widget_map = new Map<string, DropdownWidget | null>([
["realm_signup_announcements_stream_id", null],
["realm_zulip_update_announcements_stream_id", null],
["realm_default_code_block_language", null],
["realm_default_language", null],
["realm_can_access_all_users_group", null],
["realm_can_create_web_public_channel_group", null],
["folder_id", null],
@@ -839,6 +840,7 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea
case "realm_signup_announcements_stream_id":
case "realm_zulip_update_announcements_stream_id":
case "realm_default_code_block_language":
case "realm_default_language":
case "realm_can_access_all_users_group":
case "realm_can_create_web_public_channel_group":
proposed_val = get_dropdown_list_widget_setting_value($elem);
@@ -886,11 +888,6 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea
assert(elem instanceof HTMLSelectElement);
proposed_val = get_jitsi_server_url_setting_value($(elem), false);
break;
case "realm_default_language":
proposed_val = $("#org-notifications .language_selection_widget").attr(
"data-language-code",
);
break;
default:
if (current_val !== undefined) {
proposed_val = get_input_element_value(elem, typeof current_val);
@@ -2016,3 +2013,11 @@ export function get_channel_folder_value_from_dropdown_widget($elem: JQuery): nu
}
return value;
}
export const language_options = (): Option[] => {
const languages = get_language_list_columns(realm.realm_default_language);
return languages.map((language) => ({
name: language.name_with_percent,
unique_id: language.code,
}));
};

View File

@@ -18,7 +18,7 @@ import {
type RealmGroupSettingNameSupportingAnonymousGroups,
realm_group_setting_name_supporting_anonymous_groups_schema,
} from "./group_permission_settings.ts";
import {$t, $t_html, get_language_name} from "./i18n.ts";
import {$t, $t_html} from "./i18n.ts";
import * as information_density from "./information_density.ts";
import * as keydown_util from "./keydown_util.ts";
import * as loading from "./loading.ts";
@@ -598,6 +598,7 @@ export function discard_realm_property_element_changes(elem: HTMLElement): void
case "realm_signup_announcements_stream_id":
case "realm_zulip_update_announcements_stream_id":
case "realm_default_code_block_language":
case "realm_default_language":
case "realm_can_access_all_users_group":
case "realm_can_create_web_public_channel_group":
assert(typeof property_value === "string" || typeof property_value === "number");
@@ -636,18 +637,6 @@ export function discard_realm_property_element_changes(elem: HTMLElement): void
);
break;
}
case "realm_default_language":
assert(typeof property_value === "string");
$("#org-notifications .language_selection_widget").attr(
"data-language-code",
property_value,
);
$("#org-notifications .language_selection_widget .language_selection_button").text(
// We know this is defined, since we got the `property_value` from a dropdown
// of valid language options.
get_language_name(property_value)!,
);
break;
case "realm_org_type":
assert(typeof property_value === "number");
settings_components.set_input_element_value($elem, property_value);
@@ -1270,6 +1259,11 @@ export let init_dropdown_widgets = (): void => {
combined_code_language_options,
"language",
);
set_up_dropdown_widget(
"realm_default_language",
settings_components.language_options,
"language",
);
set_up_dropdown_widget_for_realm_group_settings();
};

View File

@@ -1,15 +1,16 @@
import $ from "jquery";
import Cookies from "js-cookie";
import assert from "minimalistic-assert";
import type * as tippy from "tippy.js";
import * as z from "zod/mini";
import render_dialog_default_language from "../templates/default_language_modal.hbs";
import * as channel from "./channel.ts";
import * as dialog_widget from "./dialog_widget.ts";
import * as dropdown_widget from "./dropdown_widget.ts";
import * as emojisets from "./emojisets.ts";
import * as hash_parser from "./hash_parser.ts";
import {$t_html, get_language_list_columns, get_language_name} from "./i18n.ts";
import {$t_html, get_language_list_columns} from "./i18n.ts";
import * as information_density from "./information_density.ts";
import * as loading from "./loading.ts";
import * as overlays from "./overlays.ts";
@@ -18,7 +19,6 @@ import type {RealmDefaultSettings} from "./realm_user_settings_defaults.ts";
import * as settings_components from "./settings_components.ts";
import type {RequestOpts} from "./settings_ui.ts";
import * as settings_ui from "./settings_ui.ts";
import {realm} from "./state_data.ts";
import * as ui_report from "./ui_report.ts";
import {user_settings, user_settings_schema} from "./user_settings.ts";
import type {UserSettings} from "./user_settings.ts";
@@ -47,12 +47,7 @@ const meta = {
};
export let user_settings_panel: SettingsPanel;
export let user_default_language_name: string | undefined;
export function set_default_language_name(name: string | undefined): void {
user_default_language_name = name;
}
let default_language_dropdown_widget: dropdown_widget.DropdownWidget;
function change_display_setting(
data: Record<string, string | boolean | number>,
@@ -105,87 +100,8 @@ function spectator_default_language_modal_post_render(): void {
});
}
function org_notification_default_language_modal_post_render(): void {
$("#language_selection_modal")
.find(".language")
.on("click", (e) => {
e.preventDefault();
e.stopPropagation();
dialog_widget.close();
const $link = $(e.target).closest("a[data-code]");
const setting_value = $link.attr("data-code");
assert(setting_value !== undefined);
const new_language = $link.attr("data-name");
assert(new_language !== undefined);
const $language_element = $("#org-notifications .language_selection_widget");
$language_element.find(".language_selection_button").text(new_language);
$language_element.attr("data-language-code", setting_value);
settings_components.save_discard_realm_settings_widget_status_handler(
$("#org-notifications"),
);
});
}
function user_default_language_modal_post_render(): void {
$("#language_selection_modal")
.find(".language")
.on("click", (e) => {
e.preventDefault();
e.stopPropagation();
dialog_widget.close();
const $link = $(e.target).closest("a[data-code]");
const setting_value = $link.attr("data-code");
assert(setting_value !== undefined);
const data = {default_language: setting_value};
const new_language = $link.attr("data-name");
assert(new_language !== undefined);
$("#user-preferences .language_selection_widget .language_selection_button").text(
new_language,
);
$("#user-preferences .language_selection_widget").attr(
"data-language-code",
setting_value,
);
change_display_setting(
data,
$("#settings_content").find(".general-settings-status"),
undefined,
undefined,
$t_html(
{
defaultMessage:
"Saved. Please <z-link>reload</z-link> for the change to take effect.",
},
{
"z-link": (content_html) =>
`<a class='reload_link'>${content_html.join("")}</a>`,
},
),
true,
);
});
}
function default_language_modal_post_render(): void {
if (page_params.is_spectator) {
spectator_default_language_modal_post_render();
} else if (hash_parser.get_current_hash_category() === "organization") {
org_notification_default_language_modal_post_render();
} else {
user_default_language_modal_post_render();
}
}
export function launch_default_language_setting_modal(): void {
let selected_language = user_settings.default_language;
if (hash_parser.get_current_hash_category() === "organization") {
selected_language = realm.realm_default_language;
}
export function launch_default_language_setting_modal_for_spectator(): void {
const selected_language = user_settings.default_language;
const html_body = render_dialog_default_language({
language_list: get_language_list_columns(selected_language),
@@ -199,7 +115,7 @@ export function launch_default_language_setting_modal(): void {
close_on_submit: true,
focus_submit_on_open: true,
single_footer_button: true,
post_render: default_language_modal_post_render,
post_render: spectator_default_language_modal_post_render,
on_click() {
// We perform no actions since the 'close_on_submit' field takes care
// of closing the modal.
@@ -384,6 +300,8 @@ export function set_up(settings_panel: SettingsPanel): void {
},
});
});
render_language_dropdown_widget();
}
export async function report_emojiset_change(settings_panel: SettingsPanel): Promise<void> {
@@ -435,13 +353,6 @@ export function update_page(property: UserSettingsProperty): void {
const $container = $(user_settings_panel.container);
let value = user_settings[property];
// The default_language button text updates to the language
// name and not the value of the user_settings property.
if (property === "default_language") {
$container.find(".language_selection_button").text(user_default_language_name ?? "");
return;
}
// settings_org.set_input_element_value doesn't support radio
// button widgets like these.
if (property === "emojiset" || property === "user_list_style") {
@@ -460,10 +371,57 @@ export function update_page(property: UserSettingsProperty): void {
settings_components.set_input_element_value($input_elem, value);
}
export function initialize(): void {
const user_language_name = get_language_name(user_settings.default_language);
set_default_language_name(user_language_name);
function language_select_callback(
event: JQuery.ClickEvent,
dropdown: tippy.Instance,
widget: dropdown_widget.DropdownWidget,
): void {
dropdown.hide();
event.preventDefault();
event.stopPropagation();
widget.render();
const current_value = widget.current_value;
assert(current_value !== undefined);
const data = {default_language: current_value};
change_display_setting(
data,
$("#settings_content").find(".general-settings-status"),
undefined,
undefined,
$t_html(
{
defaultMessage:
"Saved. Please <z-link>reload</z-link> for the change to take effect.",
},
{
"z-link": (content_html) => `<a class='reload_link'>${content_html.join("")}</a>`,
},
),
true,
);
}
export function set_default_language(default_language: string): void {
if (!default_language_dropdown_widget) {
return;
}
default_language_dropdown_widget.render(default_language);
}
function render_language_dropdown_widget(): void {
default_language_dropdown_widget = new dropdown_widget.DropdownWidget({
widget_name: "default_language",
get_options: settings_components.language_options,
item_click_callback: language_select_callback,
default_id: user_settings.default_language,
$events_container: $("#user-preferences .preferences-settings-form"),
unique_id_type: "string",
});
default_language_dropdown_widget.setup();
}
export function initialize(): void {
user_settings_panel = {
container: "#user-preferences",
settings_object: user_settings,

View File

@@ -628,9 +628,11 @@ input[type="checkbox"] {
margin-top: 0;
}
.language_selection_widget .language_selection_button {
text-decoration: none;
min-width: 0;
.realm_default_language-dropdown-list-container,
.default_language-dropdown-list-container {
/* This is same as saved snippets dropdown's width and
also makes all language names fit in the same line. */
width: 17.8571em;
}
#user_enter_sends_label kbd,

View File

@@ -1,16 +0,0 @@
<div class="language_selection_widget input-group prop-element" id="id_{{section_name}}" data-language-code="{{language_code}}" data-setting-widget-type="language-setting">
<label class="settings-field-label" for="id_language_selection_button">
{{section_title}}
{{#if help_link_widget_link}}
{{> ../help_link_widget link=help_link_widget_link }}
{{/if}}
</label>
{{> ../components/action_button
label=setting_value
custom_classes="language_selection_button tippy-zulip-delayed-tooltip"
data-tippy-content=(t "Change language")
id="id_language_selection_button"
intent="neutral"
attention="quiet"
}}
</div>

View File

@@ -7,14 +7,11 @@
{{> settings_save_discard_widget section_name="notifications" }}
</div>
<div class="inline-block organization-settings-parent">
<div class="realm_default_language">
{{> language_selection_widget
section_name="realm_default_language"
setting_value=realm_default_language_name
language_code=realm_default_language_code
section_title=admin_settings_label.realm_default_language
help_link_widget_link="/help/configure-organization-language"}}
</div>
{{> ../dropdown_widget_with_label
widget_name="realm_default_language"
label=admin_settings_label.realm_default_language
value_type="string"
help_link="/help/configure-organization-language"}}
{{> ../dropdown_widget_with_label
widget_name="realm_new_stream_announcements_stream_id"

View File

@@ -6,11 +6,11 @@
{{> settings_save_discard_widget section_name="general-settings" show_only_indicator=(not for_realm_settings) }}
</div>
{{#unless for_realm_settings}}
{{> language_selection_widget
section_name="default_language_name"
setting_value=default_language_name
section_title=settings_label.default_language_settings_label
language_code=default_language }}
{{> ../dropdown_widget_with_label
widget_name="default_language"
label=settings_label.default_language_settings_label
value_type="string"
help_link="/help/change-your-language"}}
{{/unless}}
<div class="input-group">

View File

@@ -1156,7 +1156,7 @@ run_test("stream_typing_message_edit", ({override}) => {
});
run_test("user_settings", ({override}) => {
settings_preferences.set_default_language_name = () => {};
settings_preferences.set_default_language = () => {};
let event = event_fixtures.user_settings__default_language;
override(user_settings, "default_language", "en");
override(settings_preferences, "update_page", noop);

View File

@@ -150,18 +150,18 @@ function test_submit_settings_form(override, submit_form) {
$save_button_header = stubs.$save_button_header;
$save_button_header.attr("id", `org-${subsection}`);
const $realm_default_language_elem = $("#id_realm_default_language");
$realm_default_language_elem.val("en");
$realm_default_language_elem.attr("id", "id_realm_default_language");
const $realm_topics_policy_elem = $("#id_realm_topics_policy");
$realm_topics_policy_elem.val("disable_empty_topic");
$realm_topics_policy_elem.attr("id", "id_realm_topics_policy");
$subsection_elem = $(`#org-${CSS.escape(subsection)}`);
$subsection_elem.set_find_results(".prop-element", [$realm_default_language_elem]);
$subsection_elem.set_find_results(".prop-element", [$realm_topics_policy_elem]);
submit_form.call({to_$: () => $(".save-button")}, ev);
assert.ok(patched);
const expected_value = {
default_language: "en",
topics_policy: "disable_empty_topic",
};
assert.deepEqual(data, expected_value);