FEATURE: Experimental admin search

This feature allows admins to find what they are
looking for in the admin interface via a command
palette. This replaces the admin sidebar filter
as the focus of the Ctrl+/ command, but the sidebar
filter can also still be used. Perhaps at some point
we may remove it or change the shortcut.

The palette presents the following data for filtering:

* The admin nav map, which is also used for the sidebar
* All site settings
* Themes
* Components

Admins can also filter which items are shown in the palette.

This is still **extremely** WIP -- the interface for
the palette is not ideal, it's not keyboard accessible,
the design needs to be refined, the code needs to be
refined, and there are still numerous other considerations
here, like:

* Do we want to include reports?
* We need to include the automatically generated plugin setting
  pages added by Ted
* Do we want to show screenshots for themes and components
  if available, or stick to icons?
* Site setting filters are janky when visitng the same setting
  page with a different filter, the page is not refreshed
This commit is contained in:
Martin Brennan
2025-02-12 14:09:39 +10:00
parent 43e8172ebc
commit d956050ca1
22 changed files with 602 additions and 63 deletions

View File

@@ -0,0 +1,132 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button";
import DToggleSwitch from "discourse/components/d-toggle-switch";
import icon from "discourse/helpers/d-icon";
import autoFocus from "discourse/modifiers/auto-focus";
export default class AdminPaletteSearch extends Component {
@service adminPaletteDataSource;
@tracked filter = "";
@tracked searchResults = [];
@tracked showTypeFilters = false;
@tracked showPageType = true;
@tracked showSettingType = true;
@tracked showThemeType = true;
@tracked showComponentType = true;
constructor() {
super(...arguments);
this.adminPaletteDataSource.buildMap();
}
get visibleTypes() {
const types = [];
if (this.showPageType) {
types.push("page");
}
if (this.showSettingType) {
types.push("setting");
}
if (this.showThemeType) {
types.push("theme");
}
if (this.showComponentType) {
types.push("component");
}
return types;
}
@action
toggleTypeFilters() {
this.showTypeFilters = !this.showTypeFilters;
}
@action
toggleTypeFilter(type) {
this[type] = !this[type];
this.search();
}
@action
changeSearchTerm(event) {
this.searchResults = [];
this.filter = event.target.value;
this.search();
}
@action
search() {
this.searchResults = this.adminPaletteDataSource.search(this.filter, {
types: this.visibleTypes,
});
}
<template>
<input
type="text"
class="admin-palette__search-input"
{{autoFocus}}
{{on "input" this.changeSearchTerm}}
/>
<DButton @icon="filter" @action={{this.toggleTypeFilters}} />
{{#if this.showTypeFilters}}
<div class="admin-palette-type-filter">
<span class="admin-palette-type-filter__page">
Pages
<DToggleSwitch
@state={{this.showPageType}}
{{on "click" (fn this.toggleTypeFilter "showPageType")}}
/>
</span>
<span class="admin-palette-type-filter__setting">
Settings
<DToggleSwitch
@state={{this.showSettingType}}
{{on "click" (fn this.toggleTypeFilter "showSettingType")}}
/>
</span>
<span class="admin-palette-type-filter__theme">
Themes
<DToggleSwitch
@state={{this.showThemeType}}
{{on "click" (fn this.toggleTypeFilter "showThemeType")}}
/>
</span>
<span class="admin-palette-type-filter__component">
Components
<DToggleSwitch
@state={{this.showComponentType}}
{{on "click" (fn this.toggleTypeFilter "showComponentType")}}
/>
</span>
</div>
{{/if}}
<div class="admin-palette__search-results">
{{#each this.searchResults as |result|}}
<div class="admin-palette__search-result">
<a href={{result.url}}>
<div class="admin-palette__name">
{{icon result.icon}}
<span class="admin-palette__name-label">{{result.label}}</span>
<span class="admin-palette__type-pill">{{result.type}}</span>
</div>
{{#if result.description}}
<p class="admin-palette__description">{{htmlSafe
result.description
}}</p>
{{/if}}
</a>
</div>
{{/each}}
</div>
</template>
}

View File

@@ -0,0 +1,20 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import DModal from "discourse/components/d-modal";
import AdminPaletteSearch from "admin/components/admin-palette-search";
export default class AdminPaletteModal extends Component {
@service currentUser;
<template>
<DModal
@closeModal={{@closeModal}}
class="admin-palette-modal"
@title="admin.palette.search"
@inline={{@inline}}
@hideHeader={{true}}
>
<AdminPaletteSearch />
</DModal>
</template>
}

View File

@@ -0,0 +1,209 @@
import Service, { service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import escapeRegExp from "discourse/lib/escape-regexp";
import getURL from "discourse/lib/get-url";
import PreloadStore from "discourse/lib/preload-store";
import { ADMIN_NAV_MAP } from "discourse/lib/sidebar/admin-nav-map";
import I18n, { i18n } from "discourse-i18n";
const RESULT_TYPES = ["page", "setting", "theme", "component"];
export default class AdminPaletteDataSource extends Service {
@service router;
@service siteSettings;
pageMapItems = [];
settingMapItems = [];
themeMapItems = [];
componentMapItems = [];
settingPageMap = {
categories: {},
areas: {},
};
_mapCached = false;
buildMap() {
if (this._mapCached) {
return;
}
ADMIN_NAV_MAP.forEach((mapItem) => {
mapItem.links.forEach((link) => {
let url;
if (link.routeModels) {
url = this.router.urlFor(link.route, ...link.routeModels);
} else {
url = this.router.urlFor(link.route);
}
const mapItemLabel =
mapItem.text || (mapItem.label ? i18n(mapItem.label) : "");
const label =
mapItemLabel +
(mapItemLabel ? " > " : "") +
(link.text || (link.label ? i18n(link.label) : ""));
if (link.settings_area) {
this.settingPageMap.areas[link.settings_area] = link.multi_tabbed
? `${url}/settings`
: url;
}
if (link.settings_category) {
this.settingPageMap.categories[link.settings_category] =
link.multi_tabbed ? `${url}/settings` : url;
}
this.pageMapItems.push({
label,
url,
keywords:
(link.keywords ? i18n(link.keywords).toLowerCase() : "") +
" " +
url +
" " +
label.toLowerCase(),
type: "page",
icon: link.icon,
description: link.description ? i18n(link.description) : "",
});
});
});
// TODO (martin) Probably hash these with the plugin name as key
const visiblePlugins = PreloadStore.get("visiblePlugins") || [];
ajax("/admin/palette/settings.json").then((result) => {
result.forEach((setting) => {
// TODO: (martin) Might want to use the sidebar link name for this instead of the
// plugin category?
let rootLabel;
if (setting.plugin) {
rootLabel =
I18n.lookup(
`admin.site_settings.categories.${setting.plugin.replaceAll(
"-",
"_"
)}`
) || i18n("admin.plugins.title");
} else if (setting.primary_area) {
rootLabel =
I18n.lookup(`admin.config.${setting.primary_area}.title`) ||
i18n(`admin.site_settings.categories.${setting.category}`);
} else {
rootLabel = i18n(
`admin.site_settings.categories.${setting.category}`
);
}
const label = rootLabel + " > " + setting.setting;
let url;
if (setting.plugin) {
const plugin = visiblePlugins.find(
(visiblePlugin) => visiblePlugin.name === setting.plugin
);
if (plugin && plugin.admin_route) {
url = plugin.admin_route.use_new_show_route
? this.router.urlFor(
`adminPlugins.show.settings`,
plugin.admin_route.location,
{ queryParams: { filter: setting.setting } }
)
: this.router.urlFor(
`adminPlugins.${plugin.admin_route.location}`
);
} else {
url = getURL(
`/admin/site_settings/category/all_results?filter=${setting.setting}`
);
}
} else if (this.settingPageMap.areas[setting.primary_area]) {
url =
this.settingPageMap.areas[setting.primary_area] +
`?filter=${setting.setting}`;
} else if (this.settingPageMap.categories[setting.category]) {
url =
this.settingPageMap.categories[setting.category] +
`?filter=${setting.setting}`;
} else {
url = getURL(
`/admin/site_settings/category/all_results?filter=${setting.setting}`
);
}
this.settingMapItems.push({
label,
description: setting.description,
url,
keywords: (
setting.setting +
" " +
setting.setting.split("_").join(" ") +
" " +
setting.description +
" " +
setting.keywords.join(" ") +
" " +
rootLabel
).toLowerCase(),
type: "setting",
icon: "gear",
});
});
});
ajax("/admin/palette/themes-and-components.json").then((result) => {
result.forEach((themeOrComponent) => {
if (themeOrComponent.component) {
this.componentMapItems.push({
label: themeOrComponent.name,
description: themeOrComponent.description,
url: getURL(`/admin/customize/components/${themeOrComponent.id}`),
keywords: (
"component" +
" " +
themeOrComponent.description +
" " +
themeOrComponent.name
).toLowerCase(),
type: "component",
icon: "puzzle-piece",
});
} else {
this.themeMapItems.push({
label: themeOrComponent.name,
description: themeOrComponent.description,
url: getURL(`/admin/customize/themes/${themeOrComponent.id}`),
keywords: (
"theme" +
" " +
themeOrComponent.description +
" " +
themeOrComponent.name
).toLowerCase(),
type: "theme",
icon: "paintbrush",
});
}
});
});
this._mapCached = true;
}
search(filter, opts = {}) {
if (filter.length < 2) {
return [];
}
opts.types = opts.types || RESULT_TYPES;
const filteredResults = [];
const escapedFilterRegExp = escapeRegExp(filter.toLowerCase());
opts.types.forEach((type) => {
this[`${type}MapItems`].forEach((mapItem) => {
if (mapItem.keywords.match(escapedFilterRegExp)) {
filteredResults.push(mapItem);
}
});
});
return filteredResults;
}
}

View File

@@ -12,6 +12,7 @@ import {
import DiscourseURL from "discourse/lib/url";
import Composer from "discourse/models/composer";
import { capabilities } from "discourse/services/capabilities";
import AdminPaletteModal from "admin/components/modal/admin-palette";
let disabledBindings = [];
export function disableDefaultKeyboardShortcuts(bindings) {
@@ -496,10 +497,20 @@ export default {
if (filterInput) {
this._scrollTo(0);
filterInput.focus();
if (this.siteSettings.use_experimental_admin_search) {
this.showAdminSearchModal();
} else {
filterInput.focus();
}
}
},
showAdminSearchModal() {
const modal = getOwner(this).lookup("service:modal");
modal.show(AdminPaletteModal);
},
fullscreenComposer() {
const composer = getOwner(this).lookup("service:composer");
if (composer.get("model")) {

View File

@@ -1,4 +1,46 @@
export const ADMIN_NAV_MAP = [
{
text: "",
name: "root",
hideSectionHeader: true,
links: [
{
name: "admin_home",
route: "admin.dashboard.general",
label: "admin.dashboard.title",
icon: "house",
moderator: true,
},
{
name: "admin_users",
route: "adminUsers",
label: "admin.community.sidebar_link.users",
icon: "users",
moderator: true,
},
{
name: "admin_groups",
route: "groups",
label: "admin.community.sidebar_link.groups",
icon: "user-group",
moderator: true,
},
{
name: "admin_all_site_settings",
route: "adminSiteSettings",
label: "admin.advanced.sidebar_link.all_site_settings",
icon: "gear",
},
{
name: "admin_whats_new",
route: "admin.whatsNew",
label: "admin.account.sidebar_link.whats_new.title",
icon: "gift",
keywords: "admin.account.sidebar_link.whats_new.keywords",
moderator: true,
},
],
},
{
name: "account",
label: "admin.account.title",
@@ -8,6 +50,8 @@ export const ADMIN_NAV_MAP = [
route: "admin.backups",
label: "admin.account.sidebar_link.backups",
icon: "box-archive",
settings_category: "backups",
multi_tabbed: true,
},
],
},
@@ -33,6 +77,7 @@ export const ADMIN_NAV_MAP = [
route: "adminConfig.about",
label: "admin.community.sidebar_link.about_your_site",
icon: "gear",
settings_area: "about",
},
{
name: "admin_badges",
@@ -45,36 +90,48 @@ export const ADMIN_NAV_MAP = [
route: "adminConfig.loginAndAuthentication.settings",
label: "admin.community.sidebar_link.login_and_authentication",
icon: "unlock",
description: "admin.config.login_and_authentication.header_description",
settings_category: "login",
},
{
name: "admin_notifications",
route: "adminConfig.notifications.settings",
label: "admin.community.sidebar_link.notifications",
icon: "bell",
description: "admin.config.notifications.header_description",
settings_area: "notifications",
},
{
name: "admin_localization",
route: "adminConfig.localization.settings",
label: "admin.community.sidebar_link.localization.title",
icon: "globe",
description: "admin.config.localization.header_description",
settings_area: "localization",
},
{
name: "admin_permalinks",
route: "adminPermalinks",
label: "admin.community.sidebar_link.permalinks",
icon: "link",
settings_area: "permalinks",
multi_tabbed: true,
},
{
name: "admin_trust_levels",
route: "adminConfig.trustLevels.settings",
label: "admin.community.sidebar_link.trust_levels",
icon: "user-shield",
description: "admin.config.trust_levels.header_description",
settings_area: "trust_levels",
},
{
name: "admin_group_permissions",
route: "adminConfig.groupPermissions.settings",
label: "admin.community.sidebar_link.group_permissions",
icon: "user-gear",
description: "admin.config.group_permissions.header_description",
settings_area: "group_permissions",
},
{
name: "admin_user_fields",
@@ -94,6 +151,7 @@ export const ADMIN_NAV_MAP = [
route: "adminConfig.legal.settings",
label: "admin.community.sidebar_link.legal",
icon: "gavel",
description: "admin.config.legal.header_description",
},
{
name: "admin_moderation_flags",
@@ -101,6 +159,8 @@ export const ADMIN_NAV_MAP = [
label: "admin.community.sidebar_link.moderation_flags.title",
keywords: "admin.community.sidebar_link.moderation_flags.keywords",
icon: "flag",
settings_area: "flags",
multi_tabbed: true,
},
],
},
@@ -113,12 +173,16 @@ export const ADMIN_NAV_MAP = [
route: "adminConfig.fonts.settings",
label: "admin.appearance.sidebar_link.font_style",
icon: "italic",
description: "admin.config.font_style.header_description",
settings_area: "fonts",
},
{
name: "admin_site_logo",
route: "adminConfig.logo.settings",
label: "admin.appearance.sidebar_link.site_logo",
icon: "fab-discourse",
description: "admin.config.logo.header_description",
settings_category: "branding",
},
{
name: "admin_color_schemes",
@@ -131,12 +195,16 @@ export const ADMIN_NAV_MAP = [
route: "adminEmojis",
label: "admin.appearance.sidebar_link.emoji",
icon: "discourse-emojis",
settings_area: "emojis",
multi_tabbed: true,
},
{
name: "admin_navigation",
route: "adminConfig.navigation.settings",
label: "admin.appearance.sidebar_link.navigation",
icon: "diagram-project",
description: "admin.config.navigation.header_description",
settings_area: "navigation",
},
{
name: "admin_themes",
@@ -190,12 +258,16 @@ export const ADMIN_NAV_MAP = [
route: "adminConfig.security.settings",
label: "admin.security.sidebar_link.security",
icon: "lock",
description: "admin.config.security.header_description",
settings_category: "security",
},
{
name: "admin_spam",
route: "adminConfig.spam.settings",
label: "admin.security.sidebar_link.spam",
icon: "robot",
description: "admin.config.spam.header_description",
settings_category: "spam",
},
{
name: "admin_logs_staff_action_logs",
@@ -241,54 +313,73 @@ export const ADMIN_NAV_MAP = [
route: "adminConfig.developer.settings",
label: "admin.advanced.sidebar_link.developer",
icon: "keyboard",
description: "admin.config.developer.header_description",
settings_category: "developer",
},
{
name: "admin_embedding",
route: "adminEmbedding",
label: "admin.advanced.sidebar_link.embedding",
icon: "code",
settings_area: "embedding",
},
{
name: "admin_rate_limits",
route: "adminConfig.rate-limits.settings",
label: "admin.advanced.sidebar_link.rate_limits",
icon: "rocket",
description: "admin.config.rate_limits.header_description",
settings_category: "rate_limits",
},
{
name: "admin_user_api",
route: "adminConfig.user-api.settings",
label: "admin.advanced.sidebar_link.user_api",
icon: "shuffle",
description: "admin.config.user_api.header_description",
settings_category: "user_api",
},
{
name: "admin_onebox",
route: "adminConfig.onebox.settings",
label: "admin.advanced.sidebar_link.onebox",
icon: "far-square",
description: "admin.config.onebox.header_description",
settings_category: "onebox",
},
{
name: "admin_files",
route: "adminConfig.files.settings",
label: "admin.advanced.sidebar_link.files",
icon: "file",
description: "admin.config.files.header_description",
settings_category: "files",
},
{
name: "admin_other_options",
route: "adminConfig.other.settings",
label: "admin.advanced.sidebar_link.other_options",
icon: "discourse-other-tab",
description: "admin.config.other.header_description",
settings_category: "uncategorized",
},
{
name: "admin_search",
route: "adminConfig.search.settings",
label: "admin.advanced.sidebar_link.search",
icon: "magnifying-glass",
description: "admin.config.search.header_description",
settings_category: "search",
},
{
name: "admin_experimental",
route: "adminConfig.experimental.settings",
label: "admin.advanced.sidebar_link.experimental",
icon: "discourse-sparkles",
description: "admin.config.experimental.header_description",
settings_category: "experimental",
},
],
},

View File

@@ -184,53 +184,6 @@ function defineAdminSection(
}
export function useAdminNavConfig(navMap) {
const adminNavSections = [
{
text: "",
name: "root",
hideSectionHeader: true,
links: [
{
name: "admin_home",
route: "admin.dashboard.general",
label: "admin.dashboard.title",
icon: "house",
moderator: true,
},
{
name: "admin_users",
route: "adminUsers",
label: "admin.community.sidebar_link.users",
icon: "users",
moderator: true,
},
{
name: "admin_groups",
route: "groups",
label: "admin.community.sidebar_link.groups",
icon: "user-group",
moderator: true,
},
{
name: "admin_all_site_settings",
route: "adminSiteSettings",
label: "admin.advanced.sidebar_link.all_site_settings",
icon: "gear",
},
{
name: "admin_whats_new",
route: "admin.whatsNew",
label: "admin.account.sidebar_link.whats_new.title",
icon: "gift",
keywords: "admin.account.sidebar_link.whats_new.keywords",
moderator: true,
},
],
},
];
navMap = adminNavSections.concat(navMap);
for (const [sectionName, additionalLinks] of Object.entries(
additionalAdminSidebarSectionLinks
)) {

View File

@@ -1224,6 +1224,7 @@ a.inline-editable-field {
@import "common/admin/plugins";
@import "common/admin/site-settings";
@import "common/admin/admin_config_area";
@import "common/admin/palette_search";
@import "common/admin/admin_table";
@import "common/admin/admin_filter";
@import "common/admin/admin_reports";

View File

@@ -0,0 +1,42 @@
.admin-palette {
&__search-result {
border-top: 1px solid var(--primary-low);
padding: 0.5em 0;
}
&__description {
font-size: var(--font-down-1);
color: var(--primary-medium);
margin-top: 0.5em;
}
&__name {
color: var(--primary);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
.d-icon {
flex: 0;
margin-right: 0.5em;
}
}
&__name-label {
flex: 1;
}
&__type-pill {
background-color: var(--primary-low);
color: var(--primary-medium);
padding: 0.25em 0.5em;
border-radius: 0.25em;
font-size: var(--font-down-2);
margin-right: 0.5em;
}
}
.admin-palette-type-filter {
display: flex;
}

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
class Admin::PaletteController < Admin::AdminController
# TODO: (martin) Maybe get all of these at once for initial cache?
def settings
render_json_dump(
SiteSetting.all_settings(
filter_names: params[:filter_names],
filter_area: params[:filter_area],
filter_plugin: params[:plugin],
filter_categories: Array.wrap(params[:categories]),
include_locale_setting: params[:filter_area] == "localization",
basic_attributes: true,
),
)
end
def themes_and_components
themes = Theme.include_relations.order(:name)
render_json_dump(serialize_data(themes, BasicThemeSerializer))
end
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class BasicThemeSerializer < ApplicationSerializer
attributes :id, :name, :created_at, :updated_at, :default, :component
attributes :id, :name, :description, :created_at, :updated_at, :default, :component
def include_default?
object.id == SiteSetting.default_theme_id
@@ -10,4 +10,8 @@ class BasicThemeSerializer < ApplicationSerializer
def default
true
end
def description
object.internal_translations.find { |t| t.key == "theme_metadata.description" }&.value
end
end

View File

@@ -10,7 +10,6 @@ class ThemeSerializer < BasicThemeSerializer
:settings,
:errors,
:supported?,
:description,
:enabled?,
:disabled_at,
:theme_fields,
@@ -84,10 +83,6 @@ class ThemeSerializer < BasicThemeSerializer
@errors.present?
end
def description
object.internal_translations.find { |t| t.key == "theme_metadata.description" }&.value
end
def include_disabled_at?
object.component? && !object.enabled?
end

View File

@@ -5160,12 +5160,21 @@ en:
all: "All reports"
config:
about:
title: "About your site"
header_decription: "Provide information here about this site and your team so that people can learn what your community is about, who is behind it, and how to reach you in case there is an issue. Displayed on your site's <a href='%{basePath}/about'>About page</a>."
developer:
title: "Developer"
header_description: "Developer settings to control rate limits, multipliers and calculations, safe mode, and other advanced features"
experimental:
title: "Experimental"
header_description: "Toggle experimental features on or off for your site, most of these can be controlled on a group basis"
emojis:
title: "Emoji"
header_description: "Add new emoji that will be available to everyone. Select multiple files to create emojis using their file names. The selected group will be used for all files that are added at the same time"
flags:
title: "Moderation"
header_description: "The flagging system in Discourse helps you and your moderator team manage content and user behavior, keeping your community respectful and healthy. The defaults are suitable for most communities and you dont have to change them. However, if your site has particular requirements you can disable flags you dont need and add your own custom flags."
font_style:
title: "Font style"
header_description: "Customize the font styles used by your themes"

View File

@@ -386,9 +386,12 @@ Discourse::Application.routes.draw do
post "preview" => "badges#preview"
end
end
get "palette/settings" => "palette#settings"
get "palette/themes-and-components" => "palette#themes_and_components"
namespace :config, constraints: StaffConstraint.new do
resources :site_settings, only: %i[index]
get "developer" => "site_settings#index"
get "fonts" => "site_settings#index"
get "files" => "site_settings#index"

View File

@@ -3533,6 +3533,10 @@ experimental:
allow_any: false
refresh: true
area: "group_permissions|navigation"
use_experimental_admin_search:
default: false
hidden: true
client: true
glimmer_topic_list_mode:
client: true
type: enum

View File

@@ -201,6 +201,7 @@ module SiteSettingExtension
include_hidden: false,
include_locale_setting: true,
only_overridden: false,
basic_attributes: false,
filter_categories: nil,
filter_plugin: nil,
filter_names: nil,
@@ -271,15 +272,22 @@ module SiteSettingExtension
setting: s,
description: description(s),
keywords: keywords(s),
default: default,
value: value.to_s,
category: categories[s],
preview: previews[s],
secret: secret_settings.include?(s),
placeholder: placeholder(s),
mandatory_values: mandatory_values[s],
requires_confirmation: requires_confirmation_settings[s],
}.merge!(type_hash)
primary_area: areas[s]&.first,
}
if !basic_attributes
opts.merge!(
default: default,
value: value.to_s,
preview: previews[s],
secret: secret_settings.include?(s),
placeholder: placeholder(s),
mandatory_values: mandatory_values[s],
requires_confirmation: requires_confirmation_settings[s],
)
opts.merge!(type_hash)
end
opts[:plugin] = plugins[s] if plugins[s]

View File

@@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
discourse_details: "Discourse Details"
js:
details:
title: Hide Details

View File

@@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
discourse_local_dates: "Discourse Local Dates"
js:
discourse_local_dates:
relative_dates:

View File

@@ -4,3 +4,8 @@ en:
welcome_post_type:
new_user_track: "Start the new user tutorial for all new users"
welcome_message: "Send all new users a welcome message with a quick start guide"
admin_js:
admin:
site_settings:
categories:
discourse_narrative_bot: "Discourse Narrative Bot"

View File

@@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
footnote: "Discourse Footnotes"
js:
footnote:
title: "Footnotes"

View File

@@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
poll: "Discourse Poll"
js:
poll:
voters:

View File

@@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
spoiler_alert: "Discourse Spoiler Alert"
js:
spoiler:
title: Blur Spoiler

View File

@@ -1,4 +1,9 @@
en:
admin_js:
admin:
site_settings:
categories:
styleguide: "Discourse Styleguide"
js:
styleguide:
title: "Styleguide"