Fix i18n issues reported on Crowdin (#11747)

* Pluralize `groups.errors.adding_too_many_users`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/248/en-ar#53882

* Pluralize `js.composer.error.title_too_short`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#41172

* Pluralize `js.composer.error.title_too_long`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#41174

* Pluralize `js.composer.error.post_length`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#41178

* Pluralize `js.topic.progress.jump_prompt_of`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#41958

* Use translations to join strings about posters
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/248/en-ar#49334
  It also makes some changes to the crawler view:
    * Removes `poster.moreCount` which is only available on the client for PMs
    * CSS class names are actually stored in `poster.extras` instead of `poster.extraClasses`

* Stop concatenating category stats
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#40740

* Pluralize `js.summary.description`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#40782

* Pluralize `js.summary.description_time_MF`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#40784

* Use translation to join list of tags
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#43372

* Pluralize `admin_js.admin.groups.manage.membership.automatic_membership_user_count`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#43720

* Pluralize `js.post.controls.delete_topic_confirm_modal`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#54804

* Stop concatenating `js.post.last_edited_on`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#42358

* Stop concatenating `js.post.wiki_last_edited_on`
  This fixes https://discourse.crowdin.com/translate/f3230e7607a36bb0a2f97fd90605a44e/246/en-ar#42356
  It also fixes a regression because `js.post.wiki_last_edited_on` wasn't used anymore since 2017.
This commit is contained in:
Gerhard Schlager 2021-02-02 10:50:04 +01:00 committed by GitHub
parent 9d2eaec88f
commit d055552994
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 111 additions and 80 deletions

View File

@ -239,7 +239,9 @@ export default Component.extend({
if (replyLength < 1) { if (replyLength < 1) {
reason = I18n.t("composer.error.post_missing"); reason = I18n.t("composer.error.post_missing");
} else if (missingReplyCharacters > 0) { } else if (missingReplyCharacters > 0) {
reason = I18n.t("composer.error.post_length", { min: minimumPostLength }); reason = I18n.t("composer.error.post_length", {
count: minimumPostLength,
});
const tl = this.get("currentUser.trust_level"); const tl = this.get("currentUser.trust_level");
if (tl === 0 || tl === 1) { if (tl === 0 || tl === 1) {
reason += reason +=

View File

@ -44,11 +44,11 @@ export default Component.extend({
reason = I18n.t("composer.error.title_missing"); reason = I18n.t("composer.error.title_missing");
} else if (missingTitleChars > 0) { } else if (missingTitleChars > 0) {
reason = I18n.t("composer.error.title_too_short", { reason = I18n.t("composer.error.title_too_short", {
min: minimumTitleLength, count: minimumTitleLength,
}); });
} else if (titleLength > this.siteSettings.max_topic_title_length) { } else if (titleLength > this.siteSettings.max_topic_title_length) {
reason = I18n.t("composer.error.title_too_long", { reason = I18n.t("composer.error.title_too_long", {
max: this.siteSettings.max_topic_title_length, count: this.siteSettings.max_topic_title_length,
}); });
} }

View File

@ -71,8 +71,10 @@ export default Controller.extend({
return; return;
} }
const joinedTags = tags.slice(0, displayN).join(", "); const joinedTags = tags
let more = Math.max(0, tags.length - displayN); .slice(0, displayN)
.join(I18n.t("tagging.tag_list_joiner"));
const more = Math.max(0, tags.length - displayN);
const tagsString = const tagsString =
more === 0 more === 0

View File

@ -1,4 +1,5 @@
import { avatarImg, formatUsername } from "discourse/lib/utilities"; import { avatarImg, formatUsername } from "discourse/lib/utilities";
import I18n from "I18n";
import { get } from "@ember/object"; import { get } from "@ember/object";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import { prioritizeNameInUx } from "discourse/lib/settings"; import { prioritizeNameInUx } from "discourse/lib/settings";
@ -59,7 +60,10 @@ function renderAvatar(user, options) {
// if a description has been provided // if a description has been provided
if (description && description.length > 0) { if (description && description.length > 0) {
// preprend the username before the description // preprend the username before the description
title = displayName + " - " + description; title = I18n.t("user.avatar.name_and_description", {
name: displayName,
description,
});
} }
} }
} }

View File

@ -5,6 +5,7 @@ import PreloadStore from "discourse/lib/preload-store";
import Site from "discourse/models/site"; import Site from "discourse/models/site";
import Topic from "discourse/models/topic"; import Topic from "discourse/models/topic";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { number } from "discourse/lib/formatter";
const CategoryList = ArrayProxy.extend({ const CategoryList = ArrayProxy.extend({
init() { init() {
@ -50,9 +51,14 @@ CategoryList.reopenClass({
case "week": case "week":
case "month": case "month":
const stat = c[`topics_${statPeriod}`]; const stat = c[`topics_${statPeriod}`];
const unit = I18n.t(statPeriod);
if (stat > 0) { if (stat > 0) {
c.stat = `<span class="value">${stat}</span> / <span class="unit">${unit}</span>`; const unit = I18n.t(`categories.topic_stat_unit.${statPeriod}`);
c.stat = I18n.t("categories.topic_stat", {
count: stat, // only used to correctly pluralize the string
number: `<span class="value">${number(stat)}</span>`,
unit: `<span class="unit">${unit}</span>`,
});
c.statTitle = I18n.t( c.statTitle = I18n.t(
`categories.topic_stat_sentence_${statPeriod}`, `categories.topic_stat_sentence_${statPeriod}`,
@ -61,13 +67,11 @@ CategoryList.reopenClass({
} }
); );
c[ c.pickAll = false;
"pick" + statPeriod[0].toUpperCase() + statPeriod.slice(1)
] = true;
break; break;
} }
default: default:
c.stat = `<span class="value">${c.topics_all_time}</span>`; c.stat = `<span class="value">${number(c.topics_all_time)}</span>`;
c.statTitle = I18n.t("categories.topic_sentence", { c.statTitle = I18n.t("categories.topic_sentence", {
count: c.topics_all_time, count: c.topics_all_time,
}); });
@ -75,6 +79,13 @@ CategoryList.reopenClass({
break; break;
} }
if (Site.currentProp("mobileView")) {
c.statTotal = I18n.t("categories.topic_stat_all_time", {
count: c.topics_all_time,
number: `<span class="value">${number(c.topics_all_time)}</span>`,
});
}
const record = Site.current().updateCategory(c); const record = Site.current().updateCategory(c);
record.setupGroupsAndPermissions(); record.setupGroupsAndPermissions();
categories.pushObject(record); categories.pushObject(record);

View File

@ -3,7 +3,7 @@
{{#if hasEdits}} {{#if hasEdits}}
<a href {{action "showEditHistory"}} <a href {{action "showEditHistory"}}
class="has-edits {{historyClass}}" class="has-edits {{historyClass}}"
title="{{i18n "post.last_edited_on"}} {{editedDate}}"> title={{i18n "post.last_edited_on" dateTime=editedDate}}>
{{d-icon "pencil-alt"}} {{d-icon "pencil-alt"}}
</a> </a>
{{/if}} {{/if}}

View File

@ -40,28 +40,18 @@
</tbody> </tbody>
</table> </table>
<footer class="clearfix category-topics-count"> <footer class="clearfix category-topics-count">
<figure title={{i18n "all_time_desc"}}> <div class="category-stat">
<a href={{category.url}}> <a href={{category.url}}>
{{number category.topics_all_time}} {{html-safe category.statTotal}}
<figcaption>{{i18n "all_time"}}</figcaption>
</a> </a>
</figure> </div>
{{#if category.pickMonth}} {{#unless category.pickAll}}
<figure title={{i18n "month_desc"}}> <div class="category-stat">
<a href={{category.url}}> <a href={{category.url}}>
{{number category.topics_month}} {{html-safe category.stat}}
<figcaption>/ {{i18n "month"}}</figcaption>
</a> </a>
</figure> </div>
{{/if}} {{/unless}}
{{#if category.pickWeek}}
<figure title={{i18n "week_desc"}}>
<a href={{category.url}}>
{{number category.topics_week}}
<figcaption>/ {{i18n "week"}}</figcaption>
</a>
</figure>
{{/if}}
</footer> </footer>
</div> </div>
{{/unless}} {{/unless}}

View File

@ -1,5 +1,5 @@
{{#d-modal-body}} {{#d-modal-body}}
<p>{{i18n "post.controls.delete_topic_confirm_modal" minViews=siteSettings.min_topic_views_for_delete_confirm}}</p> <p>{{i18n "post.controls.delete_topic_confirm_modal" count=siteSettings.min_topic_views_for_delete_confirm}}</p>
{{/d-modal-body}} {{/d-modal-body}}
<div class="modal-footer"> <div class="modal-footer">

View File

@ -43,12 +43,12 @@ export default createWidget("post-edits-indicator", {
className = `${className || ""} wiki`.trim(); className = `${className || ""} wiki`.trim();
if (attrs.version > 1) { if (attrs.version > 1) {
title = `${I18n.t("post.last_edited_on")} ${date}`; title = I18n.t("post.wiki_last_edited_on", { dateTime: date });
} else { } else {
title = I18n.t("post.wiki.about"); title = I18n.t("post.wiki.about");
} }
} else { } else {
title = `${I18n.t("post.last_edited_on")} ${date}`; title = I18n.t("post.last_edited_on", { dateTime: date });
} }
return this.attach("flat-button", { return this.attach("flat-button", {

View File

@ -17,12 +17,12 @@ createWidget("toggle-summary-description", {
(attrs.topicPostsCount * MIN_POST_READ_TIME) / 60 (attrs.topicPostsCount * MIN_POST_READ_TIME) / 60
) )
); );
return I18n.t("summary.description_time", { return I18n.messageFormat("summary.description_time_MF", {
replyCount: attrs.topicReplyCount, replyCount: attrs.topicReplyCount,
readingTime, readingTime,
}); });
} }
return I18n.t("summary.description", { replyCount: attrs.topicReplyCount }); return I18n.t("summary.description", { count: attrs.topicReplyCount });
}, },
html(attrs) { html(attrs) {

View File

@ -358,15 +358,14 @@ tr.category-topic-link {
> footer { > footer {
border-top: 1px solid var(--primary-low); border-top: 1px solid var(--primary-low);
padding: 7px 10px; padding: 7px 10px;
figure { .category-stat {
float: left; float: left;
margin: 3px 7px 0 0; margin: 3px 1em 0 0;
font-weight: bold;
font-size: $font-down-1;
}
figcaption {
display: inline;
font-weight: normal; font-weight: normal;
font-size: $font-down-1;
.value {
font-weight: bold;
}
} }
.btn { .btn {
float: right; float: right;

View File

@ -339,7 +339,7 @@ class GroupsController < ApplicationController
end end
if users.length > ADD_MEMBERS_LIMIT if users.length > ADD_MEMBERS_LIMIT
return render_json_error( return render_json_error(
I18n.t("groups.errors.adding_too_many_users", limit: ADD_MEMBERS_LIMIT) I18n.t("groups.errors.adding_too_many_users", count: ADD_MEMBERS_LIMIT)
) )
end end
usernames_already_in_group = group.users.where(id: users.map(&:id)).pluck(:username) usernames_already_in_group = group.users.where(id: users.map(&:id)).pluck(:username)

View File

@ -15,8 +15,13 @@ class TopicPoster < OpenStruct
} }
end end
# TODO: Remove when old list is removed def name_and_description
def [](attr) if SiteSetting.prioritize_username_in_ux? || user.name.blank?
public_send(attr) name = user.username
else
name = user.name
end
I18n.t("js.user.avatar.name_and_description", name: name, description: description)
end end
end end

View File

@ -9,7 +9,8 @@ class TopicPostersSummary
{ {
original_poster: I18n.t(:original_poster), original_poster: I18n.t(:original_poster),
most_recent_poster: I18n.t(:most_recent_poster), most_recent_poster: I18n.t(:most_recent_poster),
frequent_poster: I18n.t(:frequent_poster) frequent_poster: I18n.t(:frequent_poster),
joiner: I18n.t(:poster_description_joiner)
} }
end end
@ -64,7 +65,7 @@ class TopicPostersSummary
end end
def descriptions_for(user) def descriptions_for(user)
descriptions_by_id[user.id].join ', ' descriptions_by_id[user.id].join(@translations[:joiner])
end end
def shuffle_last_poster_to_back_in(summary) def shuffle_last_poster_to_back_in(summary)

View File

@ -92,13 +92,9 @@
</td> </td>
<td class='posters'> <td class='posters'>
<% t.posters.each do |poster| %> <% t.posters.each do |poster| %>
<% if poster.moreCount %> <a href="<%= Discourse.base_url %>/u/<%= poster.user.username %>" class="<%= poster.extras %>">
<a class="posters-more-count"><% poster.moreCount %></a> <img width="25" height="25" src="<%= poster.user.avatar_template.gsub('{size}', '25') %>" class="avatar" title='<%= h(poster.name_and_description) %>' aria-label='<%= h(poster.name_and_description) %>'>
<% else %> </a>
<a href="<%= Discourse.base_url %>/u/<%= poster.user.username %>" class="<%= poster.extraClasses %>">
<img width="25" height="25" src="<%= poster.user.avatar_template.gsub('{size}', '25') %>" class="avatar" title='<%= h("#{poster.user.username} - #{poster.description}") %>' aria-label='<%= h("#{poster.user.username} - #{poster.description}") %>'>
</a>
<% end %>
<% end %> <% end %>
</td> </td>
<td class="replies"> <td class="replies">

View File

@ -865,6 +865,15 @@ en:
topic_sentence: topic_sentence:
one: "%{count} topic" one: "%{count} topic"
other: "%{count} topics" other: "%{count} topics"
topic_stat:
one: "%{number} / %{unit}"
other: "%{number} / %{unit}"
topic_stat_unit:
week: "week"
month: "month"
topic_stat_all_time:
one: "%{number} total"
other: "%{number} total"
topic_stat_sentence_week: topic_stat_sentence_week:
one: "%{count} new topic in the past week." one: "%{count} new topic in the past week."
other: "%{count} new topics in the past week." other: "%{count} new topics in the past week."
@ -1516,6 +1525,7 @@ en:
avatar: avatar:
title: "Profile Picture" title: "Profile Picture"
header_title: "profile, messages, bookmarks and preferences" header_title: "profile, messages, bookmarks and preferences"
name_and_description: "%{name} - %{description}"
title: title:
title: "Title" title: "Title"
none: "(none)" none: "(none)"
@ -1592,16 +1602,6 @@ en:
learn_more: "learn more..." learn_more: "learn more..."
all_time: "total"
all_time_desc: "total topics created"
year: "year"
year_desc: "topics created in the last 365 days"
month: "month"
month_desc: "topics created in the last 30 days"
week: "week"
week_desc: "topics created in the last 7 days"
day: "day"
first_post: First post first_post: First post
mute: Mute mute: Mute
unmute: Unmute unmute: Unmute
@ -1626,8 +1626,11 @@ en:
summary: summary:
enabled_description: "You're viewing a summary of this topic: the most interesting posts as determined by the community." enabled_description: "You're viewing a summary of this topic: the most interesting posts as determined by the community."
description: "There are <b>%{replyCount}</b> replies." description:
description_time: "There are <b>%{replyCount}</b> replies with an estimated read time of <b>%{readingTime} minutes</b>." one: "There is <b>%{count}</b> reply."
other: "There are <b>%{count}</b> replies."
# keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details
description_time_MF: "There {replyCount, plural, one {is <b>#</b> reply} other {are <b>#</b> replies}} with an estimated read time of <b>{readingTime, plural, one {# minute} other {# minutes}}</b>."
enable: "Summarize This Topic" enable: "Summarize This Topic"
disable: "Show All Posts" disable: "Show All Posts"
@ -1891,10 +1894,16 @@ en:
error: error:
title_missing: "Title is required" title_missing: "Title is required"
title_too_short: "Title must be at least %{min} characters" title_too_short:
title_too_long: "Title can't be more than %{max} characters" one: "Title must be at least %{count} character"
other: "Title must be at least %{count} characters"
title_too_long:
one: "Title can't be more than %{count} character"
other: "Title can't be more than %{count} characters"
post_missing: "Post cant be empty" post_missing: "Post cant be empty"
post_length: "Post must be at least %{min} characters" post_length:
one: "Post must be at least %{count} character"
other: "Post must be at least %{count} characters"
try_like: "Have you tried the %{heart} button?" try_like: "Have you tried the %{heart} button?"
category_missing: "You must choose a category" category_missing: "You must choose a category"
tags_missing: tags_missing:
@ -2460,7 +2469,9 @@ en:
go: "go" go: "go"
jump_bottom: "jump to last post" jump_bottom: "jump to last post"
jump_prompt: "jump to..." jump_prompt: "jump to..."
jump_prompt_of: "of %{count} posts" jump_prompt_of:
one: "of %{count} post"
other: "of %{count} posts"
jump_prompt_long: "Jump to..." jump_prompt_long: "Jump to..."
jump_bottom_with_number: "jump to post %{post_number}" jump_bottom_with_number: "jump to post %{post_number}"
jump_prompt_to_date: "to date" jump_prompt_to_date: "to date"
@ -2749,8 +2760,8 @@ en:
edit_reason: "Reason: " edit_reason: "Reason: "
post_number: "post %{number}" post_number: "post %{number}"
ignored: "Ignored content" ignored: "Ignored content"
wiki_last_edited_on: "wiki last edited on" wiki_last_edited_on: "wiki last edited on %{dateTime}"
last_edited_on: "post last edited on" last_edited_on: "post last edited on %{dateTime}"
reply_as_new_topic: "Reply as linked Topic" reply_as_new_topic: "Reply as linked Topic"
reply_as_new_private_message: "Reply as new message to the same recipients" reply_as_new_private_message: "Reply as new message to the same recipients"
continue_discussion: "Continuing the discussion from %{postLink}:" continue_discussion: "Continuing the discussion from %{postLink}:"
@ -2876,7 +2887,9 @@ en:
unlock_post_description: "allow the poster to edit this post" unlock_post_description: "allow the poster to edit this post"
delete_topic_disallowed_modal: "You don't have permission to delete this topic. If you really want it to be deleted, submit a flag for moderator attention together with reasoning." delete_topic_disallowed_modal: "You don't have permission to delete this topic. If you really want it to be deleted, submit a flag for moderator attention together with reasoning."
delete_topic_disallowed: "you don't have permission to delete this topic" delete_topic_disallowed: "you don't have permission to delete this topic"
delete_topic_confirm_modal: "This topic currently has over %{minViews} views and may be a popular search destination. Are you sure you want to delete this topic entirely, instead of editing it to improve it?" delete_topic_confirm_modal:
one: "This topic currently has over %{count} view and may be a popular search destination. Are you sure you want to delete this topic entirely, instead of editing it to improve it?"
other: "This topic currently has over %{count} views and may be a popular search destination. Are you sure you want to delete this topic entirely, instead of editing it to improve it?"
delete_topic_confirm_modal_yes: "Yes, delete this topic" delete_topic_confirm_modal_yes: "Yes, delete this topic"
delete_topic_confirm_modal_no: "No, keep this topic" delete_topic_confirm_modal_no: "No, keep this topic"
delete_topic_error: "An error occurred while deleting this topic" delete_topic_error: "An error occurred while deleting this topic"
@ -3506,6 +3519,8 @@ en:
one: "%{tags} and %{count} more" one: "%{tags} and %{count} more"
other: "%{tags} and %{count} more" other: "%{tags} and %{count} more"
delete_no_unused_tags: "There are no unused tags." delete_no_unused_tags: "There are no unused tags."
# Example: "tag1, tag2, tag3"
tag_list_joiner: ", "
delete_unused: "Delete Unused Tags" delete_unused: "Delete Unused Tags"
delete_unused_description: "Delete all tags which are not attached to any topics or personal messages" delete_unused_description: "Delete all tags which are not attached to any topics or personal messages"
cancel_delete_unused: "Cancel" cancel_delete_unused: "Cancel"
@ -3749,7 +3764,9 @@ en:
effects: Effects effects: Effects
trust_levels_none: "None" trust_levels_none: "None"
automatic_membership_email_domains: "Users who register with an email domain that exactly matches one in this list will be automatically added to this group:" automatic_membership_email_domains: "Users who register with an email domain that exactly matches one in this list will be automatically added to this group:"
automatic_membership_user_count: "%{count} users have the new email domains and will be added to the group." automatic_membership_user_count:
one: "%{count} user has the new email domains and will be added to the group."
other: "%{count} users have the new email domains and will be added to the group."
primary_group: "Automatically set as primary group" primary_group: "Automatically set as primary group"
name_placeholder: "Group name, no spaces, same as username rule" name_placeholder: "Group name, no spaces, same as username rule"
primary: "Primary Group" primary: "Primary Group"

View File

@ -444,7 +444,9 @@ en:
email_already_used_in_category: "'%{email}' is already used by the category '%{category_name}'." email_already_used_in_category: "'%{email}' is already used by the category '%{category_name}'."
cant_allow_membership_requests: "You cannot allow membership requests for a group without any owners." cant_allow_membership_requests: "You cannot allow membership requests for a group without any owners."
already_requested_membership: "You have already requested membership for this group." already_requested_membership: "You have already requested membership for this group."
adding_too_many_users: "Maximum %{limit} users can be added at once" adding_too_many_users:
one: "Maximum %{count} user can be added at once"
other: "Maximum %{count} users can be added at once"
default_names: default_names:
everyone: "everyone" everyone: "everyone"
admins: "admins" admins: "admins"
@ -2361,6 +2363,8 @@ en:
most_posts: "Most Posts" most_posts: "Most Posts"
most_recent_poster: "Most Recent Poster" most_recent_poster: "Most Recent Poster"
frequent_poster: "Frequent Poster" frequent_poster: "Frequent Poster"
# Example: "Original Poster, Most Recent Poster"
poster_description_joiner: ", "
redirected_to_top_reasons: redirected_to_top_reasons:
new_user: "Welcome to our community! These are the most popular recent topics." new_user: "Welcome to our community! These are the most popular recent topics."

View File

@ -20,8 +20,8 @@ describe TopicPostersSummary do
summary.first.tap do |topic_poster| summary.first.tap do |topic_poster|
expect(topic_poster.user).to eq topic_creator expect(topic_poster.user).to eq topic_creator
expect(topic_poster.description).to eq( expect(topic_poster.description).to eq([
"#{I18n.t(:original_poster)}, #{I18n.t(:most_recent_poster)}" I18n.t(:original_poster), I18n.t(:most_recent_poster)].join(I18n.t(:poster_description_joiner))
) )
end end
end end

View File

@ -1281,7 +1281,7 @@ describe GroupsController do
expect(response.parsed_body["errors"]).to include(I18n.t( expect(response.parsed_body["errors"]).to include(I18n.t(
"groups.errors.adding_too_many_users", "groups.errors.adding_too_many_users",
limit: 1 count: 1
)) ))
ensure ensure
GroupsController.send(:remove_const, "ADD_MEMBERS_LIMIT") GroupsController.send(:remove_const, "ADD_MEMBERS_LIMIT")