REFACTOR: Fix pluralized strings in chat plugin (#20357)

* FIX: Use pluralized string

* REFACTOR: Fix misuse of pluralized string

* REFACTOR: Fix misuse of pluralized string

* DEV: Remove linting of `one` key in MessageFormat string, it doesn't work

* REFACTOR: Fix misuse of pluralized string

This also ensures that the URL works on subfolder and shows the site setting link only for admins instead of staff. The string is quite complicated, so the best option was to switch to MessageFormat.

* REFACTOR: Fix misuse of pluralized string

* FIX: Use pluralized string

This also ensures that the URL works on subfolder and shows the site setting link only for admins instead of staff.

* REFACTOR: Correctly pluralize reaction tooltips in chat

This also ensures that maximum 5 usernames are shown and fixes the number of "others" which was off by 1 if the current user reacted on a message.

* REFACTOR: Use translatable string as comma separator

* DEV: Add comment to translation to clarify the meaning of `%{identifier}`

* REFACTOR: Use translatable comma separator and use explicit interpolation keys

* REFACTOR: Don't interpolate lowercase channel status

* REFACTOR: Fix misuse of pluralized string

* REFACTOR: Don't interpolate channel status

* REFACTOR: Use %{count} interpolation key

* REFACTOR: Fix misuse of pluralized string

* REFACTOR: Correctly pluralize DM chat channel titles
This commit is contained in:
Gerhard Schlager 2023-02-20 10:31:02 +01:00 committed by GitHub
parent d71a82786a
commit 7ef482a292
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 499 additions and 368 deletions

View File

@ -138,7 +138,8 @@ class TranslationOverride < ActiveRecord::Base
:base, :base,
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: invalid_keys.join(", "), keys: invalid_keys.join(I18n.t("word_connector.comma")),
count: invalid_keys.size,
), ),
) )

View File

@ -161,6 +161,9 @@ en:
email: "Send via email" email: "Send via email"
url: "Copy and share URL" url: "Copy and share URL"
word_connector:
comma: ", "
action_codes: action_codes:
public_topic: "Made this topic public %{when}" public_topic: "Made this topic public %{when}"
open_topic: "Converted this to a topic %{when}" open_topic: "Converted this to a topic %{when}"

View File

@ -641,7 +641,9 @@ en:
translation_overrides: translation_overrides:
attributes: attributes:
value: value:
invalid_interpolation_keys: 'The following interpolation key(s) are invalid: "%{keys}"' invalid_interpolation_keys:
one: 'The following interpolation key is invalid: %{keys}'
other: 'The following interpolation keys are invalid: %{keys}'
watched_word: watched_word:
attributes: attributes:
word: word:

View File

@ -75,10 +75,6 @@ class ChatChannel < ActiveRecord::Base
Chat::ChatChannelMembershipManager.new(self).unfollow(user) Chat::ChatChannelMembershipManager.new(self).unfollow(user)
end end
def status_name
I18n.t("chat.channel.statuses.#{self.status}")
end
def url def url
"#{Discourse.base_url}/chat/c/#{self.slug || "-"}/#{self.id}" "#{Discourse.base_url}/chat/c/#{self.slug || "-"}/#{self.id}"
end end

View File

@ -52,7 +52,7 @@ class ChatMessage < ActiveRecord::Base
:base, :base,
I18n.t( I18n.t(
"chat.errors.minimum_length_not_met", "chat.errors.minimum_length_not_met",
minimum: SiteSetting.chat_minimum_message_length, count: SiteSetting.chat_minimum_message_length,
), ),
) )
end end
@ -60,7 +60,7 @@ class ChatMessage < ActiveRecord::Base
if message_too_long? if message_too_long?
self.errors.add( self.errors.add(
:base, :base,
I18n.t("chat.errors.message_too_long", maximum: SiteSetting.chat_maximum_message_length), I18n.t("chat.errors.message_too_long", count: SiteSetting.chat_maximum_message_length),
) )
end end
end end

View File

@ -25,7 +25,7 @@ class DirectMessage < ActiveRecord::Base
# direct message to self # direct message to self
if users.empty? if users.empty?
return I18n.t("chat.channel.dm_title.single_user", user: "@#{acting_user.username}") return I18n.t("chat.channel.dm_title.single_user", username: "@#{acting_user.username}")
end end
# all users deleted # all users deleted
@ -36,13 +36,16 @@ class DirectMessage < ActiveRecord::Base
return( return(
I18n.t( I18n.t(
"chat.channel.dm_title.multi_user_truncated", "chat.channel.dm_title.multi_user_truncated",
users: usernames_formatted[0..4].join(", "), comma_separated_usernames: usernames_formatted[0..4].join(I18n.t("word_connector.comma")),
leftover: usernames_formatted.length - 5, count: usernames_formatted.length - 5,
) )
) )
end end
I18n.t("chat.channel.dm_title.multi_user", users: usernames_formatted.join(", ")) I18n.t(
"chat.channel.dm_title.multi_user",
comma_separated_usernames: usernames_formatted.join(I18n.t("word_connector.comma")),
)
end end
end end

View File

@ -40,7 +40,7 @@ class ChatMessageSerializer < ApplicationSerializer
.reactions .reactions
.group_by(&:emoji) .group_by(&:emoji)
.each do |emoji, reactions| .each do |emoji, reactions|
users = reactions[0..6].map(&:user).filter { |user| user.id != scope&.user&.id }[0..5] users = reactions[0..5].map(&:user).filter { |user| user.id != scope&.user&.id }[0..4]
next unless Emoji.exists?(emoji) next unless Emoji.exists?(emoji)

View File

@ -4,7 +4,6 @@ import Component from "@ember/component";
import { import {
CHANNEL_STATUSES, CHANNEL_STATUSES,
channelStatusIcon, channelStatusIcon,
channelStatusName,
} from "discourse/plugins/chat/discourse/models/chat-channel"; } from "discourse/plugins/chat/discourse/models/chat-channel";
export default Component.extend({ export default Component.extend({
@ -38,20 +37,28 @@ export default Component.extend({
}, },
_shortStatusMessage(channelStatus) { _shortStatusMessage(channelStatus) {
return channelStatusName(channelStatus); switch (channelStatus) {
case CHANNEL_STATUSES.archived:
return I18n.t("chat.channel_status.archived");
case CHANNEL_STATUSES.closed:
return I18n.t("chat.channel_status.closed");
case CHANNEL_STATUSES.open:
return I18n.t("chat.channel_status.open");
case CHANNEL_STATUSES.readOnly:
return I18n.t("chat.channel_status.read_only");
}
}, },
_longStatusMessage(channelStatus) { _longStatusMessage(channelStatus) {
switch (channelStatus) { switch (channelStatus) {
case CHANNEL_STATUSES.closed:
return I18n.t("chat.channel_status.closed_header");
break;
case CHANNEL_STATUSES.readOnly:
return I18n.t("chat.channel_status.read_only_header");
break;
case CHANNEL_STATUSES.archived: case CHANNEL_STATUSES.archived:
return I18n.t("chat.channel_status.archived_header"); return I18n.t("chat.channel_status.archived_header");
break; case CHANNEL_STATUSES.closed:
return I18n.t("chat.channel_status.closed_header");
case CHANNEL_STATUSES.open:
return I18n.t("chat.channel_status.open_header");
case CHANNEL_STATUSES.readOnly:
return I18n.t("chat.channel_status.read_only_header");
} }
}, },
}); });

View File

