From d761077dacc7320e0bcfd465e9c7228edd48e96b Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Mon, 12 Apr 2021 16:44:36 -0700 Subject: [PATCH] compose: Convert compose_error messages to FormatJS. Signed-off-by: Anders Kaseorg --- frontend_tests/node_tests/compose.js | 54 ++++++++++++------ static/js/compose.js | 82 ++++++++++++++++++---------- static/js/reminder.js | 19 +++++-- tools/lib/template_parser.py | 1 + tools/linter_lib/custom_check.py | 2 +- 5 files changed, 107 insertions(+), 51 deletions(-) diff --git a/frontend_tests/node_tests/compose.js b/frontend_tests/node_tests/compose.js index 863a67912a..bee5abf61c 100644 --- a/frontend_tests/node_tests/compose.js +++ b/frontend_tests/node_tests/compose.js @@ -6,7 +6,7 @@ const {JSDOM} = require("jsdom"); const MockDate = require("mockdate"); const {stub_templates} = require("../zjsunit/handlebars"); -const {i18n} = require("../zjsunit/i18n"); +const {$t_html, i18n} = require("../zjsunit/i18n"); const {mock_cjs, mock_esm, set_global, zrequire} = require("../zjsunit/namespace"); const {run_test} = require("../zjsunit/test"); const blueslip = require("../zjsunit/zblueslip"); @@ -183,7 +183,7 @@ test_ui("validate_stream_message_address_info", () => { assert(!compose.validate_stream_message_address_info("Frontend")); assert.equal( $("#compose-error-msg").html(), - "translated:

The stream Frontend does not exist.

Manage your subscriptions on your Streams page.

", + "translated HTML:

The stream Frontend does not exist.

Manage your subscriptions on your Streams page.

", ); channel.post = (payload) => { @@ -191,7 +191,10 @@ test_ui("validate_stream_message_address_info", () => { payload.error({status: 500}); }; assert(!compose.validate_stream_message_address_info("social")); - assert.equal($("#compose-error-msg").html(), i18n.t("Error checking subscription")); + assert.equal( + $("#compose-error-msg").html(), + $t_html({defaultMessage: "Error checking subscription"}), + ); }); test_ui("validate", () => { @@ -229,7 +232,10 @@ test_ui("validate", () => { assert(!$("#sending-indicator").visible()); assert(!$("#compose-send-button").is_focused()); assert.equal($("#compose-send-button").prop("disabled"), false); - assert.equal($("#compose-error-msg").html(), i18n.t("You have nothing to send!")); + assert.equal( + $("#compose-error-msg").html(), + $t_html({defaultMessage: "You have nothing to send!"}), + ); reminder.is_deferred_delivery = () => true; compose.validate(); @@ -249,7 +255,9 @@ test_ui("validate", () => { assert(zephyr_checked); assert.equal( $("#compose-error-msg").html(), - i18n.t("You need to be running Zephyr mirroring in order to send messages!"), + $t_html({ + defaultMessage: "You need to be running Zephyr mirroring in order to send messages!", + }), ); initialize_pm_pill(); @@ -262,7 +270,7 @@ test_ui("validate", () => { assert(!compose.validate()); assert.equal( $("#compose-error-msg").html(), - i18n.t("Please specify at least one valid recipient"), + $t_html({defaultMessage: "Please specify at least one valid recipient"}), ); initialize_pm_pill(); @@ -273,7 +281,7 @@ test_ui("validate", () => { assert.equal( $("#compose-error-msg").html(), - i18n.t("Please specify at least one valid recipient", {}), + $t_html({defaultMessage: "Please specify at least one valid recipient"}), ); compose_state.private_message_recipient("foo@zulip.com,alice@zulip.com"); @@ -281,7 +289,7 @@ test_ui("validate", () => { assert.equal( $("#compose-error-msg").html(), - i18n.t("Please specify at least one valid recipient", {}), + $t_html({defaultMessage: "Please specify at least one valid recipient"}), ); people.add_active_user(bob); @@ -295,13 +303,19 @@ test_ui("validate", () => { compose_state.set_message_type("stream"); compose_state.stream_name(""); assert(!compose.validate()); - assert.equal($("#compose-error-msg").html(), i18n.t("Please specify a stream")); + assert.equal( + $("#compose-error-msg").html(), + $t_html({defaultMessage: "Please specify a stream"}), + ); compose_state.stream_name("Denmark"); page_params.realm_mandatory_topics = true; compose_state.topic(""); assert(!compose.validate()); - assert.equal($("#compose-error-msg").html(), i18n.t("Please specify a topic")); + assert.equal( + $("#compose-error-msg").html(), + $t_html({defaultMessage: "Please specify a topic"}), + ); }); test_ui("get_invalid_recipient_emails", (override) => { @@ -412,7 +426,9 @@ test_ui("validate_stream_message", (override) => { assert(!compose.validate()); assert.equal( $("#compose-error-msg").html(), - i18n.t("You do not have permission to use wildcard mentions in this stream."), + $t_html({ + defaultMessage: "You do not have permission to use wildcard mentions in this stream.", + }), ); }); @@ -435,7 +451,7 @@ test_ui("test_validate_stream_message_post_policy_admin_only", () => { assert(!compose.validate()); assert.equal( $("#compose-error-msg").html(), - i18n.t("Only organization admins are allowed to post to this stream."), + $t_html({defaultMessage: "Only organization admins are allowed to post to this stream."}), ); // Reset error message. @@ -449,7 +465,7 @@ test_ui("test_validate_stream_message_post_policy_admin_only", () => { assert(!compose.validate()); assert.equal( $("#compose-error-msg").html(), - i18n.t("Only organization admins are allowed to post to this stream."), + $t_html({defaultMessage: "Only organization admins are allowed to post to this stream."}), ); }); @@ -469,7 +485,7 @@ test_ui("test_validate_stream_message_post_policy_full_members_only", () => { assert(!compose.validate()); assert.equal( $("#compose-error-msg").html(), - i18n.t("Guests are not allowed to post to this stream."), + $t_html({defaultMessage: "Guests are not allowed to post to this stream."}), ); // reset compose_state.stream_name to 'social' again so that any tests occurring after this @@ -885,7 +901,10 @@ test_ui("enter_with_preview_open", (override) => { compose.enter_with_preview_open(); assert($("#enter_sends").prop("checked")); - assert.equal($("#compose-error-msg").html(), i18n.t("You have nothing to send!")); + assert.equal( + $("#compose-error-msg").html(), + $t_html({defaultMessage: "You have nothing to send!"}), + ); }); test_ui("finish", (override) => { @@ -902,7 +921,10 @@ test_ui("finish", (override) => { assert(!$("#sending-indicator").visible()); assert(!$("#compose-send-button").is_focused()); assert.equal($("#compose-send-button").prop("disabled"), false); - assert.equal($("#compose-error-msg").html(), i18n.t("You have nothing to send!")); + assert.equal( + $("#compose-error-msg").html(), + $t_html({defaultMessage: "You have nothing to send!"}), + ); })(); (function test_when_compose_validation_succeed() { diff --git a/static/js/compose.js b/static/js/compose.js index dfbab1700e..1ff5c9015d 100644 --- a/static/js/compose.js +++ b/static/js/compose.js @@ -1,4 +1,3 @@ -import Handlebars from "handlebars/runtime"; import $ from "jquery"; import _ from "lodash"; @@ -18,7 +17,7 @@ import * as compose_state from "./compose_state"; import * as compose_ui from "./compose_ui"; import * as drafts from "./drafts"; import * as echo from "./echo"; -import {i18n} from "./i18n"; +import {$t_html, i18n} from "./i18n"; import * as loading from "./loading"; import * as markdown from "./markdown"; import * as notifications from "./notifications"; @@ -282,13 +281,13 @@ export function create_message_object() { return message; } -export function compose_error(error_text, bad_input) { +export function compose_error(error_html, bad_input) { $("#compose-send-status") .removeClass(common.status_classes) .addClass("alert-error") .stop(true) .fadeTo(0, 1); - $("#compose-error-msg").html(error_text); + $("#compose-error-msg").html(error_html); $("#compose-send-button").prop("disabled", false); $("#sending-indicator").hide(); if (bad_input !== undefined) { @@ -304,13 +303,13 @@ export function nonexistent_stream_reply_error() { }, 5000); } -function compose_not_subscribed_error(error_text, bad_input) { +function compose_not_subscribed_error(error_html, bad_input) { $("#compose-send-status") .removeClass(common.status_classes) .addClass("home-error-bar") .stop(true) .fadeTo(0, 1); - $("#compose-error-msg").html(error_text); + $("#compose-error-msg").html(error_html); $("#compose-send-button").prop("disabled", false); $("#sending-indicator").hide(); $(".compose-send-status-close").hide(); @@ -554,7 +553,10 @@ function validate_stream_message_mentions(stream_id) { if (wildcard_mention !== null && stream_count > wildcard_mention_large_stream_threshold) { if (!wildcard_mention_allowed()) { compose_error( - i18n.t("You do not have permission to use wildcard mentions in this stream."), + $t_html({ + defaultMessage: + "You do not have permission to use wildcard mentions in this stream.", + }), ); return false; } @@ -610,12 +612,16 @@ function validate_stream_message_post_policy(sub) { const stream_post_policy = sub.stream_post_policy; if (stream_post_policy === stream_post_permission_type.admins.code) { - compose_error(i18n.t("Only organization admins are allowed to post to this stream.")); + compose_error( + $t_html({ + defaultMessage: "Only organization admins are allowed to post to this stream.", + }), + ); return false; } if (page_params.is_guest && stream_post_policy !== stream_post_permission_type.everyone.code) { - compose_error(i18n.t("Guests are not allowed to post to this stream.")); + compose_error($t_html({defaultMessage: "Guests are not allowed to post to this stream."})); return false; } @@ -623,16 +629,19 @@ function validate_stream_message_post_policy(sub) { const current_datetime = new Date(Date.now()); const person_date_joined = new Date(person.date_joined); const days = (current_datetime - person_date_joined) / 1000 / 86400; - let error_text; + let error_html; if ( stream_post_policy === stream_post_permission_type.non_new_members.code && days < page_params.realm_waiting_period_threshold ) { - error_text = i18n.t( - "New members are not allowed to post to this stream.
Permission will be granted in __days__ days.", + error_html = $t_html( + { + defaultMessage: + "New members are not allowed to post to this stream.
Permission will be granted in {days} days.", + }, {days}, ); - compose_error(error_text); + compose_error(error_html); return false; } return true; @@ -641,20 +650,23 @@ function validate_stream_message_post_policy(sub) { export function validation_error(error_type, stream_name) { let response; - const context = {}; - context.stream_name = Handlebars.Utils.escapeExpression(stream_name); - switch (error_type) { case "does-not-exist": - response = i18n.t( - "

The stream __stream_name__ does not exist.

Manage your subscriptions on your Streams page.

", - context, + response = $t_html( + { + defaultMessage: + "

The stream {stream_name} does not exist.

Manage your subscriptions on your Streams page.

", + }, + { + stream_name, + "z-link": (content_html) => `${content_html}`, + }, ); compose_error(response, $("#stream_message_recipient_stream")); return false; case "error": compose_error( - i18n.t("Error checking subscription"), + $t_html({defaultMessage: "Error checking subscription"}), $("#stream_message_recipient_stream"), ); return false; @@ -682,14 +694,20 @@ export function validate_stream_message_address_info(stream_name) { function validate_stream_message() { const stream_name = compose_state.stream_name(); if (stream_name === "") { - compose_error(i18n.t("Please specify a stream"), $("#stream_message_recipient_stream")); + compose_error( + $t_html({defaultMessage: "Please specify a stream"}), + $("#stream_message_recipient_stream"), + ); return false; } if (page_params.realm_mandatory_topics) { const topic = compose_state.topic(); if (topic === "") { - compose_error(i18n.t("Please specify a topic"), $("#stream_message_recipient_topic")); + compose_error( + $t_html({defaultMessage: "Please specify a topic"}), + $("#stream_message_recipient_topic"), + ); return false; } } @@ -740,7 +758,7 @@ function validate_private_message() { if (user_ids.length !== 1 || !people.get_by_user_id(user_ids[0]).is_bot) { // Unless we're composing to a bot compose_error( - i18n.t("Private messages are disabled in this organization."), + $t_html({defaultMessage: "Private messages are disabled in this organization."}), $("#private_message_recipient"), ); return false; @@ -749,7 +767,7 @@ function validate_private_message() { if (compose_state.private_message_recipient().length === 0) { compose_error( - i18n.t("Please specify at least one valid recipient"), + $t_html({defaultMessage: "Please specify at least one valid recipient"}), $("#private_message_recipient"), ); return false; @@ -764,14 +782,14 @@ function validate_private_message() { if (invalid_recipients.length === 1) { context = {recipient: invalid_recipients.join()}; compose_error( - i18n.t("The recipient __recipient__ is not valid", context), + $t_html({defaultMessage: "The recipient {recipient} is not valid"}, context), $("#private_message_recipient"), ); return false; } else if (invalid_recipients.length > 1) { context = {recipients: invalid_recipients.join()}; compose_error( - i18n.t("The recipients __recipients__ are not valid", context), + $t_html({defaultMessage: "The recipients {recipients} are not valid"}, context), $("#private_message_recipient"), ); return false; @@ -789,12 +807,20 @@ export function validate() { } if (/^\s*$/.test(message_content)) { - compose_error(i18n.t("You have nothing to send!"), $("#compose-textarea")); + compose_error( + $t_html({defaultMessage: "You have nothing to send!"}), + $("#compose-textarea"), + ); return false; } if ($("#zephyr-mirror-error").is(":visible")) { - compose_error(i18n.t("You need to be running Zephyr mirroring in order to send messages!")); + compose_error( + $t_html({ + defaultMessage: + "You need to be running Zephyr mirroring in order to send messages!", + }), + ); return false; } diff --git a/static/js/reminder.js b/static/js/reminder.js index 600ad575be..ba137c3076 100644 --- a/static/js/reminder.js +++ b/static/js/reminder.js @@ -4,7 +4,7 @@ import _ from "lodash"; import * as channel from "./channel"; import * as compose from "./compose"; import * as hash_util from "./hash_util"; -import {i18n} from "./i18n"; +import {$t_html, i18n} from "./i18n"; import * as message_lists from "./message_lists"; import * as notifications from "./notifications"; import {page_params} from "./page_params"; @@ -66,15 +66,22 @@ export function schedule_message(request = compose.create_message_object()) { $("#compose-textarea").prop("disabled", false); if (command_line.slice(command.length, command.length + 1) !== " ") { compose.compose_error( - i18n.t( - "Invalid slash command. Check if you are missing a space after the command.", - ), + $t_html({ + defaultMessage: + "Invalid slash command. Check if you are missing a space after the command.", + }), $("#compose-textarea"), ); } else if (deliver_at.trim() === "") { - compose.compose_error(i18n.t("Please specify a date or time"), $("#compose-textarea")); + compose.compose_error( + $t_html({defaultMessage: "Please specify a date or time"}), + $("#compose-textarea"), + ); } else { - compose.compose_error(i18n.t("Your reminder note is empty!"), $("#compose-textarea")); + compose.compose_error( + $t_html({defaultMessage: "Your reminder note is empty!"}), + $("#compose-textarea"), + ); } return; } diff --git a/tools/lib/template_parser.py b/tools/lib/template_parser.py index 4a85006711..650dc4d762 100644 --- a/tools/lib/template_parser.py +++ b/tools/lib/template_parser.py @@ -320,6 +320,7 @@ def is_special_html_tag(s: str, tag: str) -> bool: OPTIONAL_CLOSING_TAGS = [ + "br", "circle", "img", "input", diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index ba21dfc744..0d4a59c10d 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -139,7 +139,7 @@ js_rules = RuleList( {"pattern": r"\+.*i18n\.t\(.+\)", "description": "Do not concatenate i18n strings"}, { "pattern": "[.]html[(]", - "exclude_pattern": r"""\.html\(("|'|render_|html|message\.content|util\.clean_user_content_links|i18n\.t|rendered_|$|\)|error_text|widget_elem|\$error|\$\("

"\))""", + "exclude_pattern": r"""\.html\(("|'|render_|html|message\.content|util\.clean_user_content_links|i18n\.t|rendered_|$|\)|error_html|widget_elem|\$error|\$\("

"\))""", "exclude": { "static/js/portico", "static/js/lightbox.js",