mirror of
https://github.com/discourse/discourse.git
synced 2026-08-05 18:09:26 +08:00
The /about page (unlike /guidelines or /tos) is not backed by a topic, but instead via a bunch of site settings. If it were backed by a topic, localization would be easy as it can just use TopicLocalizations. We also can't swap it out to use a topic, since there are so many fields on the about page that can't be safely contained in a topic. ---------- This PR adds SiteSettingLocalization, allowing new attributes in site settings config for localization. For now this is wired into the About page settings flow: - admins can select a non-default locale on /admin/config/about - only translatable About fields are shown for that locale - localized About values are used on /about and /about.json - company_url can be set manually per locale, but is excluded from automatic translation - ~~AI backfill can translate eligible site setting localizations~~ no backfill (added in https://github.com/discourse/discourse/pull/41123/commits/386c655d2f44aca54440c069bd06ea8007793d84, removed in https://github.com/discourse/discourse/pull/41123/commits/6f8ec9cc43dca4195bda638b1ce980eb68a59941. about page has very non automa-ble fields This intentionally keeps the scope narrow. It does not make every site setting localizable, and any other setting can be localized by adding to that allowlist. Some screenshots |page | 📸 | |--|--| | eng setting | <img width="1278" height="871" alt="Screenshot 2026-06-23 at 11 00 34 PM" src="https://github.com/user-attachments/assets/59431464-fb05-41d9-8c4f-fd9aae7fb45f" /> | ja setting | <img width="1278" height="871" alt="Screenshot 2026-06-23 at 11 01 20 PM" src="https://github.com/user-attachments/assets/73cd2878-f59a-4dc4-a76d-2f46b74a6eed" /> | ja anon about page | <img width="1278" height="871" alt="Screenshot 2026-06-23 at 11 01 48 PM" src="https://github.com/user-attachments/assets/02464a5f-9ca0-4519-9521-be41498c1967" />
202 lines
5 KiB
Ruby
Vendored
202 lines
5 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
class About
|
|
class CategoryMods
|
|
include ActiveModel::Serialization
|
|
attr_reader :category, :moderators
|
|
|
|
def initialize(category, moderators)
|
|
@category = category
|
|
@moderators = moderators
|
|
end
|
|
|
|
def parent_category
|
|
category.parent_category
|
|
end
|
|
end
|
|
|
|
include ActiveModel::Serialization
|
|
include StatsCacheable
|
|
|
|
def self.stats_cache_key
|
|
"about-stats"
|
|
end
|
|
|
|
def self.fetch_stats
|
|
Stat.api_stats
|
|
end
|
|
|
|
def initialize(user = nil, locale: I18n.locale, show_original: false)
|
|
@user = user
|
|
@locale = locale
|
|
@show_original = show_original
|
|
end
|
|
|
|
def version
|
|
Discourse::VERSION::STRING
|
|
end
|
|
|
|
def https
|
|
SiteSetting.force_https
|
|
end
|
|
|
|
def title
|
|
localized_site_setting(:title)
|
|
end
|
|
|
|
def locale
|
|
SiteSetting.default_locale
|
|
end
|
|
|
|
def description
|
|
localized_site_setting(:site_description)
|
|
end
|
|
|
|
def extended_site_description
|
|
localized_site_setting(
|
|
:extended_site_description,
|
|
cooked: true,
|
|
fallback: SiteSetting.extended_site_description_cooked,
|
|
)
|
|
end
|
|
|
|
def banner_image
|
|
url = SiteSetting.about_banner_image&.url
|
|
return if url.blank?
|
|
GlobalPath.full_cdn_url(url)
|
|
end
|
|
|
|
def site_creation_date
|
|
Discourse.site_creation_date
|
|
end
|
|
|
|
def moderators
|
|
@moderators ||=
|
|
if guardian.public_can_see_profiles?
|
|
apply_hidden_profile(
|
|
apply_excluded_groups(
|
|
User.where(moderator: true).human_users.order(last_seen_at: :desc),
|
|
ignore_groups: [Group::AUTO_GROUPS[:admins]],
|
|
),
|
|
)
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def admins
|
|
@admins ||=
|
|
if guardian.public_can_see_profiles?
|
|
DiscoursePluginRegistry.apply_modifier(
|
|
:about_admins,
|
|
apply_hidden_profile(
|
|
apply_excluded_groups(
|
|
User.where(admin: true).human_users.order(last_seen_at: :desc),
|
|
ignore_groups: [Group::AUTO_GROUPS[:moderators]],
|
|
),
|
|
),
|
|
)
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
def stats
|
|
@stats ||= About.fetch_cached_stats
|
|
end
|
|
|
|
def category_moderators
|
|
return [] unless guardian.public_can_see_profiles?
|
|
|
|
allowed_cats = guardian.allowed_category_ids
|
|
return [] if allowed_cats.blank?
|
|
|
|
cats_with_mods = Category.joins(:category_moderation_groups).distinct.pluck(:id)
|
|
|
|
category_ids = cats_with_mods & allowed_cats
|
|
return [] if category_ids.blank?
|
|
|
|
per_cat_limit = category_mods_limit / category_ids.size
|
|
per_cat_limit = 1 if per_cat_limit < 1
|
|
|
|
filter_hidden_profiles = !guardian.is_staff? && SiteSetting.allow_users_to_hide_profile
|
|
|
|
results = DB.query(<<~SQL, category_ids:)
|
|
WITH moderator_users AS (
|
|
SELECT
|
|
cmg.category_id AS category_id,
|
|
u.id AS user_id,
|
|
u.last_seen_at,
|
|
ROW_NUMBER() OVER (PARTITION BY cmg.category_id, u.id ORDER BY u.last_seen_at DESC) as rn
|
|
FROM category_moderation_groups cmg
|
|
INNER JOIN group_users gu
|
|
ON cmg.group_id = gu.group_id
|
|
INNER JOIN users u
|
|
ON gu.user_id = u.id
|
|
#{"LEFT JOIN user_options uo ON uo.user_id = u.id" if filter_hidden_profiles}
|
|
WHERE cmg.category_id IN (:category_ids)
|
|
#{"AND uo.hide_profile IS NOT TRUE" if filter_hidden_profiles}
|
|
)
|
|
SELECT id AS category_id, user_ids
|
|
FROM categories
|
|
INNER JOIN (
|
|
SELECT
|
|
category_id,
|
|
(ARRAY_AGG(user_id ORDER BY last_seen_at DESC))[:#{per_cat_limit}] AS user_ids
|
|
FROM moderator_users
|
|
WHERE rn = 1
|
|
GROUP BY category_id
|
|
) X
|
|
ON X.category_id = id
|
|
ORDER BY position
|
|
SQL
|
|
|
|
cats = Category.where(id: results.map(&:category_id)).index_by(&:id)
|
|
mods = User.where(id: results.map(&:user_ids).flatten.uniq).index_by(&:id)
|
|
|
|
results.map { |row| CategoryMods.new(cats[row.category_id], mods.values_at(*row.user_ids)) }
|
|
end
|
|
|
|
def category_mods_limit
|
|
@category_mods_limit || 100
|
|
end
|
|
|
|
def category_mods_limit=(number)
|
|
@category_mods_limit = number
|
|
end
|
|
|
|
private
|
|
|
|
def guardian
|
|
@guardian ||= Guardian.new(@user)
|
|
end
|
|
|
|
def localized_site_setting(setting_name, cooked: false, fallback: nil)
|
|
SiteSettingLocalization.value_for(
|
|
setting_name,
|
|
locale: @locale,
|
|
cooked:,
|
|
fallback: fallback || SiteSetting.public_send(setting_name),
|
|
show_original: @show_original,
|
|
)
|
|
end
|
|
|
|
def apply_hidden_profile(query)
|
|
return query if guardian.is_staff?
|
|
return query unless SiteSetting.allow_users_to_hide_profile
|
|
|
|
query.left_joins(:user_option).where("user_options.hide_profile IS NOT TRUE")
|
|
end
|
|
|
|
def apply_excluded_groups(query, ignore_groups: [])
|
|
group_ids = SiteSetting.about_page_hidden_groups_map - ignore_groups
|
|
return query if group_ids.blank?
|
|
|
|
query.joins(
|
|
DB.sql_fragment(
|
|
"LEFT JOIN group_users ON group_users.group_id IN (:group_ids) AND group_users.user_id = users.id",
|
|
group_ids:,
|
|
),
|
|
).where("group_users.id": nil)
|
|
end
|
|
end
|