mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Currently the `content_localization_language_switcher` validation error
names prerequisite settings as plain text so admins have to search for
each one manually.
This change extends the `{{setting:...}}` marker from #40338 with a
multi-setting form: `{{settings:one,two|label}}`. This renders a single
link to a filtered view of all the referenced settings.
The error message now links each required setting inline plus an "All
required settings" link. The admin UI receives an HTML message (and
plain text is still provided elsewhere, like the API).
<img width="1408" height="464" alt="image"
src="https://github.com/user-attachments/assets/576bcd79-daf1-4bd7-84ec-2add4db27f85"
/>
All settings are shown with an OR search prefixed by `any:` so
pipe-delineated settings can still be searched for separately
`any:content_localization_language_switcher|set_locale_from_cookie|allow_user_locale|content_localization_supported_locales`
<img width="2318" height="1706" alt="image"
src="https://github.com/user-attachments/assets/332092de-8d7c-4935-9e08-c3cb38a38fad"
/>
294 lines
8.4 KiB
Ruby
Vendored
294 lines
8.4 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module SiteSettings
|
|
class LabelFormatter
|
|
HUMANIZED_ACRONYMS =
|
|
Set.new(
|
|
%w[
|
|
2fa
|
|
acl
|
|
ai
|
|
api
|
|
arn
|
|
aws
|
|
bg
|
|
cdn
|
|
cors
|
|
csp
|
|
csrf
|
|
css
|
|
cta
|
|
csv
|
|
cx
|
|
db
|
|
dm
|
|
dns
|
|
eu
|
|
faq
|
|
fg
|
|
ga
|
|
gb
|
|
gif
|
|
gpu
|
|
gpt
|
|
gtm
|
|
hd
|
|
html
|
|
http
|
|
https
|
|
iam
|
|
id
|
|
imap
|
|
ip
|
|
jpg
|
|
json
|
|
kb
|
|
llm
|
|
mb
|
|
mfa
|
|
oauth
|
|
oidc
|
|
pdf
|
|
pm
|
|
png
|
|
pop3
|
|
rest
|
|
rss
|
|
s3
|
|
saml
|
|
smtp
|
|
sso
|
|
svg
|
|
tei
|
|
tl
|
|
tl0
|
|
tl1
|
|
tl2
|
|
tl3
|
|
tl4
|
|
tld
|
|
totp
|
|
txt
|
|
ui
|
|
url
|
|
ux
|
|
vpc
|
|
xml
|
|
yaml
|
|
yml
|
|
],
|
|
).freeze
|
|
|
|
HUMANIZED_MIXED_CASE = [
|
|
%w[apple Apple],
|
|
["adobe analytics", "Adobe Analytics"],
|
|
["amazon web services", "Amazon Web Services"],
|
|
%w[android Android],
|
|
%w[chinese Chinese],
|
|
%w[discord Discord],
|
|
%w[discourse Discourse],
|
|
["discourse connect", "Discourse Connect"],
|
|
["discourse discover", "Discourse Discover"],
|
|
["discourse narrative bot", "Discourse Narrative Bot"],
|
|
%w[facebook Facebook],
|
|
%w[foundation Foundation],
|
|
%w[github GitHub],
|
|
%w[google Google],
|
|
["google analytics", "Google Analytics"],
|
|
["google tag manager", "Google Tag Manager"],
|
|
%w[gravatar Gravatar],
|
|
%w[gravatars Gravatars],
|
|
%w[gitter Gitter],
|
|
%w[horizon Horizon],
|
|
%w[ios iOS],
|
|
%w[japanese Japanese],
|
|
%w[linkedin LinkedIn],
|
|
%w[meta Meta],
|
|
%w[mediaconvert MediaConvert],
|
|
%w[microsoft Microsoft],
|
|
%w[matrix Matrix],
|
|
%w[mattermost Mattermost],
|
|
%w[oauth2 OAuth2],
|
|
["openid connect", "OpenID Connect"],
|
|
%w[openai OpenAI],
|
|
%w[opengraph OpenGraph],
|
|
["powered by discourse", "Powered by Discourse"],
|
|
%w[tiktok TikTok],
|
|
%w[tos ToS],
|
|
%w[twitter Twitter],
|
|
%w[telegram Telegram],
|
|
%w[teams Teams],
|
|
%w[rocketchat RocketChat],
|
|
%w[slack Slack],
|
|
%w[vimeo Vimeo],
|
|
%w[wordpress WordPress],
|
|
%w[webex WebEx],
|
|
%w[youtube YouTube],
|
|
%w[zulip Zulip],
|
|
].freeze
|
|
|
|
HUMANIZED_MIXED_CASE_REGEX =
|
|
HUMANIZED_MIXED_CASE.map { |key, value| [/\b#{Regexp.escape(key)}\b/i, value] }.freeze
|
|
|
|
SETTING_LINK_PATTERN = /\{\{setting:([a-z][a-z0-9_]*)\}\}/
|
|
SETTINGS_LINK_PATTERN =
|
|
/\{\{settings:([a-z][a-z0-9_]*(?:,[a-z][a-z0-9_]*)*)(?:\|([^{}|]+))?\}\}/
|
|
|
|
LINK_ATTRIBUTES = %w[
|
|
class
|
|
data-setting-name
|
|
data-setting-area
|
|
data-setting-category
|
|
data-setting-plugin
|
|
].freeze
|
|
|
|
class << self
|
|
def description(setting)
|
|
desc = I18n.t("site_settings.#{setting}", base_path: Discourse.base_path, default: "")
|
|
expand_setting_links(desc)
|
|
end
|
|
|
|
def settings_filter_href(filter)
|
|
"#{Discourse.base_path}/admin/site_settings/category/all_results?filter=#{CGI.escape(filter)}"
|
|
end
|
|
|
|
def linkify(setting)
|
|
setting = setting.to_sym
|
|
|
|
# The href points at the generic "all settings" page as a fallback that
|
|
# works without JavaScript. In the admin UI the linkify-setting-links
|
|
# modifier rewrites it to the setting's actual config page, using the
|
|
# data attributes below so it doesn't have to look the metadata up.
|
|
attributes = {
|
|
"class" => "site-setting-link",
|
|
"href" => settings_filter_href(setting.to_s),
|
|
"data-setting-name" => setting,
|
|
}
|
|
|
|
if (area = SiteSetting.areas[setting]&.first)
|
|
attributes["data-setting-area"] = area
|
|
end
|
|
if (category = SiteSetting.categories[setting])
|
|
attributes["data-setting-category"] = category
|
|
end
|
|
if (plugin = SiteSetting.plugins[setting])
|
|
attributes["data-setting-plugin"] = plugin
|
|
end
|
|
|
|
attribute_string =
|
|
attributes.map { |name, value| %(#{name}="#{CGI.escapeHTML(value.to_s)}") }.join(" ")
|
|
|
|
%(<a #{attribute_string}>#{CGI.escapeHTML(humanized_name(setting))}</a>).html_safe
|
|
end
|
|
|
|
# Links a group of settings as a single anchor pointing at the
|
|
# all-settings page filtered to every name at once, via the explicit
|
|
# `any:one|two` OR filter syntax (unprefixed pipes stay literal so
|
|
# admins can search pipe-delimited list values exactly). Unlike #linkify
|
|
# there is no per-setting config page to rewrite the href to, so no data
|
|
# attributes are emitted for the linkify-setting-links modifier.
|
|
def linkify_settings(settings, label: nil)
|
|
filter = "any:#{settings.map(&:to_s).join("|")}"
|
|
label ||= settings.map { |setting| humanized_name(setting) }.join(", ")
|
|
|
|
%(<a class="site-setting-link" href="#{CGI.escapeHTML(settings_filter_href(filter))}">#{CGI.escapeHTML(label)}</a>).html_safe
|
|
end
|
|
|
|
def contains_setting_links?(text)
|
|
return false if text.blank?
|
|
text.match?(SETTING_LINK_PATTERN) || text.match?(SETTINGS_LINK_PATTERN)
|
|
end
|
|
|
|
def expand_setting_links(text, escape_text: false)
|
|
return text if text.blank? || !text.include?("{{setting")
|
|
|
|
text = CGI.escapeHTML(text) if escape_text
|
|
|
|
text
|
|
.gsub(SETTINGS_LINK_PATTERN) do
|
|
label = Regexp.last_match(2)&.strip
|
|
label = CGI.unescapeHTML(label) if escape_text && label
|
|
linkify_settings(Regexp.last_match(1).split(","), label:)
|
|
end
|
|
.gsub(SETTING_LINK_PATTERN) { linkify(Regexp.last_match(1)) }
|
|
.html_safe
|
|
end
|
|
|
|
# Renders markers as plain text — the humanized setting names (quoted)
|
|
# for {{setting:...}}, and the label or name list for {{settings:...}} —
|
|
# for surfaces that display error messages outside the admin UI.
|
|
def plain_setting_links(text)
|
|
return text if text.blank? || !text.include?("{{setting")
|
|
|
|
text
|
|
.gsub(SETTINGS_LINK_PATTERN) do
|
|
Regexp.last_match(2)&.strip ||
|
|
Regexp.last_match(1).split(",").map { |s| humanized_name(s) }.join(", ")
|
|
end
|
|
.gsub(SETTING_LINK_PATTERN) { "'#{humanized_name(Regexp.last_match(1))}'" }
|
|
end
|
|
|
|
def humanized_name(setting)
|
|
name = setting.to_s.tr("_", " ")
|
|
words = name.split(" ")
|
|
|
|
words[0] = words[0].capitalize
|
|
|
|
words.map! do |word|
|
|
word_downcase = word.downcase
|
|
|
|
if HUMANIZED_ACRONYMS.include?(word_downcase)
|
|
word.upcase
|
|
elsif word.end_with?("s") && HUMANIZED_ACRONYMS.include?(word_downcase[0...-1])
|
|
word_downcase[0...-1].upcase + "s"
|
|
else
|
|
word
|
|
end
|
|
end
|
|
|
|
result = words.join(" ")
|
|
|
|
HUMANIZED_MIXED_CASE_REGEX.each do |regex, replacement|
|
|
result = result.gsub(regex, replacement)
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def keywords(setting)
|
|
translated_keywords = I18n.t("site_settings.keywords.#{setting}", default: "")
|
|
english_translated_keywords = []
|
|
|
|
if I18n.locale != :en
|
|
english_translated_keywords =
|
|
I18n.t("site_settings.keywords.#{setting}", default: "", locale: :en).split("|")
|
|
end
|
|
|
|
# TODO (martin) We can remove this workaround of checking if
|
|
# we get an array back once keyword translations in languages other
|
|
# than English have been updated not to use YAML arrays.
|
|
if translated_keywords.is_a?(Array)
|
|
return(
|
|
(
|
|
translated_keywords + [SiteSetting.deprecated_setting_alias(setting)] +
|
|
english_translated_keywords
|
|
).compact
|
|
)
|
|
end
|
|
|
|
translated_keywords
|
|
.split("|")
|
|
.concat([SiteSetting.deprecated_setting_alias(setting)] + english_translated_keywords)
|
|
.compact
|
|
end
|
|
|
|
def placeholder(setting)
|
|
if !I18n.t("site_settings.placeholder.#{setting}", default: "").empty?
|
|
I18n.t("site_settings.placeholder.#{setting}")
|
|
elsif SiteIconManager.respond_to?("#{setting}_url")
|
|
SiteIconManager.public_send("#{setting}_url")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|