FIX: Use localized auto group names instead of hardcoded English (#37757)

Automatic group names (everyone, staff, admins, etc.) are translated
based on the site's default locale. However, the `AUTO_GROUPS` JS
constant had hardcoded English names which were used directly in several
places — most notably during category creation. This caused the
"everyone" group to always display its English name in the permissions
table, even on sites with a different default locale.

This removes the `name` and `display_name` fields from `AUTO_GROUPS`,
keeping only `id` and `automatic`. A new `groupsById` getter on the
`Site` model provides a lookup map built from server-provided group data
(which contains the properly localized names). All consumers now resolve
group names via `site.groupsById[groupId].name` instead of reading from
the constant.

The avatar-flair system is also refactored from name-based to ID-based
lookups, and group identity checks in `user.js` now compare by `id`
rather than `name`.

https://meta.discourse.org/t/395225
This commit is contained in:
Régis Hanol
2026-02-16 09:41:36 +01:00
committed by GitHub
parent 4879ae0ae5
commit 574ca5f825
13 changed files with 61 additions and 95 deletions
@@ -39,10 +39,7 @@ export default class AdminConfigAreasAboutContactInformation extends Component {
@action
setContactGroup(groupIds, { set }) {
this.contactGroupId = groupIds[0];
set(
"contactGroupName",
this.site.groups.find((group) => group.id === groupIds[0])?.name
);
set("contactGroupName", this.site.groupsById[groupIds[0]]?.name);
}
@action
@@ -26,6 +26,7 @@ import { eq } from "discourse/truth-helpers";
import { i18n } from "discourse-i18n";
export default class UpcomingChangeItem extends Component {
@service site;
@service toasts;
@tracked bufferedGroups = this.args.change.groups;
@@ -246,7 +247,7 @@ export default class UpcomingChangeItem extends Component {
await this.toggleChange(isEnabled, newValue);
if (newValue === "staff") {
this.groupsChanged(AUTO_GROUPS.staff.name);
this.groupsChanged(this.site.groupsById[AUTO_GROUPS.staff.id].name);
} else if (newValue === "everyone" || newValue === "no_one") {
this.groupsChanged("");
}
@@ -22,12 +22,6 @@ import IconPicker from "discourse/select-kit/components/icon-picker";
import { eq, or } from "discourse/truth-helpers";
import { i18n } from "discourse-i18n";
const EVERYONE_FULL_PERMISSION = {
group_id: AUTO_GROUPS.everyone.id,
group_name: AUTO_GROUPS.everyone.name,
permission_type: PermissionType.FULL,
};
export default class UpsertCategoryGeneral extends Component {
@service site;
@service siteSettings;
@@ -44,6 +38,15 @@ export default class UpsertCategoryGeneral extends Component {
#previousPermissions = null;
// This needs to be dynamic because the name of the everyone group can be changed by admins
get #everyoneFullPermission() {
return {
group_id: AUTO_GROUPS.everyone.id,
group_name: this.site.groupsById[AUTO_GROUPS.everyone.id].name,
permission_type: PermissionType.FULL,
};
}
get isParentRestricted() {
const parentId = this.args.transientData.parent_category_id;
if (!parentId) {
@@ -101,10 +104,9 @@ export default class UpsertCategoryGeneral extends Component {
@action
onChangeAccessGroups(groupIds) {
const newPermissions = groupIds.map((groupId) => {
const group = this.site.groups.find((g) => g.id === groupId);
return {
group_id: groupId,
group_name: group?.name,
group_name: this.site.groupsById[groupId]?.name,
permission_type: PermissionType.FULL,
};
});
@@ -200,7 +202,7 @@ export default class UpsertCategoryGeneral extends Component {
};
if (value === "public") {
this.#setFormPermissions([EVERYONE_FULL_PERMISSION]);
this.#setFormPermissions([this.#everyoneFullPermission]);
} else if (value === "group_restricted") {
if (this.#previousPermissions?.length) {
this.#setFormPermissions(this.#previousPermissions);
@@ -214,7 +216,7 @@ export default class UpsertCategoryGeneral extends Component {
async onParentCategoryChange(parentCategoryId) {
if (!parentCategoryId) {
this.categoryVisibilityState = null;
this.#setFormPermissions([EVERYONE_FULL_PERMISSION]);
this.#setFormPermissions([this.#everyoneFullPermission]);
return;
}
@@ -239,7 +241,7 @@ export default class UpsertCategoryGeneral extends Component {
this.#setFormPermissions(newPermissions);
} else {
this.#setFormPermissions([EVERYONE_FULL_PERMISSION]);
this.#setFormPermissions([this.#everyoneFullPermission]);
}
}
} catch (error) {
@@ -109,11 +109,10 @@ export default class UpsertCategorySecurity extends Component {
@action
onSelectGroup(groupId) {
const group = this.site.groups.find((g) => g.id === groupId);
const newPermissions = [
...(this.permissions || []),
{
group_name: group?.name,
group_name: this.site.groupsById[groupId]?.name,
group_id: groupId,
permission_type: this.minimumPermission,
},
@@ -78,7 +78,7 @@ export default class NewCategory extends DiscourseRoute {
return [
{
group_id: AUTO_GROUPS.everyone.id,
group_name: AUTO_GROUPS.everyone.name,
group_name: this.site.groupsById[AUTO_GROUPS.everyone.id].name,
permission_type: PermissionType.FULL,
},
];
+15 -14
View File
@@ -3,9 +3,9 @@ import { AUTO_GROUPS } from "discourse/lib/constants";
let _autoGroupFlair, _noAutoFlair;
// All automatic groups except "everyone" (which doesn't have flair)
const FLAIR_GROUP_NAMES = Object.values(AUTO_GROUPS)
const FLAIR_AUTO_GROUP_IDS = Object.values(AUTO_GROUPS)
.filter((g) => g.id !== AUTO_GROUPS.everyone.id)
.map((g) => g.name);
.map((g) => g.id);
export default function autoGroupFlairForUser(site, user) {
if (!_autoGroupFlair) {
@@ -17,24 +17,25 @@ export default function autoGroupFlairForUser(site, user) {
return null;
}
if (user.admin && _autoGroupFlair.admins) {
return _autoGroupFlair.admins;
if (user.admin && _autoGroupFlair[AUTO_GROUPS.admins.id]) {
return _autoGroupFlair[AUTO_GROUPS.admins.id];
}
if (user.moderator && _autoGroupFlair.moderators) {
return _autoGroupFlair.moderators;
if (user.moderator && _autoGroupFlair[AUTO_GROUPS.moderators.id]) {
return _autoGroupFlair[AUTO_GROUPS.moderators.id];
}
if (_autoGroupFlair.staff && (user.admin || user.moderator)) {
return _autoGroupFlair.staff;
if ((user.admin || user.moderator) && _autoGroupFlair[AUTO_GROUPS.staff.id]) {
return _autoGroupFlair[AUTO_GROUPS.staff.id];
}
let trustLevel = user.trust_level || user.trustLevel;
if (trustLevel) {
for (let i = trustLevel; i >= 0; i--) {
if (_autoGroupFlair[`trust_level_${i}`]) {
return _autoGroupFlair[`trust_level_${i}`];
const group = AUTO_GROUPS[`trust_level_${i}`];
if (group && _autoGroupFlair[group.id]) {
return _autoGroupFlair[group.id];
}
}
}
@@ -49,11 +50,11 @@ function initializeAutoGroupFlair(site) {
_autoGroupFlair = {};
_noAutoFlair = true;
FLAIR_GROUP_NAMES.forEach((groupName) => {
const group = site.groups?.find((item) => item.name === groupName);
if (group && group.flair_url) {
FLAIR_AUTO_GROUP_IDS.forEach((groupId) => {
const group = site.groupsById[groupId];
if (group?.flair_url) {
_noAutoFlair = false;
_autoGroupFlair[groupName] = {
_autoGroupFlair[groupId] = {
flair_name: group.name.replace(/_/g, " "),
flair_url: group.flair_url,
flair_bg_color: group.flair_bg_color,
+9 -44
View File
@@ -27,50 +27,15 @@ export const CATEGORY_STYLE_TYPES = { square: 0, icon: 1, emoji: 2 };
export const CATEGORY_TEXT_COLORS = ["FFFFFF", "000000"];
export const AUTO_GROUPS = {
everyone: {
id: 0,
automatic: true,
name: "everyone",
display_name: "everyone",
},
admins: { id: 1, automatic: true, name: "admins", display_name: "admins" },
moderators: {
id: 2,
automatic: true,
name: "moderators",
display_name: "moderators",
},
staff: { id: 3, automatic: true, name: "staff", display_name: "staff" },
trust_level_0: {
id: 10,
automatic: true,
name: "trust_level_0",
display_name: "trust_level_0",
},
trust_level_1: {
id: 11,
automatic: true,
name: "trust_level_1",
display_name: "trust_level_1",
},
trust_level_2: {
id: 12,
automatic: true,
name: "trust_level_2",
display_name: "trust_level_2",
},
trust_level_3: {
id: 13,
automatic: true,
name: "trust_level_3",
display_name: "trust_level_3",
},
trust_level_4: {
id: 14,
automatic: true,
name: "trust_level_4",
display_name: "trust_level_4",
},
everyone: { id: 0, automatic: true, name: "everyone" },
admins: { id: 1, automatic: true, name: "admins" },
moderators: { id: 2, automatic: true, name: "moderators" },
staff: { id: 3, automatic: true, name: "staff" },
trust_level_0: { id: 10, automatic: true, name: "trust_level_0" },
trust_level_1: { id: 11, automatic: true, name: "trust_level_1" },
trust_level_2: { id: 12, automatic: true, name: "trust_level_2" },
trust_level_3: { id: 13, automatic: true, name: "trust_level_3" },
trust_level_4: { id: 14, automatic: true, name: "trust_level_4" },
};
export const GROUP_SMTP_SSL_MODES = { none: 0, ssl_tls: 1, starttls: 2 };
+2 -1
View File
@@ -849,7 +849,8 @@ export default class Category extends RestModel {
permissions.forEach((p) => (rval[p.group_name] = p.permission_type));
} else {
// empty permissions => staff-only access
rval[AUTO_GROUPS.staff.name] = PermissionType.FULL;
rval[Site.currentProp("groupsById")[AUTO_GROUPS.staff.id].name] =
PermissionType.FULL;
}
return rval;
}
+8
View File
@@ -7,6 +7,7 @@ import { htmlSafe } from "@ember/template";
import { isEmpty } from "@ember/utils";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import { removeValueFromArray } from "discourse/lib/array-tools";
import { AUTO_GROUPS } from "discourse/lib/constants";
import discourseComputed from "discourse/lib/decorators";
import deprecated, { withSilencedDeprecations } from "discourse/lib/deprecated";
import { isRailsTesting, isTesting } from "discourse/lib/environment";
@@ -101,6 +102,13 @@ export default class Site extends RestModel {
this.topicCountDesc = ["topic_count:desc"];
}
get groupsById() {
const map = {};
Object.values(AUTO_GROUPS).forEach((g) => (map[g.id] = g));
this.groups?.forEach((g) => (map[g.id] = g));
return map;
}
@dependentKeyCompat
get desktopView() {
return !this.mobileView;
+1 -1
View File
@@ -792,7 +792,7 @@ export default class User extends RestModel.extend(Evented) {
const groups = this.groups || [];
return groups.filter((group) => {
return !group.automatic || group.name === AUTO_GROUPS.moderators.name;
return !group.automatic || group.id === AUTO_GROUPS.moderators.id;
});
}
@@ -2,33 +2,34 @@ import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import UserAvatarFlair from "discourse/components/user-avatar-flair";
import { resetFlair } from "discourse/lib/avatar-flair";
import { AUTO_GROUPS } from "discourse/lib/constants";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
function setupSiteGroups(that) {
that.site.groups = [
{
id: 1,
id: AUTO_GROUPS.admins.id,
name: "admins",
flair_url: "bars",
flair_bg_color: "CC000A",
flair_color: "FFFFFA",
},
{
id: 2,
id: AUTO_GROUPS.staff.id,
name: "staff",
flair_url: "bars",
flair_bg_color: "CC0005",
flair_color: "FFFFF5",
},
{
id: 3,
id: AUTO_GROUPS.trust_level_1.id,
name: "trust_level_1",
flair_url: "dice-one",
flair_bg_color: "CC0001",
flair_color: "FFFFF1",
},
{
id: 4,
id: AUTO_GROUPS.trust_level_2.id,
name: "trust_level_2",
flair_url: "dice-two",
flair_bg_color: "CC0002",
+1 -8
View File
@@ -20,14 +20,7 @@ task "javascript:update_constants" => :environment do
auto_groups =
Group::AUTO_GROUPS.inject({}) do |result, (group_name, group_id)|
result.merge(
group_name => {
id: group_id,
automatic: true,
name: group_name,
display_name: group_name,
},
)
result.merge(group_name => { id: group_id, automatic: true, name: group_name })
end
MAIN_FONT_KEYS = %w[helvetica inter lato montserrat open_sans poppins roboto merriweather mukta]
@@ -33,6 +33,7 @@ import RagUploader from "./rag-uploader";
export default class PersonaEditor extends Component {
@service router;
@service site;
@service dialog;
@service toasts;
@service siteSettings;
@@ -108,10 +109,7 @@ export default class PersonaEditor extends Component {
(g) => g.id === AUTO_GROUPS.everyone.id
);
if (!hasEveryoneGroup) {
groups.push({
id: AUTO_GROUPS.everyone.id,
name: AUTO_GROUPS.everyone.name,
});
groups.push(this.site.groupsById[AUTO_GROUPS.everyone.id]);
}
this.allGroups = groups;