@ -19,7 +19,6 @@ import { readOnly, reads } from "@ember/object/computed";
import { SKIP } from "discourse/lib/autocomplete"; import { SKIP } from "discourse/lib/autocomplete";
import { Promise } from "rsvp"; import { Promise } from "rsvp";
import { translations } from "pretty-text/emoji/data"; import { translations } from "pretty-text/emoji/data";
import { channelStatusName } from "discourse/plugins/chat/discourse/models/chat-channel";
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete"; import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
import { import {
chatComposerButtons, chatComposerButtons,
@ -566,17 +565,21 @@ export default Component.extend(TextareaTextManipulation, {
@discourseComputed("userSilenced", "chatChannel.{chatable.users.[],id}") @discourseComputed("userSilenced", "chatChannel.{chatable.users.[],id}")
placeholder(userSilenced, chatChannel) { placeholder(userSilenced, chatChannel) {
if (!chatChannel.canModifyMessages(this.currentUser)) { if (!chatChannel.canModifyMessages(this.currentUser)) {
return I18n.t("chat.placeholder_new_message_disallowed", { return I18n.t(
status: channelStatusName(chatChannel.status).toLowerCase(), `chat.placeholder_new_message_disallowed.${chatChannel.status}`
}); );
} }
if (chatChannel.isDraft) { if (chatChannel.isDraft) {
return I18n.t("chat.placeholder_start_conversation", { if (chatChannel?.chatable?.users?.length) {
usernames: chatChannel?.chatable?.users?.length return I18n.t("chat.placeholder_start_conversation_users", {
? chatChannel.chatable.users.mapBy("username").join(", ") usernames: chatChannel.chatable.users
: "...", .mapBy("username")
}); .join(I18n.t("word_connector.comma")),
});
} else {
return I18n.t("chat.placeholder_start_conversation");
}
} }
if (userSilenced) { if (userSilenced) {
@ -596,14 +599,14 @@ export default Component.extend(TextareaTextManipulation, {
return I18n.t("chat.placeholder_self"); return I18n.t("chat.placeholder_self");
} }
return I18n.t("chat.placeholder_others", { return I18n.t("chat.placeholder_users", {
messageRecipient: directMessageRecipients commaSeparatedNames: directMessageRecipients
.map((u) => u.name || `@${u.username}`) .map((u) => u.name || `@${u.username}`)
.join(", "), .join(I18n.t("word_connector.comma")),
}); });
} else { } else {
return I18n.t("chat.placeholder_others", { return I18n.t("chat.placeholder_channel", {
messageRecipient: `#${chatChannel.title}`, channelName: `#${chatChannel.title}`,
}); });
} }
}, },

View File

@ -2,6 +2,7 @@ import Component from "@glimmer/component";
import I18n from "I18n"; import I18n from "I18n";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import getURL from "discourse-common/lib/get-url";
export default class ChatMentionWarnings extends Component { export default class ChatMentionWarnings extends Component {
@service siteSettings; @service siteSettings;
@ -79,31 +80,22 @@ export default class ChatMentionWarnings extends Component {
return; return;
} }
let notificationLimit = I18n.t( if (this.currentUser.admin) {
"chat.mention_warning.groups.notification_limit" return htmlSafe(
); I18n.t("chat.mention_warning.too_many_mentions_admin", {
count: this.siteSettings.max_mentions_per_chat_message,
if (this.currentUser.staff) { siteSettingUrl: getURL(
notificationLimit = htmlSafe( "/admin/site_settings/category/plugins?filter=max_mentions_per_chat_message"
`<a ),
target="_blank" })
href="/admin/site_settings/category/plugins?filter=max_mentions_per_chat_message" );
> } else {
${notificationLimit} return htmlSafe(
</a>` I18n.t("chat.mention_warning.too_many_mentions", {
count: this.siteSettings.max_mentions_per_chat_message,
})
); );
} }
const settingLimit = I18n.t("chat.mention_warning.mentions_limit", {
count: this.siteSettings.max_mentions_per_chat_message,
});
return htmlSafe(
I18n.t("chat.mention_warning.too_many_mentions", {
notification_limit: notificationLimit,
limit: settingLimit,
})
);
} }
get unreachableBody() { get unreachableBody() {
@ -111,17 +103,21 @@ export default class ChatMentionWarnings extends Component {
return; return;
} }
if (this.unreachableGroupMentionsCount <= 2) { switch (this.unreachableGroupMentionsCount) {
return I18n.t("chat.mention_warning.groups.unreachable", { case 1:
group: this.unreachableGroupMentions[0], return I18n.t("chat.mention_warning.groups.unreachable_1", {
group_2: this.unreachableGroupMentions[1], group: this.unreachableGroupMentions[0],
count: this.unreachableGroupMentionsCount, });
}); case 2:
} else { return I18n.t("chat.mention_warning.groups.unreachable_2", {
return I18n.t("chat.mention_warning.groups.unreachable_multiple", { group1: this.unreachableGroupMentions[0],
group: this.unreachableGroupMentions[0], group2: this.unreachableGroupMentions[1],
count: this.unreachableGroupMentionsCount - 1, //N others });
}); default:
return I18n.t("chat.mention_warning.groups.unreachable_multiple", {
group: this.unreachableGroupMentions[0],
count: this.unreachableGroupMentionsCount - 1,
});
} }
} }
@ -130,44 +126,18 @@ export default class ChatMentionWarnings extends Component {
return; return;
} }
let notificationLimit = I18n.t( return htmlSafe(
"chat.mention_warning.groups.notification_limit" I18n.messageFormat("chat.mention_warning.groups.too_many_members_MF", {
groupCount: this.overMembersLimitMentionsCount,
isAdmin: this.currentUser.admin,
siteSettingUrl: getURL(
"/admin/site_settings/category/plugins?filter=max_users_notified_per_group_mention"
),
notificationLimit:
this.siteSettings.max_users_notified_per_group_mention,
group1: this.overMembersLimitGroupMentions[0],
group2: this.overMembersLimitGroupMentions[1],
})
); );
if (this.currentUser.staff) {
notificationLimit = htmlSafe(
`<a
target="_blank"
href="/admin/site_settings/category/plugins?filter=max_users_notified_per_group_mention"
>
${notificationLimit}
</a>`
);
}
const settingLimit = I18n.t("chat.mention_warning.groups.users_limit", {
count: this.siteSettings.max_users_notified_per_group_mention,
});
if (this.hasOverMembersLimitGroupMentions <= 2) {
return htmlSafe(
I18n.t("chat.mention_warning.groups.too_many_members", {
group: this.overMembersLimitGroupMentions[0],
group_2: this.overMembersLimitGroupMentions[1],
count: this.overMembersLimitMentionsCount,
notification_limit: notificationLimit,
limit: settingLimit,
})
);
} else {
return htmlSafe(
I18n.t("chat.mention_warning.groups.too_many_members_multiple", {
group: this.overMembersLimitGroupMentions[0],
count: this.overMembersLimitMentionsCount - 1, //N others
notification_limit: notificationLimit,
limit: settingLimit,
})
);
}
} }
} }

View File

@ -83,37 +83,91 @@ export default class ChatMessageReaction extends Component {
@computed("reaction") @computed("reaction")
get popoverContent() { get popoverContent() {
let usernames = this.reaction.users.mapBy("username").join(", "); return this.reaction.reacted
if (this.reaction.reacted) { ? this._reactionTextWithSelf()
if (this.reaction.count === 1) { : this._reactionText();
return I18n.t("chat.reactions.only_you", { }
emoji: this.reaction.emoji,
}); _reactionTextWithSelf() {
} else if (this.reaction.count > 1 && this.reaction.count < 6) { const reactionCount = this.reaction.count;
return I18n.t("chat.reactions.and_others", {
usernames, if (reactionCount === 0) {
emoji: this.reaction.emoji, return;
});
} else if (this.reaction.count >= 6) {
return I18n.t("chat.reactions.you_others_and_more", {
usernames,
emoji: this.reaction.emoji,
more: this.reaction.count - 5,
});
}
} else {
if (this.reaction.count > 0 && this.reaction.count < 6) {
return I18n.t("chat.reactions.only_others", {
usernames,
emoji: this.reaction.emoji,
});
} else if (this.reaction.count >= 6) {
return I18n.t("chat.reactions.others_and_more", {
usernames,
emoji: this.reaction.emoji,
more: this.reaction.count - 5,
});
}
} }
if (reactionCount === 1) {
return I18n.t("chat.reactions.only_you", {
emoji: this.reaction.emoji,
});
}
const maxUsernames = 4;
const usernames = this.reaction.users
.slice(0, maxUsernames)
.mapBy("username");
if (reactionCount === 2) {
return I18n.t("chat.reactions.you_and_single_user", {
emoji: this.reaction.emoji,
username: usernames.pop(),
});
}
// `-1` because the current user ("you") isn't included in `usernames`
const unnamedUserCount = reactionCount - usernames.length - 1;
if (unnamedUserCount > 0) {
return I18n.t("chat.reactions.you_multiple_users_and_more", {
emoji: this.reaction.emoji,
commaSeparatedUsernames: this._joinUsernames(usernames),
count: unnamedUserCount,
});
}
return I18n.t("chat.reactions.you_and_multiple_users", {
emoji: this.reaction.emoji,
username: usernames.pop(),
commaSeparatedUsernames: this._joinUsernames(usernames),
});
}
_reactionText() {
const reactionCount = this.reaction.count;
if (reactionCount === 0) {
return;
}
const maxUsernames = 5;
const usernames = this.reaction.users
.slice(0, maxUsernames)
.mapBy("username");
if (reactionCount === 1) {
return I18n.t("chat.reactions.single_user", {
emoji: this.reaction.emoji,
username: usernames.pop(),
});
}
const unnamedUserCount = reactionCount - usernames.length;
if (unnamedUserCount > 0) {
return I18n.t("chat.reactions.multiple_users_and_more", {
emoji: this.reaction.emoji,
commaSeparatedUsernames: this._joinUsernames(usernames),
count: unnamedUserCount,
});
}
return I18n.t("chat.reactions.multiple_users", {
emoji: this.reaction.emoji,
username: usernames.pop(),
commaSeparatedUsernames: this._joinUsernames(usernames),
});
}
_joinUsernames(usernames) {
return usernames.join(I18n.t("word_connector.comma"));
} }
} }

View File

@ -411,49 +411,53 @@ export default class ChatMessage extends Component {
} }
get mentionedCannotSeeText() { get mentionedCannotSeeText() {
return I18n.t("chat.mention_warning.cannot_see", { return this._findTranslatedWarning(
username: this.mentionWarning?.cannot_see?.[0]?.username, "chat.mention_warning.cannot_see",
count: this.mentionWarning?.cannot_see?.length, "chat.mention_warning.cannot_see_multiple",
others: this._othersTranslation( {
this.mentionWarning?.cannot_see?.length - 1 username: this.mentionWarning?.cannot_see?.[0]?.username,
), count: this.mentionWarning?.cannot_see?.length,
}); }
);
} }
get mentionedWithoutMembershipText() { get mentionedWithoutMembershipText() {
return I18n.t("chat.mention_warning.without_membership", { return this._findTranslatedWarning(
username: this.mentionWarning?.without_membership?.[0]?.username, "chat.mention_warning.without_membership",
count: this.mentionWarning?.without_membership?.length, "chat.mention_warning.without_membership_multiple",
others: this._othersTranslation( {
this.mentionWarning?.without_membership?.length - 1 username: this.mentionWarning?.without_membership?.[0]?.username,
), count: this.mentionWarning?.without_membership?.length,
}); }
);
} }
get groupsWithDisabledMentions() { get groupsWithDisabledMentions() {
return I18n.t("chat.mention_warning.group_mentions_disabled", { return this._findTranslatedWarning(
group_name: this.mentionWarning?.group_mentions_disabled?.[0], "chat.mention_warning.group_mentions_disabled",
count: this.mentionWarning?.group_mentions_disabled?.length, "chat.mention_warning.group_mentions_disabled_multiple",
others: this._othersTranslation( {
this.mentionWarning?.group_mentions_disabled?.length - 1 group_name: this.mentionWarning?.group_mentions_disabled?.[0],
), count: this.mentionWarning?.group_mentions_disabled?.length,
}); }
);
} }
get groupsWithTooManyMembers() { get groupsWithTooManyMembers() {
return I18n.t("chat.mention_warning.too_many_members", { return this._findTranslatedWarning(
group_name: this.mentionWarning.groups_with_too_many_members?.[0], "chat.mention_warning.too_many_members",
count: this.mentionWarning.groups_with_too_many_members?.length, "chat.mention_warning.too_many_members_multiple",
others: this._othersTranslation( {
this.mentionWarning.groups_with_too_many_members?.length - 1 group_name: this.mentionWarning.groups_with_too_many_members?.[0],
), count: this.mentionWarning.groups_with_too_many_members?.length,
}); }
);
} }
_othersTranslation(othersCount) { _findTranslatedWarning(oneKey, multipleKey, args) {
return I18n.t("chat.mention_warning.warning_multiple", { const translationKey = args.count === 1 ? oneKey : multipleKey;
count: othersCount, args.count--;
}); return I18n.t(translationKey, args);
} }
@action @action

View File

@ -33,14 +33,18 @@ export default Component.extend({
if (usernames.length < 4) { if (usernames.length < 4) {
const lastUsername = usernames.pop(); const lastUsername = usernames.pop();
const commaSeparatedUsernames = usernames.join(", "); const commaSeparatedUsernames = usernames.join(
I18n.t("word_connector.comma")
);
return I18n.t("chat.replying_indicator.multiple_users", { return I18n.t("chat.replying_indicator.multiple_users", {
commaSeparatedUsernames, commaSeparatedUsernames,
lastUsername, lastUsername,
}); });
} }
const commaSeparatedUsernames = usernames.slice(0, 2).join(", "); const commaSeparatedUsernames = usernames
.slice(0, 2)
.join(I18n.t("word_connector.comma"));
return I18n.t("chat.replying_indicator.many_users", { return I18n.t("chat.replying_indicator.many_users", {
commaSeparatedUsernames, commaSeparatedUsernames,
count: usernames.length - 2, count: usernames.length - 2,

View File

@ -103,20 +103,40 @@ export default class CreateChannelController extends Controller.extend(
_updateAutoJoinConfirmWarning(category, catPermissions) { _updateAutoJoinConfirmWarning(category, catPermissions) {
const allowedGroups = catPermissions.allowed_groups; const allowedGroups = catPermissions.allowed_groups;
let warning;
if (catPermissions.private) { if (catPermissions.private) {
const warningTranslationKey = switch (allowedGroups.length) {
allowedGroups.length < 3 ? "warning_groups" : "warning_multiple_groups"; case 1:
warning = I18n.t(
this.set( "chat.create_channel.auto_join_users.warning_1_group",
"autoJoinWarning", {
I18n.t(`chat.create_channel.auto_join_users.${warningTranslationKey}`, { count: catPermissions.members_count,
members_count: catPermissions.members_count, group: escapeExpression(allowedGroups[0]),
group: escapeExpression(allowedGroups[0]), }
group_2: escapeExpression(allowedGroups[1]), );
count: allowedGroups.length, break;
}) case 2:
); warning = I18n.t(
"chat.create_channel.auto_join_users.warning_2_groups",
{
count: catPermissions.members_count,
group1: escapeExpression(allowedGroups[0]),
group2: escapeExpression(allowedGroups[1]),
}
);
break;
default:
warning = I18n.messageFormat(
"chat.create_channel.auto_join_users.warning_multiple_groups_MF",
{
groupCount: allowedGroups.length - 1,
userCount: catPermissions.members_count,
groupName: escapeExpression(allowedGroups[0]),
}
);
break;
}
} else { } else {
this.set( this.set(
"autoJoinWarning", "autoJoinWarning",
@ -125,6 +145,8 @@ export default class CreateChannelController extends Controller.extend(
}) })
); );
} }
this.set("autoJoinWarning", warning);
} }
_updatePermissionsHint(category) { _updatePermissionsHint(category) {
@ -136,20 +158,42 @@ export default class CreateChannelController extends Controller.extend(
.then((catPermissions) => { .then((catPermissions) => {
this._updateAutoJoinConfirmWarning(category, catPermissions); this._updateAutoJoinConfirmWarning(category, catPermissions);
const allowedGroups = catPermissions.allowed_groups; const allowedGroups = catPermissions.allowed_groups;
const translationKey = const settingLink = `/c/${escapeExpression(fullSlug)}/edit/security`;
allowedGroups.length < 3 ? "hint_groups" : "hint_multiple_groups"; let hint;
this.set( switch (allowedGroups.length) {
"categoryPermissionsHint", case 1:
htmlSafe( hint = I18n.t(
I18n.t(`chat.create_channel.choose_category.${translationKey}`, { "chat.create_channel.choose_category.hint_1_group",
link: `/c/${escapeExpression(fullSlug)}/edit/security`, {
hint: escapeExpression(allowedGroups[0]), settingLink,
hint_2: escapeExpression(allowedGroups[1]), group: escapeExpression(allowedGroups[0]),
count: allowedGroups.length, }
}) );
) break;
); case 2:
hint = I18n.t(
"chat.create_channel.choose_category.hint_2_groups",
{
settingLink,
group1: escapeExpression(allowedGroups[0]),
group2: escapeExpression(allowedGroups[1]),
}
);
break;
default:
hint = I18n.t(
"chat.create_channel.choose_category.hint_multiple_groups",
{
settingLink,
group: escapeExpression(allowedGroups[0]),
count: allowedGroups.length - 1,
}
);
break;
}
this.set("categoryPermissionsHint", htmlSafe(hint));
}); });
} else { } else {
this.set("categoryPermissionsHint", DEFAULT_HINT); this.set("categoryPermissionsHint", DEFAULT_HINT);

View File

@ -223,8 +223,8 @@ export default {
} }
get title() { get title() {
return I18n.t("chat.placeholder_others", { return I18n.t("chat.placeholder_channel", {
messageRecipient: this.channel.escapedTitle, channelName: this.channel.escapedTitle,
}); });
} }

View File

@ -1,5 +1,4 @@
import RestModel from "discourse/models/rest"; import RestModel from "discourse/models/rest";
import I18n from "I18n";
import User from "discourse/models/user"; import User from "discourse/models/user";
import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership"; import UserChatChannelMembership from "discourse/plugins/chat/discourse/models/user-chat-channel-membership";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
@ -21,19 +20,6 @@ export const CHANNEL_STATUSES = {
archived: "archived", archived: "archived",
}; };
export function channelStatusName(channelStatus) {
switch (channelStatus) {
case CHANNEL_STATUSES.open:
return I18n.t("chat.channel_status.open");
case CHANNEL_STATUSES.readOnly:
return I18n.t("chat.channel_status.read_only");
case CHANNEL_STATUSES.closed:
return I18n.t("chat.channel_status.closed");
case CHANNEL_STATUSES.archived:
return I18n.t("chat.channel_status.archived");
}
}
export function channelStatusIcon(channelStatus) { export function channelStatusIcon(channelStatus) {
if (channelStatus === CHANNEL_STATUSES.open) { if (channelStatus === CHANNEL_STATUSES.open) {
return null; return null;

View File

@ -106,46 +106,96 @@ en:
new_messages: "new messages" new_messages: "new messages"
mention_warning: mention_warning:
dismiss: "dismiss" dismiss: "dismiss"
cannot_see: cannot_see: "%{username} can't access this channel and was not notified."
one: "%{username} cannot access this channel and was not notified." cannot_see_multiple:
other: "%{username} and %{others} cannot access this channel and were not notified." one: "%{username} and %{count} other user cannot access this channel and were not notified."
other: "%{username} and %{count} other users cannot access this channel and were not notified."
invitations_sent: invitations_sent:
one: "Invitation sent" one: "Invitation sent"
other: "Invitations sent" other: "Invitations sent"
invite: "Invite to channel" invite: "Invite to channel"
without_membership: without_membership: "%{username} has not joined this channel."
one: "%{username} has not joined this channel." without_membership_multiple:
other: "%{username} and %{others} have not joined this channel." one: "%{username} and %{count} other user have not joined this channel."
group_mentions_disabled: other: "%{username} and %{count} other users have not joined this channel."
one: "%{group_name} doesn't allow mentions" group_mentions_disabled: "%{group_name} doesn't allow mentions."
other: "%{group_name} and %{others} doesn't allow mentions" group_mentions_disabled_multiple:
too_many_members: one: "%{group_name} and %{count} other group don't allow mentions."
one: "%{group_name} has too many members. No one was notified" other: "%{group_name} and %{count} other groups don't allow mentions."
other: "%{group_name} and %{others} have too many members. No one was notified" too_many_members: "%{group_name} has too many members. No one was notified."
warning_multiple: too_many_members_multiple:
one: "%{count} other" one: "%{group_name} and %{count} other group have too many members. No one was notified."
other: "%{count} others" other: "%{group_name} and %{count} other groups have too many members. No one was notified."
groups: groups:
header: header:
some: "Some users won't be notified" some: "Some users won't be notified"
all: "Nobody will be notified" all: "Nobody will be notified"
unreachable: unreachable_1: "@%{group} doesn't allow mentions."
one: "@%{group} doesn't allow mentions" unreachable_2: "@%{group1} and @%{group2} don't allow mentions."
other: "@%{group} and @%{group_2} doesn't allow mentions" unreachable_multiple:
unreachable_multiple: "@%{group} and %{count} others doesn't allow mentions" one: "@%{group} and %{count} other group don't allow mentions."
too_many_members: other: "@%{group} and %{count} other groups don't allow mentions."
one: "Mentioning @%{group} exceeds the %{notification_limit} of %{limit}" too_many_members_MF: |
other: "Mentioning both @%{group} or @%{group_2} exceeds the %{notification_limit} of %{limit}" { groupCount, plural,
too_many_members_multiple: "These %{count} groups exceed the %{notification_limit} of %{limit}" =1 {
users_limit: { isAdmin, select,
one: "%{count} user" true {
other: "%{count} users" { notificationLimit, plural,
notification_limit: "notification limit" one {Mentioning @{group1} exceeds the <a href="{siteSettingUrl}" target="_blank">notification limit</a> of # user.}
too_many_mentions: "This message exceeds the %{notification_limit} of %{limit}" other {Mentioning @{group1} exceeds the <a href="{siteSettingUrl}" target="_blank">notification limit</a> of # users.}
mentions_limit: }
one: "%{count} mention" }
other: "%{count} mentions" false {
{ notificationLimit, plural,
one {Mentioning @{group1} exceeds the notification limit of # user.}
other {Mentioning @{group1} exceeds the notification limit of # users.}
}
}
other {}
}
}
=2 {
{ isAdmin, select,
true {
{ notificationLimit, plural,
one {Mentioning @{group1} and @{group2} exceeds the <a href="{siteSettingUrl}" target="_blank">notification limit</a> of # user.}
other {Mentioning @{group1} and @{group2} exceeds the <a href="{siteSettingUrl}" target="_blank">notification limit</a> of # users.}
}
}
false {
{ notificationLimit, plural,
one {Mentioning @{group1} and @{group2} exceeds the notification limit of # user.}
other {Mentioning @{group1} and @{group2} exceeds the notification limit of # users.}
}
}
other {}
}
}
other {
{ isAdmin, select,
true {
{ notificationLimit, plural,
one {Mentioning these {groupCount} groups exceeds the <a href="{siteSettingUrl}" target="_blank">notification limit</a> of # user.}
other {Mentioning these {groupCount} groups exceeds the <a href="{siteSettingUrl}" target="_blank">notification limit</a> of # users.}
}
}
false {
{ notificationLimit, plural,
one {Mentioning these {groupCount} groups exceeds the notification limit of # user.}
other {Mentioning these {groupCount} groups exceeds the notification limit of # users.}
}
}
other {}
}
}
}
too_many_mentions:
one: "This message exceeds the notification limit of %{count} mention."
other: "This message exceeds the notification limit of %{count} mentions."
too_many_mentions_admin:
one: 'This message exceeds the <a href="%{siteSettingUrl}" target="_blank">notification limit</a> of %{count} mention.'
other: 'This message exceeds the <a href="%{siteSettingUrl}" target="_blank">notification limit</a> of %{count} mentions.'
aria_roles: aria_roles:
header: "Chat header" header: "Chat header"
composer: "Chat composer" composer: "Chat composer"
@ -163,10 +213,15 @@ en:
close_full_page: "Close full-screen chat" close_full_page: "Close full-screen chat"
open_message: "Open message in chat" open_message: "Open message in chat"
placeholder_self: "Jot something down" placeholder_self: "Jot something down"
placeholder_others: "Chat with %{messageRecipient}" placeholder_channel: "Chat with %{channelName}"
placeholder_new_message_disallowed: "Channel is %{status}, you cannot send new messages right now." placeholder_users: "Chat with %{commaSeparatedNames}"
placeholder_new_message_disallowed:
archived: "Channel is archived, you cannot send new messages right now."
closed: "Channel is closed, you cannot send new messages right now."
read_only: "Channel is read only, you cannot send new messages right now."
placeholder_silenced: "You cannot send messages at this time." placeholder_silenced: "You cannot send messages at this time."
placeholder_start_conversation: Start a conversation with %{usernames} placeholder_start_conversation: "Start a conversation with ..."
placeholder_start_conversation_users: "Start a conversation with %{commaSeparatedUsernames}"
remove_upload: "Remove file" remove_upload: "Remove file"
react: "React with emoji" react: "React with emoji"
reply: "Reply" reply: "Reply"
@ -276,18 +331,36 @@ en:
create_channel: create_channel:
auto_join_users: auto_join_users:
public_category_warning: "%{category} is a public category. Automatically add all recently active users to this channel?" public_category_warning: "%{category} is a public category. Automatically add all recently active users to this channel?"
warning_groups: warning_1_group:
one: Automatically add %{members_count} users from %{group}? one: "Automatically add %{count} user from %{group}?"
other: Automatically add %{members_count} users from %{group} and %{group_2}? other: "Automatically add %{count} users from %{group}?"
warning_multiple_groups: Automatically add %{members_count} users from %{group_1} and %{count} others? warning_2_groups:
one: "Automatically add %{count} user from %{group1} and %{group2}?"
other: "Automatically add %{count} users from %{group1} and %{group2}?"
warning_multiple_groups_MF: |
{ groupCount, plural,
one {
{ userCount, plural,
one {Automatically add {userCount} user from {groupName} and {groupCount} other group?}
other {Automatically add {userCount} users from {groupName} and {groupCount} other group?}
}
}
other {
{ userCount, plural,
one {Automatically add {userCount} user from {groupName} and {groupCount} other groups?}
other {Automatically add {userCount} users from {groupName} and {groupCount} other groups?}
}
}
}
choose_category: choose_category:
label: "Choose a category" label: "Choose a category"
none: "select one..." none: "select one..."
default_hint: Manage access by visiting <a href=%{link} target="_blank">%{category} security settings</a> default_hint: Manage access by visiting <a href=%{link} target="_blank">%{category} security settings</a>
hint_groups: hint_1_group: 'Users in %{group} will have access to this channel per the <a href="%{settingLink}" target="_blank">security settings</a>'
one: Users in %{hint} will have access to this channel per the <a href=%{link} target="_blank">security settings</a> hint_2_groups: 'Users in %{group1} and %{group2} will have access to this channel per the <a href="%{settingLink}" target="_blank">security settings</a>'
other: Users in %{hint} and %{hint_2} will have access to this channel per the <a href=%{link} target="_blank">security settings</a> hint_multiple_groups:
hint_multiple_groups: Users in %{hint_1} and %{count} other groups will have access to this channel per the <a href=%{link} target="_blank">security settings</a> one: 'Users in %{group} and %{count} other group will have access to this channel per the <a href="%{settingLink}" target="_blank">security settings</a>'
other: 'Users in %{group} and %{count} other groups will have access to this channel per the <a href="%{settingLink}" target="_blank">security settings</a>'
create: "Create channel" create: "Create channel"
description: "Description (optional)" description: "Description (optional)"
name: "Channel name" name: "Channel name"
@ -303,10 +376,16 @@ en:
reactions: reactions:
only_you: "You reacted with :%{emoji}:" only_you: "You reacted with :%{emoji}:"
and_others: "You, %{usernames} reacted with :%{emoji}:" you_and_single_user: "You and %{username} reacted with :%{emoji}:"
only_others: "%{usernames} reacted with :%{emoji}:" you_and_multiple_users: "You, %{commaSeparatedUsernames} and %{username} reacted with :%{emoji}:"
others_and_more: "%{usernames} and %{more} others reacted with :%{emoji}:" you_multiple_users_and_more:
you_others_and_more: "You, %{usernames} and %{more} others reacted with :%{emoji}:" one: "You, %{commaSeparatedUsernames} and %{count} other reacted with :%{emoji}:"
other: "You, %{commaSeparatedUsernames} and %{count} others reacted with :%{emoji}:"
single_user: "%{username} reacted with :%{emoji}:"
multiple_users: "%{commaSeparatedUsernames} and %{username} reacted with :%{emoji}:"
multiple_users_and_more:
one: "%{commaSeparatedUsernames} and %{count} other reacted with :%{emoji}:"
other: "%{commaSeparatedUsernames} and %{count} others reacted with :%{emoji}:"
composer: composer:
toggle_toolbar: "Toggle toolbar" toggle_toolbar: "Toggle toolbar"
@ -465,11 +544,13 @@ en:
direct: 'mentioned you in "%{channel}"' direct: 'mentioned you in "%{channel}"'
direct_html: '<span>%{username}</span> <span>mentioned you in "%{channel}"</span>' direct_html: '<span>%{username}</span> <span>mentioned you in "%{channel}"</span>'
other_plain: 'mentioned %{identifier} in "%{channel}"' other_plain: 'mentioned %{identifier} in "%{channel}"'
# %{identifier} is either @here or @all
other_html: '<span>%{username}</span> <span>mentioned %{identifier} in "%{channel}"</span>' other_html: '<span>%{username}</span> <span>mentioned %{identifier} in "%{channel}"</span>'
direct_message_chat_mention: direct_message_chat_mention:
direct: "mentioned you in personal chat" direct: "mentioned you in personal chat"
direct_html: "<span>%{username}</span> <span>mentioned you in personal chat</span>" direct_html: "<span>%{username}</span> <span>mentioned you in personal chat</span>"
other_plain: "mentioned %{identifier} in personal chat" other_plain: "mentioned %{identifier} in personal chat"
# %{identifier} is either @here or @all
other_html: "<span>%{username}</span> <span>mentioned %{identifier} in personal chat</span>" other_html: "<span>%{username}</span> <span>mentioned %{identifier} in personal chat</span>"
chat_message: "New chat message" chat_message: "New chat message"
chat_quoted: "%{username} quoted your chat message" chat_quoted: "%{username} quoted your chat message"

View File

@ -49,16 +49,26 @@ en:
deleted_chat_username: deleted deleted_chat_username: deleted
errors: errors:
channel_exists_for_category: "A channel already exists for this category and name" channel_exists_for_category: "A channel already exists for this category and name"
channel_new_message_disallowed: "The channel is %{status}, no new messages can be sent" channel_new_message_disallowed:
channel_modify_message_disallowed: "The channel is %{status}, no messages can be edited or deleted" archived: "The channel is archived, no new messages can be sent"
closed: "The channel is closed, no new messages can be sent"
read_only: "The channel is read only, no new messages can be sent"
channel_modify_message_disallowed:
archived: "The channel is archived, no messages can be edited or deleted"
closed: "The channel is closed, no messages can be edited or deleted"
read_only: "The channel is read only, no messages can be edited or deleted"
user_cannot_send_message: "You cannot send messages at this time." user_cannot_send_message: "You cannot send messages at this time."
rate_limit_exceeded: "Exceeded the limit of chat messages that can be sent within 30 seconds" rate_limit_exceeded: "Exceeded the limit of chat messages that can be sent within 30 seconds"
auto_silence_from_flags: "Chat message flagged with score high enough to silence user." auto_silence_from_flags: "Chat message flagged with score high enough to silence user."
channel_cannot_be_archived: "The channel cannot be archived at this time, it must be either closed or open to archive." channel_cannot_be_archived: "The channel cannot be archived at this time, it must be either closed or open to archive."
duplicate_message: "You posted an identical message too recently." duplicate_message: "You posted an identical message too recently."
delete_channel_failed: "Delete channel failed, please try again." delete_channel_failed: "Delete channel failed, please try again."
minimum_length_not_met: "Message is too short, must have a minimum of %{minimum} characters." minimum_length_not_met:
message_too_long: "Message is too long, messages must be a maximum of %{maximum} characters." one: "Message is too short, must have a minimum of %{count} character."
other: "Message is too short, must have a minimum of %{count} characters."
message_too_long:
one: "Message is too long, messages must be a maximum of %{count} characters."
other: "Message is too long, messages must be a maximum of %{count} characters."
draft_too_long: "Draft is too long." draft_too_long: "Draft is too long."
max_reactions_limit_reached: "New reactions are not allowed on this message." max_reactions_limit_reached: "New reactions are not allowed on this message."
message_move_invalid_channel: "The source and destination channel must be public channels." message_move_invalid_channel: "The source and destination channel must be public channels."
@ -70,8 +80,9 @@ en:
actor_disallowed_dms: "You have chosen to prevent users from sending you private and direct messages, so you cannot create new direct messages." actor_disallowed_dms: "You have chosen to prevent users from sending you private and direct messages, so you cannot create new direct messages."
actor_preventing_target_user_from_dm: "You have chosen to prevent %{username} from sending you private and direct messages, so you cannot create new direct messages to them." actor_preventing_target_user_from_dm: "You have chosen to prevent %{username} from sending you private and direct messages, so you cannot create new direct messages to them."
user_cannot_send_direct_messages: "Sorry, you cannot send direct messages." user_cannot_send_direct_messages: "Sorry, you cannot send direct messages."
over_chat_max_direct_message_users_allow_self: "You can only create a direct message with yourself."
over_chat_max_direct_message_users: over_chat_max_direct_message_users:
one: "You can only create a direct message with yourself." one: "You can't create a direct message with more than %{count} other user."
other: "You can't create a direct message with more than %{count} other users." other: "You can't create a direct message with more than %{count} other users."
original_message_not_found: "The ancestor of the message you are replying cannot be found or has been deleted." original_message_not_found: "The ancestor of the message you are replying cannot be found or has been deleted."
reviewables: reviewables:
@ -110,20 +121,17 @@ en:
transcript_title: "Transcript of previous messages in %{channel_name}" transcript_title: "Transcript of previous messages in %{channel_name}"
transcript_body: "To give you more context, we included a transcript of the previous messages in this conversation (up to ten):\n\n%{transcript}" transcript_body: "To give you more context, we included a transcript of the previous messages in this conversation (up to ten):\n\n%{transcript}"
channel: channel:
statuses:
read_only: "Read Only"
archived: "Archived"
closed: "Closed"
open: "Open"
archive: archive:
first_post_raw: "This topic is an archive of the [%{channel_name}](%{channel_url}) chat channel." first_post_raw: "This topic is an archive of the [%{channel_name}](%{channel_url}) chat channel."
messages_moved: messages_moved:
one: "@%{acting_username} moved a message to the [%{channel_name}](%{first_moved_message_url}) channel." one: "@%{acting_username} moved a message to the [%{channel_name}](%{first_moved_message_url}) channel."
other: "@%{acting_username} moved %{count} messages to the [%{channel_name}](%{first_moved_message_url}) channel." other: "@%{acting_username} moved %{count} messages to the [%{channel_name}](%{first_moved_message_url}) channel."
dm_title: dm_title:
single_user: "%{user}" single_user: "%{username}"
multi_user: "%{users}" multi_user: "%{comma_separated_usernames}"
multi_user_truncated: "%{users} and %{leftover} others" multi_user_truncated:
one: "%{comma_separated_usernames} and %{count} other"
other: "%{comma_separated_usernames} and %{count} others"
category_channel: category_channel:
errors: errors:

View File

@ -82,10 +82,7 @@ class Chat::ChatMessageCreator
raise StandardError.new(I18n.t("chat.errors.user_cannot_send_direct_messages")) raise StandardError.new(I18n.t("chat.errors.user_cannot_send_direct_messages"))
else else
raise StandardError.new( raise StandardError.new(
I18n.t( I18n.t("chat.errors.channel_new_message_disallowed.#{@chat_channel.status}"),
"chat.errors.channel_new_message_disallowed",
status: @chat_channel.status_name,
),
) )
end end
end end

View File

@ -53,10 +53,7 @@ class Chat::ChatMessageReactor
raise Discourse::InvalidAccess.new( raise Discourse::InvalidAccess.new(
nil, nil,
nil, nil,
custom_message: "chat.errors.channel_modify_message_disallowed", custom_message: "chat.errors.channel_modify_message_disallowed.#{@chat_channel.status}",
custom_message_params: {
status: @chat_channel.status_name,
},
) )
end end

View File

@ -51,10 +51,7 @@ class Chat::ChatMessageUpdater
def validate_channel_status! def validate_channel_status!
return if @guardian.can_modify_channel_message?(@chat_channel) return if @guardian.can_modify_channel_message?(@chat_channel)
raise StandardError.new( raise StandardError.new(
I18n.t( I18n.t("chat.errors.channel_modify_message_disallowed.#{@chat_channel.status}"),
"chat.errors.channel_modify_message_disallowed",
status: @chat_channel.status_name,
),
) )
end end

View File

@ -30,12 +30,16 @@ module Chat::DirectMessageChannelCreator
target_users = target_users.reject { |user| user.id == acting_user.id } target_users = target_users.reject { |user| user.id == acting_user.id }
if !acting_user.staff? && target_users.size > SiteSetting.chat_max_direct_message_users if !acting_user.staff? && target_users.size > SiteSetting.chat_max_direct_message_users
raise NotAllowed.new( if SiteSetting.chat_max_direct_message_users == 0
I18n.t( raise NotAllowed.new(I18n.t("chat.errors.over_chat_max_direct_message_users_allow_self"))
"chat.errors.over_chat_max_direct_message_users", else
count: SiteSetting.chat_max_direct_message_users + 1, # +1 for the acting_user raise NotAllowed.new(
), I18n.t(
) "chat.errors.over_chat_max_direct_message_users",
count: SiteSetting.chat_max_direct_message_users + 1, # +1 for the acting_user
),
)
end
end end
end end

View File

@ -64,7 +64,7 @@ describe Chat::ChatMessageCreator do
expect(creator.error.message).to match( expect(creator.error.message).to match(
I18n.t( I18n.t(
"chat.errors.minimum_length_not_met", "chat.errors.minimum_length_not_met",
{ minimum: SiteSetting.chat_minimum_message_length }, { count: SiteSetting.chat_minimum_message_length },
), ),
) )
end end
@ -79,10 +79,7 @@ describe Chat::ChatMessageCreator do
) )
expect(creator.failed?).to eq(true) expect(creator.failed?).to eq(true)
expect(creator.error.message).to match( expect(creator.error.message).to match(
I18n.t( I18n.t("chat.errors.message_too_long", { count: SiteSetting.chat_maximum_message_length }),
"chat.errors.message_too_long",
{ maximum: SiteSetting.chat_maximum_message_length },
),
) )
end end
@ -866,10 +863,7 @@ describe Chat::ChatMessageCreator do
creator = create_message(user1) creator = create_message(user1)
expect(creator.failed?).to eq(true) expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq( expect(creator.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_new_message_disallowed.closed"),
"chat.errors.channel_new_message_disallowed",
status: public_chat_channel.status_name,
),
) )
end end
@ -885,18 +879,12 @@ describe Chat::ChatMessageCreator do
creator = create_message(user1) creator = create_message(user1)
expect(creator.failed?).to eq(true) expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq( expect(creator.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_new_message_disallowed.read_only"),
"chat.errors.channel_new_message_disallowed",
status: public_chat_channel.status_name,
),
) )
creator = create_message(admin1) creator = create_message(admin1)
expect(creator.failed?).to eq(true) expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq( expect(creator.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_new_message_disallowed.read_only"),
"chat.errors.channel_new_message_disallowed",
status: public_chat_channel.status_name,
),
) )
end end
end end
@ -908,18 +896,12 @@ describe Chat::ChatMessageCreator do
creator = create_message(user1) creator = create_message(user1)
expect(creator.failed?).to eq(true) expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq( expect(creator.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_new_message_disallowed.archived"),
"chat.errors.channel_new_message_disallowed",
status: public_chat_channel.status_name,
),
) )
creator = create_message(admin1) creator = create_message(admin1)
expect(creator.failed?).to eq(true) expect(creator.failed?).to eq(true)
expect(creator.error.message).to eq( expect(creator.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_new_message_disallowed.archived"),
"chat.errors.channel_new_message_disallowed",
status: public_chat_channel.status_name,
),
) )
end end
end end

View File

@ -62,7 +62,7 @@ describe Chat::ChatMessageUpdater do
expect(updater.error.message).to match( expect(updater.error.message).to match(
I18n.t( I18n.t(
"chat.errors.minimum_length_not_met", "chat.errors.minimum_length_not_met",
{ minimum: SiteSetting.chat_minimum_message_length }, { count: SiteSetting.chat_minimum_message_length },
), ),
) )
expect(chat_message.reload.message).to eq(og_message) expect(chat_message.reload.message).to eq(og_message)
@ -82,7 +82,7 @@ describe Chat::ChatMessageUpdater do
) )
expect(updater.failed?).to eq(true) expect(updater.failed?).to eq(true)
expect(updater.error.message).to match( expect(updater.error.message).to match(
I18n.t("chat.errors.message_too_long", { maximum: SiteSetting.chat_maximum_message_length }), I18n.t("chat.errors.message_too_long", { count: SiteSetting.chat_maximum_message_length }),
) )
expect(chat_message.reload.message).to eq(og_message) expect(chat_message.reload.message).to eq(og_message)
end end
@ -528,10 +528,7 @@ describe Chat::ChatMessageUpdater do
updater = update_message(user1) updater = update_message(user1)
expect(updater.failed?).to eq(true) expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq( expect(updater.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_modify_message_disallowed.closed"),
"chat.errors.channel_modify_message_disallowed",
status: public_chat_channel.status_name,
),
) )
end end
@ -548,18 +545,12 @@ describe Chat::ChatMessageUpdater do
updater = update_message(user1) updater = update_message(user1)
expect(updater.failed?).to eq(true) expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq( expect(updater.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_modify_message_disallowed.read_only"),
"chat.errors.channel_modify_message_disallowed",
status: public_chat_channel.status_name,
),
) )
updater = update_message(admin1) updater = update_message(admin1)
expect(updater.failed?).to eq(true) expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq( expect(updater.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_modify_message_disallowed.read_only"),
"chat.errors.channel_modify_message_disallowed",
status: public_chat_channel.status_name,
),
) )
end end
end end
@ -571,18 +562,12 @@ describe Chat::ChatMessageUpdater do
updater = update_message(user1) updater = update_message(user1)
expect(updater.failed?).to eq(true) expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq( expect(updater.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_modify_message_disallowed.archived"),
"chat.errors.channel_modify_message_disallowed",
status: public_chat_channel.status_name,
),
) )
updater = update_message(admin1) updater = update_message(admin1)
expect(updater.failed?).to eq(true) expect(updater.failed?).to eq(true)
expect(updater.error.message).to eq( expect(updater.error.message).to eq(
I18n.t( I18n.t("chat.errors.channel_modify_message_disallowed.archived"),
"chat.errors.channel_modify_message_disallowed",
status: public_chat_channel.status_name,
),
) )
end end
end end

View File

@ -212,7 +212,7 @@ describe Chat::DirectMessageChannelCreator do
subject.create!(acting_user: user_1, target_users: [user_1, user_2]) subject.create!(acting_user: user_1, target_users: [user_1, user_2])
}.to raise_error( }.to raise_error(
Chat::DirectMessageChannelCreator::NotAllowed, Chat::DirectMessageChannelCreator::NotAllowed,
I18n.t("chat.errors.over_chat_max_direct_message_users", count: 1), I18n.t("chat.errors.over_chat_max_direct_message_users_allow_self"),
) )
end end
end end

View File

@ -20,7 +20,8 @@ describe DirectMessage do
expect(direct_message.chat_channel_title_for_user(chat_channel, user1)).to eq( expect(direct_message.chat_channel_title_for_user(chat_channel, user1)).to eq(
I18n.t( I18n.t(
"chat.channel.dm_title.multi_user", "chat.channel.dm_title.multi_user",
users: [user3, user2].map { |u| "@#{u.username}" }.join(", "), comma_separated_usernames:
[user3, user2].map { |u| "@#{u.username}" }.join(I18n.t("word_connector.comma")),
), ),
) )
end end
@ -36,8 +37,12 @@ describe DirectMessage do
expect(direct_message.chat_channel_title_for_user(chat_channel, user1)).to eq( expect(direct_message.chat_channel_title_for_user(chat_channel, user1)).to eq(
I18n.t( I18n.t(
"chat.channel.dm_title.multi_user_truncated", "chat.channel.dm_title.multi_user_truncated",
users: users[1..5].sort_by(&:username).map { |u| "@#{u.username}" }.join(", "), comma_separated_usernames:
leftover: 2, users[1..5]
.sort_by(&:username)
.map { |u| "@#{u.username}" }
.join(I18n.t("word_connector.comma")),
count: 2,
), ),
) )
end end
@ -46,7 +51,7 @@ describe DirectMessage do
direct_message = Fabricate(:direct_message, users: [user1, user2]) direct_message = Fabricate(:direct_message, users: [user1, user2])
expect(direct_message.chat_channel_title_for_user(chat_channel, user1)).to eq( expect(direct_message.chat_channel_title_for_user(chat_channel, user1)).to eq(
I18n.t("chat.channel.dm_title.single_user", user: "@#{user2.username}"), I18n.t("chat.channel.dm_title.single_user", username: "@#{user2.username}"),
) )
end end
@ -54,7 +59,7 @@ describe DirectMessage do
direct_message = Fabricate(:direct_message, users: [user1]) direct_message = Fabricate(:direct_message, users: [user1])
expect(direct_message.chat_channel_title_for_user(chat_channel, user1)).to eq( expect(direct_message.chat_channel_title_for_user(chat_channel, user1)).to eq(
I18n.t("chat.channel.dm_title.single_user", user: "@#{user1.username}"), I18n.t("chat.channel.dm_title.single_user", username: "@#{user1.username}"),
) )
end end

View File

@ -320,7 +320,7 @@ RSpec.describe Chat::ChatController do
post "/chat/#{chat_channel.id}.json", params: { message: message } post "/chat/#{chat_channel.id}.json", params: { message: message }
expect(response.status).to eq(422) expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include( expect(response.parsed_body["errors"]).to include(
I18n.t("chat.errors.channel_new_message_disallowed", status: chat_channel.status_name), I18n.t("chat.errors.channel_new_message_disallowed.closed"),
) )
end end
@ -336,7 +336,7 @@ RSpec.describe Chat::ChatController do
post "/chat/#{chat_channel.id}.json", params: { message: message } post "/chat/#{chat_channel.id}.json", params: { message: message }
expect(response.status).to eq(422) expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include( expect(response.parsed_body["errors"]).to include(
I18n.t("chat.errors.channel_new_message_disallowed", status: chat_channel.status_name), I18n.t("chat.errors.channel_new_message_disallowed.read_only"),
) )
end end
@ -903,7 +903,7 @@ RSpec.describe Chat::ChatController do
}.not_to change { chat_message.reactions.where(user: user, emoji: emoji).count } }.not_to change { chat_message.reactions.where(user: user, emoji: emoji).count }
expect(response.status).to eq(403) expect(response.status).to eq(403)
expect(response.parsed_body["errors"]).to include( expect(response.parsed_body["errors"]).to include(
I18n.t("chat.errors.channel_modify_message_disallowed", status: chat_channel.status_name), I18n.t("chat.errors.channel_modify_message_disallowed.#{chat_channel.status}"),
) )
end end

View File

@ -55,7 +55,7 @@ RSpec.describe Chat::IncomingChatWebhooksController do
}.not_to change { ChatMessage.where(chat_channel: chat_channel).count } }.not_to change { ChatMessage.where(chat_channel: chat_channel).count }
expect(response.status).to eq(422) expect(response.status).to eq(422)
expect(response.parsed_body["errors"]).to include( expect(response.parsed_body["errors"]).to include(
I18n.t("chat.errors.channel_new_message_disallowed", status: chat_channel.status_name), I18n.t("chat.errors.channel_new_message_disallowed.read_only"),
) )
end end

View File

@ -30,11 +30,7 @@ RSpec.describe "Closed channel", type: :system, js: true do
chat.visit_channel(channel_1) chat.visit_channel(channel_1)
expect(page).to have_field( expect(page).to have_field(
placeholder: placeholder: I18n.t("js.chat.placeholder_new_message_disallowed.closed"),
I18n.t(
"js.chat.placeholder_new_message_disallowed",
status: I18n.t("js.chat.channel_status.closed").downcase,
),
disabled: true, disabled: true,
) )
end end
@ -54,7 +50,7 @@ RSpec.describe "Closed channel", type: :system, js: true do
chat.visit_channel(channel_1) chat.visit_channel(channel_1)
expect(page).to have_no_field( expect(page).to have_no_field(
placeholder: I18n.t("js.chat.placeholder_new_message_disallowed"), placeholder: I18n.t("js.chat.placeholder_new_message_disallowed.closed"),
disabled: true, disabled: true,
) )
end end

View File

@ -69,7 +69,7 @@ RSpec.describe "Create channel", type: :system, js: true do
end end
end end
context "when category has a malicous group name" do context "when category has a malicious group name" do
fab!(:group_1) do fab!(:group_1) do
group = Group.new(name: "<script>e</script>") group = Group.new(name: "<script>e</script>")
group.save(validate: false) group.save(validate: false)

View File

@ -22,7 +22,7 @@ RSpec.describe "JIT messages", type: :system, js: true do
channel.send_message("hi @#{other_user.username}") channel.send_message("hi @#{other_user.username}")
expect(page).to have_content( expect(page).to have_content(
I18n.t("js.chat.mention_warning.without_membership.one", username: other_user.username), I18n.t("js.chat.mention_warning.without_membership", username: other_user.username),
wait: 5, wait: 5,
) )
end end
@ -44,7 +44,7 @@ RSpec.describe "JIT messages", type: :system, js: true do
channel.send_message("hi @#{other_user.username}") channel.send_message("hi @#{other_user.username}")
expect(page).to have_content( expect(page).to have_content(
I18n.t("js.chat.mention_warning.cannot_see.one", username: other_user.username), I18n.t("js.chat.mention_warning.cannot_see", username: other_user.username),
wait: 5, wait: 5,
) )
end end
@ -61,7 +61,7 @@ RSpec.describe "JIT messages", type: :system, js: true do
channel.send_message("hi @#{group_1.name}") channel.send_message("hi @#{group_1.name}")
expect(page).to have_content( expect(page).to have_content(
I18n.t("js.chat.mention_warning.group_mentions_disabled.one", group_name: group_1.name), I18n.t("js.chat.mention_warning.group_mentions_disabled", group_name: group_1.name),
wait: 5, wait: 5,
) )
end end

View File

@ -30,7 +30,7 @@ RSpec.describe "Read only", type: :system, js: true do
chat.visit_channel(channel_1) chat.visit_channel(channel_1)
expect(page).to have_field( expect(page).to have_field(
placeholder: I18n.t("js.chat.placeholder_new_message_disallowed", status: "read only"), placeholder: I18n.t("js.chat.placeholder_new_message_disallowed.read_only"),
disabled: true, disabled: true,
) )
end end
@ -50,7 +50,7 @@ RSpec.describe "Read only", type: :system, js: true do
chat.visit_channel(channel_1) chat.visit_channel(channel_1)
expect(page).to have_field( expect(page).to have_field(
placeholder: I18n.t("js.chat.placeholder_new_message_disallowed", status: "read only"), placeholder: I18n.t("js.chat.placeholder_new_message_disallowed.read_only"),
disabled: true, disabled: true,
) )
end end

View File

@ -37,8 +37,6 @@ class LocaleFileValidator
"Pluralized strings must have only the sub-keys 'one' and 'other'.\nThe following keys have missing or additional keys:", "Pluralized strings must have only the sub-keys 'one' and 'other'.\nThe following keys have missing or additional keys:",
invalid_one_keys: invalid_one_keys:
"The following keys contain the number 1 instead of the interpolation key %{count}:", "The following keys contain the number 1 instead of the interpolation key %{count}:",
invalid_message_format_one_key:
"The following keys use 'one {1 foo}' instead of the generic 'one {# foo}':",
} }
PLURALIZATION_KEYS = %w[zero one two few many other] PLURALIZATION_KEYS = %w[zero one two few many other]
@ -88,7 +86,6 @@ class LocaleFileValidator
@errors[:invalid_relative_links] = [] @errors[:invalid_relative_links] = []
@errors[:invalid_relative_image_sources] = [] @errors[:invalid_relative_image_sources] = []
@errors[:invalid_interpolation_key_format] = [] @errors[:invalid_interpolation_key_format] = []
@errors[:invalid_message_format_one_key] = []
each_translation(yaml) do |key, value| each_translation(yaml) do |key, value|
@errors[:invalid_relative_links] << key if value.match?(%r{href\s*=\s*["']/[^/]|\]\(/[^/]}i) @errors[:invalid_relative_links] << key if value.match?(%r{href\s*=\s*["']/[^/]|\]\(/[^/]}i)
@ -98,10 +95,6 @@ class LocaleFileValidator
if value.match?(/{{.+?}}/) && !key.end_with?("_MF") if value.match?(/{{.+?}}/) && !key.end_with?("_MF")
@errors[:invalid_interpolation_key_format] << key @errors[:invalid_interpolation_key_format] << key
end end
if key.end_with?("_MF") && value.match?(/one {.*?1.*?}/)
@errors[:invalid_message_format_one_key] << key
end
end end
end end

View File

@ -27,6 +27,7 @@ RSpec.describe TranslationOverride do
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: "key, omg", keys: "key, omg",
count: 2,
), ),
) )
end end
@ -61,6 +62,7 @@ RSpec.describe TranslationOverride do
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: "something", keys: "something",
count: 1,
), ),
) )
end end
@ -78,6 +80,7 @@ RSpec.describe TranslationOverride do
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: "topic_title_url_encoded", keys: "topic_title_url_encoded",
count: 1,
), ),
) )
end end
@ -132,6 +135,7 @@ RSpec.describe TranslationOverride do
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: "key3, key4", keys: "key3, key4",
count: 2,
), ),
) )
end end

View File

@ -165,6 +165,7 @@ RSpec.describe Admin::EmailTemplatesController do
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: "email_wrongfix", keys: "email_wrongfix",
count: 1,
) )
}", }",
] ]
@ -183,6 +184,7 @@ RSpec.describe Admin::EmailTemplatesController do
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: "invalid", keys: "invalid",
count: 1,
) )
}", }",
] ]
@ -201,12 +203,14 @@ RSpec.describe Admin::EmailTemplatesController do
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: "invalid", keys: "invalid",
count: 1,
) )
}", }",
"<b>Body</b>: #{ "<b>Body</b>: #{
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: "invalid", keys: "invalid",
count: 1,
) )
}", }",
] ]

View File

@ -580,6 +580,7 @@ RSpec.describe Admin::SiteTextsController do
I18n.t( I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: "key, omg", keys: "key, omg",
count: 2,
), ),
) )
end end