diff --git a/Gemfile.lock b/Gemfile.lock index 56b4d03c26..5d4ea41328 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,12 +61,12 @@ GEM aws-sdk-sns (1.21.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.0) + aws-sigv4 (1.1.1) aws-eventstream (~> 1.0, >= 1.0.2) barber (0.12.2) ember-source (>= 1.0, < 3.1) execjs (>= 1.2, < 3) - better_errors (2.5.1) + better_errors (2.6.0) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -141,7 +141,7 @@ GEM globalid (0.4.2) activesupport (>= 4.2.0) guess_html_encoding (0.0.11) - hashdiff (1.0.0) + hashdiff (1.0.1) hashie (3.6.0) highline (1.7.10) hkdf (0.3.0) @@ -171,7 +171,7 @@ GEM logstash-event (1.2.02) logstash-logger (0.26.1) logstash-event (~> 1.2) - logster (2.7.0) + logster (2.7.1) loofah (2.4.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -204,7 +204,7 @@ GEM multipart-post (2.1.1) mustache (1.1.1) nio4r (2.5.2) - nokogiri (1.10.8) + nokogiri (1.10.9) mini_portile2 (~> 2.4.0) nokogumbo (2.0.2) nokogiri (~> 1.8, >= 1.8.4) @@ -215,7 +215,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - oj (3.10.2) + oj (3.10.5) omniauth (1.9.0) hashie (>= 3.4.6, < 3.7.0) rack (>= 1.6.2, < 3) @@ -252,7 +252,7 @@ GEM parallel (1.19.1) parallel_tests (2.31.0) parallel - parser (2.7.0.2) + parser (2.7.0.4) ast (~> 2.4.0) pg (1.2.2) progress (3.5.2) @@ -264,7 +264,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.3) - puma (4.3.1) + puma (4.3.3) nio4r (~> 2.0) r2 (0.2.7) rack (2.0.8) @@ -279,9 +279,9 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - rails_multisite (2.0.7) - activerecord (> 4.2, < 7) - railties (> 4.2, < 7) + rails_multisite (2.1.0) + activerecord (> 5.0, < 7) + railties (> 5.0, < 7) railties (6.0.1) actionpack (= 6.0.1) activesupport (= 6.0.1) @@ -339,7 +339,7 @@ GEM rspec-support (~> 3.8) rspec-support (3.9.2) rtlit (0.0.5) - rubocop (0.80.0) + rubocop (0.80.1) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.7.0.1) @@ -379,10 +379,10 @@ GEM rack (~> 2.0) rack-protection (>= 2.0.0) redis (>= 4.1.0) - simplecov (0.18.3) + simplecov (0.18.5) docile (~> 1.1) simplecov-html (~> 0.11) - simplecov-html (0.12.1) + simplecov-html (0.12.2) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 index 9cdca3f098..a27022390f 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 @@ -53,7 +53,7 @@ export default Controller.extend({ }, copy() { - var newColorScheme = Ember.copy(this.model, true); + const newColorScheme = this.model.copy(); newColorScheme.set( "name", I18n.t("admin.customize.colors.copy_name_prefix") + diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 index 869918bebb..e2ff119aa4 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 @@ -26,7 +26,7 @@ export default Controller.extend({ actions: { newColorSchemeWithBase(baseKey) { const base = this.baseColorSchemes.findBy("base_scheme_id", baseKey); - const newColorScheme = Ember.copy(base, true); + const newColorScheme = base.copy(); newColorScheme.setProperties({ name: I18n.t("admin.customize.colors.new_name"), base_scheme_id: base.get("base_scheme_id") diff --git a/app/assets/javascripts/admin/models/color-scheme.js.es6 b/app/assets/javascripts/admin/models/color-scheme.js.es6 index 8486002386..afabb1ed6e 100644 --- a/app/assets/javascripts/admin/models/color-scheme.js.es6 +++ b/app/assets/javascripts/admin/models/color-scheme.js.es6 @@ -4,7 +4,7 @@ import { ajax } from "discourse/lib/ajax"; import ColorSchemeColor from "admin/models/color-scheme-color"; import EmberObject from "@ember/object"; -const ColorScheme = EmberObject.extend(Ember.Copyable, { +const ColorScheme = EmberObject.extend({ init() { this._super(...arguments); diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index bd7074ced2..d0152c1a9e 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -21,6 +21,8 @@ const Report = EmberObject.extend({ average: false, percent: false, higher_is_better: true, + description_link: null, + description: null, @discourseComputed("type", "start_date", "end_date") reportUrl(type, start_date, end_date) { diff --git a/app/assets/javascripts/admin/templates/components/admin-report.hbs b/app/assets/javascripts/admin/templates/components/admin-report.hbs index f8a36fa7d3..cb0f333bb0 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report.hbs @@ -23,9 +23,15 @@ {{#if model.description}} - - {{d-icon "question-circle"}} - + {{#if model.description_link}} + + {{d-icon "question-circle"}} + + {{else}} + + {{d-icon "question-circle"}} + + {{/if}} {{/if}} {{/unless}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-start-backup.hbs b/app/assets/javascripts/admin/templates/modal/admin-start-backup.hbs index dd201e2aed..011de55de9 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-start-backup.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-start-backup.hbs @@ -1,12 +1,14 @@ {{#d-modal-body title="admin.backups.operations.backup.confirm"}} {{d-button - class="btn-primary" + class="btn-primary backup-with-uploads" action=(action "startBackupWithUploads") label="yes_value"}} {{d-button + class="backup-no-uploads" action=(action "startBackupWithoutUploads") label="admin.backups.operations.backup.without_uploads"}} {{d-button + class="btn-default" action=(action "cancel") label="no_value"}} {{/d-modal-body}} diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 24c71dd8fb..52f44e78f5 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -1014,7 +1014,7 @@ export default Component.extend({ ); // Short upload urls need resolution - resolveAllShortUrls(ajax); + resolveAllShortUrls(ajax, this.siteSettings, ".d-editor-preview-wrapper"); if (this._enableAdvancedEditorPreviewSync()) { this._syncScroll( diff --git a/app/assets/javascripts/discourse/components/cook-text.js.es6 b/app/assets/javascripts/discourse/components/cook-text.js.es6 index 256636a3c5..f3ad48f549 100644 --- a/app/assets/javascripts/discourse/components/cook-text.js.es6 +++ b/app/assets/javascripts/discourse/components/cook-text.js.es6 @@ -16,7 +16,7 @@ const CookText = Component.extend({ next(() => window .requireModule("pretty-text/upload-short-url") - .resolveAllShortUrls(ajax) + .resolveAllShortUrls(ajax, this.siteSettings) ); }); } diff --git a/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 b/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 index 5622cfab94..7789ccd2bc 100644 --- a/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 +++ b/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 @@ -68,21 +68,21 @@ export default Component.extend({ "topicTrackingState.incomingCount" ) message() { - var msg = null; + let msg = null; if ( this.publicTopicCount < this.requiredTopics && this.publicPostCount < this.requiredPosts ) { - msg = "too_few_topics_and_posts_notice"; + msg = "too_few_topics_and_posts_notice_MF"; } else if (this.publicTopicCount < this.requiredTopics) { - msg = "too_few_topics_notice"; + msg = "too_few_topics_notice_MF"; } else { - msg = "too_few_posts_notice"; + msg = "too_few_posts_notice_MF"; } return new Handlebars.SafeString( - I18n.t(msg, { + I18n.messageFormat(msg, { requiredTopics: this.requiredTopics, requiredPosts: this.requiredPosts, currentTopics: this.publicTopicCount, diff --git a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 index 855fed4e28..eb61067167 100644 --- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 +++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 @@ -219,6 +219,11 @@ export default Component.extend({ } }, + setCategory(category) { + this.set("searchedTerms.category", category); + this.set("category", category); + }, + setSearchedTermValueForCategory() { const match = this.filterBlocks(REGEXP_CATEGORY_PREFIX); if (match.length !== 0) { @@ -235,21 +240,21 @@ export default Component.extend({ (!existingInput && userInput) || (existingInput && userInput && existingInput.id !== userInput.id) ) - this.set("searchedTerms.category", userInput); + this.setCategory(userInput); } else if (isNaN(subcategories)) { const userInput = Category.findSingleBySlug(subcategories[0]); if ( (!existingInput && userInput) || (existingInput && userInput && existingInput.id !== userInput.id) ) - this.set("searchedTerms.category", userInput); + this.setCategory(userInput); } else { const userInput = Category.findById(subcategories[0]); if ( (!existingInput && userInput) || (existingInput && userInput && existingInput.id !== userInput.id) ) - this.set("searchedTerms.category", userInput); + this.setCategory(userInput); } } else this.set("searchedTerms.category", ""); }, diff --git a/app/assets/javascripts/discourse/components/topic-status.js.es6 b/app/assets/javascripts/discourse/components/topic-status.js.es6 index ecd6f448de..569292eb5e 100644 --- a/app/assets/javascripts/discourse/components/topic-status.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-status.js.es6 @@ -44,6 +44,17 @@ export default Component.extend({ : this._reset("warning"); }, + @discourseComputed( + "showPrivateMessageIcon", + "topic.isPrivateMessage", + "topic.is_warning" + ) + topicPrivateMessage(showPrivateMessageIcon, privateMessage, warning) { + return showPrivateMessageIcon && privateMessage && !warning + ? this._set("privateMessage", "envelope", "personal_message") + : this._reset("privateMessage"); + }, + @discourseComputed("topic.pinned") topicPinned(pinned) { return pinned diff --git a/app/assets/javascripts/discourse/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6 index e6a88ded97..71baa92c91 100644 --- a/app/assets/javascripts/discourse/helpers/application.js.es6 +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -41,6 +41,11 @@ registerUnbound("number", (orig, params) => { if (n.toString() !== title.toString() && addTitle) { result += " title='" + Handlebars.Utils.escapeExpression(title) + "'"; } + if (params.ariaLabel) { + const ariaLabel = Handlebars.Utils.escapeExpression(params.ariaLabel); + result += ` aria-label='${ariaLabel}'`; + } + result += ">" + n + ""; return new safe(result); diff --git a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 index c0099d7f30..fbfc4484de 100644 --- a/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 +++ b/app/assets/javascripts/discourse/initializers/register-service-worker.js.es6 @@ -10,9 +10,12 @@ export default { if (isSupported) { const caps = Discourse.__container__.lookup("capabilities:main"); - const isApple = caps.isSafari || caps.isIOS || caps.isIpadOS; + const isAppleBrowser = + caps.isSafari || + (caps.isIOS && + !window.matchMedia("(display-mode: standalone)").matches); - if (Discourse.ServiceWorkerURL && !isApple) { + if (Discourse.ServiceWorkerURL && !isAppleBrowser) { navigator.serviceWorker.getRegistrations().then(registrations => { for (let registration of registrations) { if ( diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index 3dc13d0d79..93cad9b4c4 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -25,6 +25,7 @@ const SERVER_SIDE_ONLY = [ /^\/posts\/\d+\/raw/, /^\/raw\/\d+/, /^\/wizard/, + /^\/go\//, // EXPERIMENTAL: https://meta.discourse.org/t/-/142605 /\.rss$/, /\.json$/, /^\/admin\/upgrade$/, diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index ce91d7c8dc..e44a60d53a 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -66,9 +66,12 @@ export function avatarImg(options, getURL) { const classes = "avatar" + (options.extraClasses ? " " + options.extraClasses : ""); - const title = options.title - ? " title='" + escapeExpression(options.title || "") + "'" - : ""; + + let title = ""; + if (options.title) { + const escaped = escapeExpression(options.title || ""); + title = ` title='${escaped}' aria-label='${escaped}'`; + } return ( "{{archivedIcon}} {{~/if~}} +{{~#if topicPrivateMessage~}} + {{privateMessageIcon}} +{{~/if~}} {{~#if topicWarning~}} - {{warningIcon}} + {{warningIcon}} {{~/if~}} {{~#if topicPinned~}} {{~#if canAct~}} diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index 1317bbc753..e903c5cd02 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -36,7 +36,7 @@ {{#if canBulkSelect}} {{d-button icon="list" class="btn-default bulk-select" title="topics.bulk.toggle" action=(action "toggleBulkSelect")}} - {{bulk-select-button selected=selected action=(action "search")}} + {{bulk-select-button selected=selected category=category action=(action "search")}} {{/if}} {{#if bulkSelectEnabled}} @@ -85,7 +85,8 @@ {{/if}} - {{topic-status topic=result.topic disableActions=true}}{{#highlight-text highlight=q}}{{{unbound result.topic.fancyTitle}}}{{/highlight-text}} + {{topic-status topic=result.topic disableActions=true showPrivateMessageIcon=true}} + {{#highlight-text highlight=q}}{{{unbound result.topic.fancyTitle}}}{{/highlight-text}}
@@ -210,6 +211,7 @@ {{search-advanced-options searchTerm=searchTerm isExpanded=true + category=category }} {{d-button diff --git a/app/assets/javascripts/discourse/templates/list/posts-count-column.hbr b/app/assets/javascripts/discourse/templates/list/posts-count-column.hbr index b37ddd9c1b..fb6642f0f0 100644 --- a/app/assets/javascripts/discourse/templates/list/posts-count-column.hbr +++ b/app/assets/javascripts/discourse/templates/list/posts-count-column.hbr @@ -1,6 +1,6 @@ <{{view.tagName}} class='num posts-map posts {{view.likesHeat}}' title='{{view.title}}'> {{raw-plugin-outlet name="topic-list-before-reply-count"}} - {{number topic.replyCount noTitle="true"}} + {{number topic.replyCount noTitle="true" ariaLabel=view.title}} diff --git a/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs index 5b0da4457e..fd8f57c54c 100644 --- a/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs +++ b/app/assets/javascripts/discourse/templates/preferences-second-factor.hbs @@ -27,7 +27,7 @@ {{/if}} {{#if loaded}} -
+

{{i18n "user.second_factor.totp.title"}}

{{d-button action=(action "createTotp") @@ -54,7 +54,7 @@
-
+

{{i18n "user.second_factor.security_key.title"}}

{{d-button action=(action "createSecurityKey") @@ -81,7 +81,7 @@
-
+

{{i18n "user.second_factor_backup.title"}}

{{#if model.second_factor_enabled}} diff --git a/app/assets/javascripts/discourse/widgets/connector.js.es6 b/app/assets/javascripts/discourse/widgets/connector.js.es6 index 9701f4c80a..99b8053881 100644 --- a/app/assets/javascripts/discourse/widgets/connector.js.es6 +++ b/app/assets/javascripts/discourse/widgets/connector.js.es6 @@ -1,5 +1,4 @@ import { next } from "@ember/runloop"; -import deprecated from "discourse-common/lib/deprecated"; import { setOwner, getOwner } from "@ember/application"; export default class Connector { @@ -16,44 +15,18 @@ export default class Connector { next(() => { const mounted = widget._findView(); - if (opts.templateName) { - deprecated( - `Using a 'templateName' for a connector is deprecated. Use 'component' instead [${opts.templateName}]` - ); - } - - const container = getOwner ? getOwner(mounted) : mounted.container; - - let view; - if (opts.component) { const connector = widget.register.lookupFactory( "component:connector-container" ); - view = connector.create({ + + const view = connector.create({ layoutName: `components/${opts.component}`, model: widget.findAncestorModel() }); - } - if (opts.templateName) { - let context; - if (opts.context === "model") { - const model = widget.findAncestorModel(); - context = model; - } + setOwner(view, getOwner(mounted)); - view = Ember.View.create({ - container: container || widget.register, - templateName: opts.templateName, - context - }); - } - - if (view) { - if (setOwner) { - setOwner(view, getOwner(mounted)); - } mounted._connected.push(view); view.renderer.appendTo(view, $elem[0]); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 index 6e6392d774..213aef39d9 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 @@ -145,9 +145,10 @@ function videoHTML(token, opts) { const src = token.attrGet("src"); const origSrc = token.attrGet("data-orig-src"); const preloadType = opts.secureMedia ? "none" : "metadata"; + const dataOrigSrcAttr = origSrc !== null ? `data-orig-src="${origSrc}"` : ""; return `
`; @@ -157,8 +158,9 @@ function audioHTML(token, opts) { const src = token.attrGet("src"); const origSrc = token.attrGet("data-orig-src"); const preloadType = opts.secureMedia ? "none" : "metadata"; + const dataOrigSrcAttr = origSrc !== null ? `data-orig-src="${origSrc}"` : ""; return ``; } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/upload-protocol.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/upload-protocol.js.es6 index 3e075b13ac..174e603212 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/upload-protocol.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/upload-protocol.js.es6 @@ -60,7 +60,16 @@ function rule(state) { break; case "a": if (mapped) { - token.attrs[srcIndex][1] = mapped.short_path; + // when secure media is enabled we want the full /secure-media-uploads/ + // url to take advantage of access control security + if ( + state.md.options.discourse.limitedSiteSettings.secureMedia && + mapped.url.indexOf("secure-media-uploads") > -1 + ) { + token.attrs[srcIndex][1] = mapped.url; + } else { + token.attrs[srcIndex][1] = mapped.short_path; + } } else { token.attrs[srcIndex][1] = state.md.options.discourse.getURL( "/404" diff --git a/app/assets/javascripts/pretty-text/upload-short-url.js.es6 b/app/assets/javascripts/pretty-text/upload-short-url.js.es6 index b82e524173..401d73dff0 100644 --- a/app/assets/javascripts/pretty-text/upload-short-url.js.es6 +++ b/app/assets/javascripts/pretty-text/upload-short-url.js.es6 @@ -9,6 +9,11 @@ export function lookupCachedUploadUrl(shortUrl) { const MISSING = "missing"; export function lookupUncachedUploadUrls(urls, ajax) { + urls = _.compact(urls); + if (urls.length === 0) { + return; + } + return ajax("/uploads/lookup-urls", { method: "POST", data: { short_urls: urls } @@ -39,10 +44,9 @@ export function resetCache() { _cache = {}; } -function retrieveCachedUrl($upload, dataAttribute, callback) { +function retrieveCachedUrl($upload, siteSettings, dataAttribute, callback) { const cachedUpload = lookupCachedUploadUrl($upload.data(dataAttribute)); - const url = - dataAttribute === "orig-href" ? cachedUpload.short_path : cachedUpload.url; + const url = getAttributeBasedUrl(dataAttribute, cachedUpload, siteSettings); if (url) { $upload.removeAttr(`data-${dataAttribute}`); @@ -52,12 +56,34 @@ function retrieveCachedUrl($upload, dataAttribute, callback) { } } -function _loadCachedShortUrls($uploads) { +function getAttributeBasedUrl(dataAttribute, cachedUpload, siteSettings) { + if (!cachedUpload.url) { + return; + } + + // non-attachments always use the full URL + if (dataAttribute !== "orig-href") { + return cachedUpload.url; + } + + // attachments should use the full /secure-media-uploads/ URL + // in this case for permission checks + if ( + siteSettings.secure_media && + cachedUpload.url.indexOf("secure-media-uploads") > -1 + ) { + return cachedUpload.url; + } + + return cachedUpload.short_path; +} + +function _loadCachedShortUrls($uploads, siteSettings) { $uploads.each((_idx, upload) => { const $upload = $(upload); switch (upload.tagName) { case "A": - retrieveCachedUrl($upload, "orig-href", url => { + retrieveCachedUrl($upload, siteSettings, "orig-href", url => { $upload.attr("href", url); // Replace "|attachment" with class='attachment' @@ -72,24 +98,32 @@ function _loadCachedShortUrls($uploads) { break; case "IMG": - retrieveCachedUrl($upload, "orig-src", url => { + retrieveCachedUrl($upload, siteSettings, "orig-src", url => { $upload.attr("src", url); }); break; - case "SOURCE": // video tag > source tag - retrieveCachedUrl($upload, "orig-src", url => { + case "SOURCE": // video/audio tag > source tag + retrieveCachedUrl($upload, siteSettings, "orig-src", url => { $upload.attr("src", url); if (url.startsWith(`//${window.location.host}`)) { let hostRegex = new RegExp("//" + window.location.host, "g"); url = url.replace(hostRegex, ""); } - $upload.attr("src", window.location.origin + url); + let fullUrl = window.location.origin + url; + $upload.attr("src", fullUrl); // this is necessary, otherwise because of the src change the - // video just doesn't bother loading! - $upload.parent()[0].load(); + // video/audio just doesn't bother loading! + let $parent = $upload.parent(); + $parent[0].load(); + + // set the url and text for the tag within the съветника за настройка ✨" emails_are_disabled: "Всички изходящи имейли са изцяло забранени от администратора. Няма да бъдат изпращани никакви имейл известия." bootstrap_mode_enabled: "За да помогнем със стартирането на вашия нов сайт по-лесно поставихме сайта в \"стартиращо режим\". Всички нови потребители ще получат ниво на доверие 1 и ще имат активирани дневни известия за активноста във форума. Този режим ще бъде автоматично спрян когато бройката регистрирани потребители стигне %{min_users} ." themes: @@ -256,8 +259,17 @@ bg: not_bookmarked: "добавете тази публикация в Отметки" remove: "Премахнете отметката" save: "Запази" + reminders: + later_today: "По-късно днес
{{date}}" + next_business_day: "Следващият работен ден
{{date}}" + tomorrow: "Утре
{{date}}" + next_week: "Следващата седмица
{{date}}" + next_month: "Следващият месец
{{date}}" drafts: + resume: "Продължи" remove: "Премахване" + new_topic: "Проект на нова тема" + topic_reply: "Проект на отговор" abandon: yes_value: "Да, напусни" no_value: "Не, запази" @@ -287,38 +299,89 @@ bg: edit: "Редактирай този банер >>" choose_topic: none_found: "Няма нови теми." + title: + search: "Търсене на тема" + choose_message: + none_found: "Не са намерени съобщения." + title: + search: "Търси съобщения" review: order_by: "Подреди по" + in_reply_to: "в отговор на" explain: + formula: "Формула" total: "Общо" + awaiting_approval: "Очаква одобрение" delete: "Изтрий" settings: save_changes: "Запази промените" title: "Настройки" + view_all: "Виж всички" + grouped_by_topic: "Групирани по тема" + title: "Преглед" topic: "Тема:" filtered_user: "Потребител" + show_all_topics: "покажи всички теми" user: + bio: "Биография" + website: "Уебсайт" username: "Потребителско име" email: "Имейл" name: "Име" + fields: "Полета" + user_percentage: + agreed: + one: "{{count}}% са съгласни" + other: "{{count}}% са съгласни" + disagreed: + one: "{{count}}% не са съгласни" + other: "{{count}}% не са съгласни" + ignored: + one: "{{count}}% игнорира" + other: "{{count}}% са без отношение" topics: topic: "Тема" + reviewable_count: "Брой" + details: "детайли" + unique_users: + one: "%{count} потребител" + other: "{{count}} потребители" + replies: + one: "%{count} отговор" + other: "{{count}} отговора" edit: "Редактирай" save: "Запази" cancel: "Отмени" filters: + all_categories: "(всички категории)" type: title: "Тип" + all: "(всички видове)" refresh: "Опресни" + status: "Статус" category: "Категория" + orders: + priority: "Приоритет" + priority_asc: "Приоритет (по обратен ред)" + created_at: "Създадено на" + created_at_asc: "Създадено на (по обратен ред)" + priority: + medium: "Среден" + high: "Висок" scores: + score: "Точки" date: "Дата" type: "Тип" + status: "Статус" statuses: pending: title: "Чакащи" + approved: + title: "Одобрена" rejected: title: "Отхвърлени" + deleted: + title: "Изтрита" types: reviewable_user: title: "Потребител" @@ -434,6 +497,11 @@ bg: members: title: "Членове" filter_placeholder: "Потребителско име" + remove_member: "Премахни Потребител" + remove_member_description: "Премахни %{username} от тази група" + make_owner: "Направи Собственик" + remove_owner: "Премахни като Собственик" + remove_owner_description: "Премахни %{username} като собственик на тази група" owner: "Собественик" topics: "Теми" posts: "Публикации" @@ -519,6 +587,7 @@ bg: topics_entered: "добавени теми" post_count: "# публикации" confirm_delete_other_accounts: "Сигурни ли сте, че искате да изтриете тези профили?" + copied: "копирано" user_fields: none: "(изберете опция)" user: @@ -1679,6 +1748,7 @@ bg: search_priority: options: normal: "Нормален" + high: "Висок" sort_options: likes: "Харесвания" views: "Преглеждания" diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index dec41a729a..99061a6922 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -174,6 +174,8 @@ bs_BA: banner: enabled: "postavi kao zastavicu %{when}. Pojavljivat će se na vrhu svake stranice sve dok je korisnik ne zatvori." disabled: "odkloni zastavicu %{when}.Neće se više prikazivati na vrhu svake stranice." + forwarded: "gornji email proslijeđen" + topic_admin_menu: "akcije u vezi teme" wizard_required: "Dobrodošli na vaš novi Discourse! Odpočnimo sa čarobnjakom za postavke ✨" emails_are_disabled: "Sve odlazeće email poruke su globalno onemogućene od strane administratora. Ni jedna notifikacija bilo koje vrste neće biti poslana." bootstrap_mode_enabled: "Kako bi olakšali lansiranje vaše nove web stranice, trenutačno ste u \"bootstrap\" načinu rada. Svim novim korisnicima će se dodjeljivati nivo povjerenja 1 i imati uključen dnevni izvještaj dešavanja na forumu preko email-a. Ovo će se automatski isključiti nakon što %{min_users} korisnika prijavi račun na forumu." @@ -285,9 +287,19 @@ bs_BA: bookmarks: created: "zabilježili ste ovu stranicu" not_bookmarked: "sačuvaj ovaj post" + created_with_reminder: "postavili ste zabilješku na ovu objavu s podsjetnikom %{date}" remove: "Ukloni zabilješku" confirm_clear: "Dali ste sigurni dali želite izbrisati sve sačuvane stvari iz ove teme?" save: "Sačuvaj" + no_timezone: 'Još uvijek niste postavili vremensku zonu, tako da nećete biti u mogućnosti koristiti podsjetnike. Postavite vremensku zonu na vašem profilu.' + reminders: + at_desktop: "Sljedeći put sam za svojim desktop kompjuterom" + later_today: "Danas, malo kasnije
{{date}}" + next_business_day: "Sljedeći radni dan
{{date}}" + tomorrow: "Sutradan
{{date}}" + next_week: "Sljedeće sedmice
{{date}}" + next_month: "Sljedeći mjesec
{{date}}" + custom: "Precizno vrijeme i datum" drafts: resume: "Nastavi" remove: "Ukloni" @@ -332,13 +344,41 @@ bs_BA: banner: close: "Odkaži ovu zastavicu." edit: "Uredite ovu zastavicu >>" + pwa: + install_banner: "Da li želite instalirati %{title} na ovaj uređaj?" choose_topic: none_found: "Nema tema." + title: + search: "Traži Temu" + placeholder: "ovdje ukucajte naslov teme, url ili id" choose_message: none_found: "nisam našao nijednu poruku" + title: + search: "Traži poruku" + placeholder: "ovdje ukucajte naslov poruke, url ili id" review: order_by: "Pordeak po" in_reply_to: "odgovori na" + explain: + why: "objasnite zašto je ovaj objekat završio na listi za čekanje" + title: "Ocjenjiv Skor" + formula: "Formula" + subtotal: "Podsuma" + total: "Suma" + min_score_visibility: "Minimalni skor za Vidljivost" + score_to_hide: "Skor za Skrivanje objave" + take_action_bonus: + name: "preduzeo akciju" + title: "Kada član uprave odluči da poduzme akciju, zastavici je dodijeljen bonus." + user_accuracy_bonus: + name: "korisnička preciznost" + title: "Korisnicima čije su zastave prethodno bivale usaglašene dobivaju bonus." + trust_level_bonus: + name: "nivo povjerenja" + title: "Ocjenjivi objekti kreirani od strane korisnika sa višim nivoom povjerenja imaju viši skor." + type_bonus: + name: "bonus tip" + title: "Neki ocjenjivi tipovi mogu zaprimiti bonus od strane osoblja foruma kako bi tim tipovima podigli prioritet" claim_help: optional: "Možete tražiti ova da sprijećite ostale da ga pregledaju" required: "VI morate tvrditi stavari prije ih morate cijeniti" @@ -373,6 +413,8 @@ bs_BA: deleted_post: "(post izbrisan)" deleted_user: "(korisnik izbrisan)" user: + bio: "Biografija" + website: "Web stranica" username: "Ime" email: "Email" name: "Ime" @@ -572,6 +614,8 @@ bs_BA: leave: "Napusti" request: "Zatraži" message: "Poruka" + confirm_leave: "Jeste li sigurni da želite napustiti ovu grupu?" + allow_membership_requests: "Dopusti korisnicima da smiju slati molbe za učlanjenje u grupu vlasnicima grupa (Potrebno je da je grupa javno vidljiva)" membership_request_template: "Prilagođeni memorandum kao priložak koji se prikazuje korisnicima prilikom slanja zahtjeva za učlanjenje" membership_request: submit: "Poslati zahtjev " @@ -619,6 +663,7 @@ bs_BA: remove_owner: "Ukloni kao vlasnika" remove_owner_description: "Ukloni %{username} kao vlasnika ove grupe" owner: "Vlasnik" + forbidden: "Nije vam dozvoljeno da vidite članove." topics: "Teme" posts: "Postovi" mentions: "Spomenuto" @@ -753,14 +798,19 @@ bs_BA: activity_stream: "Aktivnost" preferences: "Postavke" feature_topic_on_profile: + open_search: "Odaberi Novu temu" + title: "Odaberi temu" + search_label: "Traži temu po naslovu" save: "Sačuvaj" clear: title: "Clear" + warning: "Jeste li sigurni da želite vašu istaknutu temu očistiti?" profile_hidden: "Javni profil ovog korisnika je skriven" expand_profile: "Proširi" collapse_profile: "Spusti" bookmarks: "Zabilješke" bio: "O Meni" + timezone: "Vremenska zona" invited_by: "Pozvan od" trust_level: "Nivo povjerenja" notifications: "Obaviještenja" @@ -787,6 +837,7 @@ bs_BA: enable_quoting: "Uključi \"citiran odgovor\" za označen tekst" enable_defer: "Omogući odlaganje za označavanje tema nepročitanih" change: "promjeni" + featured_topic: "Istaknuta tema" moderator: "{{user}} je moderator" admin: "{{user}} je admin" moderator_tooltip: "Ovaj korisnik je moderator" @@ -894,6 +945,7 @@ bs_BA: copied_to_clipboard: "Kopirano u međuspremnik" copy_to_clipboard_error: "Pogreška pri kopiranju podataka u međuspremnik" remaining_codes: "Imate preostale rezervne kodove {{count}} ." + use: "Koristi backup kod" enable_prerequisites: "Morate omogućiti primarni drugi faktor prije generiranja rezervnih kodova." codes: title: "Generirani sigurnosni kodovi" @@ -901,6 +953,7 @@ bs_BA: second_factor: title: "Two Factor Authentication" enable: "Upravljanje autentifikacijom sa dva faktora" + forgot_password: "Zaboravili ste password?" confirm_password_description: "Molimo vas da potvrdite šifru kako bi nastavili" name: "Ime" label: "Šifra" @@ -914,6 +967,7 @@ bs_BA: extended_description: | Dvofaktorna autentifikacija dodaje dodatnu sigurnost vašem računu tako što zahtijeva dodatnu jednokratnu oznaku pored vaše lozinke. Tokeni se mogu generirati na Android i iOS uređajima. oauth_enabled_warning: "Imajte na umu da će ulogovanje korištenjem socijalnih mreža biti isključeno u momentu kad uključite two factor authentication (dvofaktorsku ovjeru autentičnosti) na vašem korisničkom računu." + use: "Koristi Authenticator app?" enforced_notice: "Morate omogućiti autentifikaciju s dva faktora prije pristupa ovoj web-lokaciji." disable: "onemogući" disable_title: "Onemogući Drugi Faktor" @@ -921,11 +975,20 @@ bs_BA: edit: "Izmijeni" edit_title: "Uredi drugi faktor" edit_description: "Ime drugog faktora" + enable_security_key_description: "Kada ste pripremili vaš fizikalni sigurnosni ključ (physical security key), pritisnite ispod dugme Registriraj." totp: title: "Autentikatori zasnovani na tokenu" add: "Novi Authenticator" default_name: "Moj Authenticator" security_key: + register: "Registriraj" + title: "Sigurnosni ključevi" + add: "Registriraj sigurnosni ključ" + default_name: "Glavni sigurnosni ključ" + not_allowed_error: "Proces registracije sigurnosnog ključa je ili vremenski istekao ili je odkazan." + already_added_error: "Već ste registrovali ovaj sigurnosni ključ. Nemate potrebe da ga ponovo registrujete." + edit: "Izmijeni sigurnosni ključ" + edit_description: "Ime sigurnosnog ključa" delete: "Izbriši" change_about: title: "Promjeni O meni" @@ -952,9 +1015,15 @@ bs_BA: uploaded_avatar_empty: "Dodajte vašu sliku" upload_title: "Učitajte vašu sliku sa uređaja" image_is_not_a_square: "Upozorenje: morali smo izrezat vašu sliku; nije bila kvadratnog oblika." + change_profile_background: + title: "Profil zaglavlja" + instructions: "Profili zaglavlja će biti centrirani i imati standardno širinu od 1110 piksela." change_card_background: title: "Pozadina Korisničke kartice" instructions: "Pozadinske slike će biti centrirane i imati standard širinu od 590 pixela." + change_featured_topic: + title: "Istaknuta tema" + instructions: "Link na ovu temu će biti prikazan na vašoj korisničkoj kartici i profilu." email: title: "Email" primary: "Primarni Email" @@ -979,6 +1048,7 @@ bs_BA: confirm_modal_title: "Povezani %{provider} račun" confirm_description: account_specific: "Vaš %{provider} račun %{account_description} će biti korišten za autentifikaciju." + generic: "Vaš %{provider} račun će biti korišten za prijavu." name: title: "Ime" instructions: "vaše puno ime (opciono)" @@ -1090,6 +1160,7 @@ bs_BA: search: "kucaj da potražiš pozivnice..." title: "Pozivnice" user: "Pozvan korisnik" + sent: "Zadnje poslano" none: "Nema pozivnica za prikazati" truncated: one: "Prikaz prve pozivnice." @@ -1307,6 +1378,8 @@ bs_BA: reset: "Resetuj Šifru" complete_username: "Ako se vaš nalog podudara sa korisnikom %{username}, uskoro ćete primiti email koji će vam objasniti kako da resetujete vašu šifru." complete_email: "Ako se vaš nalog podudara sa %{email}, uskoro ćete primiti email koji će vam objasniti kako da resetujete vašu šifru." + complete_username_found: "Pronašli smo račun koji se podudara sa korisničkim imenom %{username}. Trebali bi uskoro dobiti email sa instrukcijama kako da resetujete vaš password." + complete_email_found: "Pronašli smo račun koji se podudara sa %{email}. Trebali bi uskoro dobiti email sa instrukcijama kako da resetujete vaš password." complete_username_not_found: "Nema naloga sa korisničkim imenom %{username}" complete_email_not_found: "Nema naloga sa email-om %{email}" help: "Email još ne stiže? Prvo provjerite vaš spam folder u email pregledniku.

Niste sigurni koji email ste koristili? Unesite email adresu i mi ćemo vas obavjestiti da li ista postoji kod nas.

Ukoliko nemate više pristup vašoj email kojom ste registrovali korisnički račun, molimo vas da se obratite našim administratorima za pomoć.

" @@ -1330,8 +1403,15 @@ bs_BA: password: "Šifra" second_factor_title: "Two Factor Authentication" second_factor_description: "Molimo da unesete kod za ovjeru autentičnosti sa vaše aplikacije:" + second_factor_backup: "Loguj se koristeći backup kod" second_factor_backup_title: "rezevna zaštita za dva faktora" second_factor_backup_description: "Molim ukucajte jedan od vaši rezevni kodova" + second_factor: "Loguj se koristeći Authenticator app" + security_key_description: "Kada ste pripremili vaš fizikalni sigurnosni ključ (physical security key), pritisnite ispod dugme Prijava pomoću sigurnosnog ključa." + security_key_alternative: "Pokušaj na drugi način" + security_key_authenticate: "Prijava pomoću sigurnosnog ključa" + security_key_not_allowed_error: "Ovaj proces prijave pomoću sigurnosnog ključa je ili vremenski istekao ili je odkazan." + security_key_no_matching_credential_error: "Nisu pronađeni korisnički podatci koristeći navedeni sigurnosni ključ." email_placeholder: "email ili korisnik" caps_lock_warning: "Uključena su vam velika slova" error: "Nepoznata greška" @@ -1426,6 +1506,7 @@ bs_BA: one: "Označi bar {{count}} predmet." few: "Označi bar {{count}} predmeta." other: "Označi bar {{count}} predmeta." + invalid_selection_length: "Označeno mora biti najmanje {{count}} karaktera dugo." date_time_picker: from: Od to: Ka @@ -1486,10 +1567,12 @@ bs_BA: title_missing: "Naslov je obavezan" title_too_short: "Naslov mora biti najmanje {{min}} karaktera" title_too_long: "Naslov ne može biti više od {{max}} karaktera" + post_missing: "Objava ne može biti prazna" post_length: "Odgovor mora biti najmanje {{min}} karaktera" try_like: "Dail ste pokušali{{heart}}dugme?" category_missing: "Morate odabrati kategoriju" tags_missing: "Morate odabrati najmanje {{count}} oznaka" + topic_template_not_modified: "Molimo da dodate detalje i specifikacije za vašu temu tako što ćete izmijeniti template teme." save_edit: "Sačuvaj izmjene" overwrite_edit: "Overwrite Edit" reply_original: "Odgovori na Originalnu temu" @@ -1514,6 +1597,7 @@ bs_BA: view_new_post: "Pogledaj svoj novi post." saving: "Spašavam" saved: "Sačuvano!" + saved_draft: "Sastav je u toku. Tipkajte kako bi nastavili dalje." uploading: "Uplodujem..." show_preview: "pokaži pregled »" hide_preview: "« sakri pregled" @@ -2247,7 +2331,7 @@ bs_BA: attachment_download_requires_login: "Sorry, you need to be logged in to download attachments." abandon_edit: no_value: "Ne, sačuvaj" - no_save_draft: "Ne,spasi skicu" + no_save_draft: "Ne, spasi sastav" abandon: confirm: "Da li ste sigurni da želite otkazati vaš post?" no_value: "Ne, sačuvaj" @@ -2574,6 +2658,7 @@ bs_BA: help: "Ovu temu sajt ne lista među najnovijim temama. Neće biti prisutna ni među listama tema unutar kategorija. Jedini način da se dođe do ove teme je direktan link" personal_message: title: "Ova tema je lična poruka" + help: "Ova tema je lična poruka" posts: "Odgovori" posts_long: "postoji {{number}} odgovora u ovoj temi" posts_likes_MF: | @@ -2729,6 +2814,7 @@ bs_BA: title: "Aplikacija" create: "%{shortcut} Započni novu temu" notifications: "%{shortcut} Otvori notifikacije" + hamburger_menu: "%{shortcut} Otvori padajući meni" user_profile_menu: "%{shortcut} Otvori meni korisnika" show_incoming_updated_topics: "%{shortcut} Pročitaj promjenje teme" search: "%{shortcut} Traži" @@ -2761,6 +2847,7 @@ bs_BA: mark_watching: "%{shortcut} Motri temu" print: "%{shortcut} Odštampaj temu" defer: "%{shortcut} Defer temu" + topic_admin_actions: "%{shortcut} Otvori opcije administriranja teme" badges: earned_n_times: one: "Zaradio / la sam ovu oznaku %{count} time" @@ -2805,13 +2892,39 @@ bs_BA: changed: "oznake promijenjene:" tags: "Oznake" choose_for_topic: "izborne oznake" + info: "Informacija" + default_info: "Ovaj oznaka nije predodređena ni za jednu kategoriju i nema sinonime." + category_restricted: "Ova oznaka je predodređena za kategorije na koje nemate pravo pristupa." + synonyms: "Sinonimi" + synonyms_description: "U slučaju da se sljedeće oznake koriste, iste će biti zamijenjene sa %{base_tag_name}." + tag_groups_info: + one: 'Ovaj tag pripada grupi "{{tag_groups}}".' + few: "Ovaj tag pripada sljedećim grupama: {{tag_groups}}." + other: "Ova oznaka pripada sljedećim grupama: {{tag_groups}}." + category_restrictions: + one: "Moguće je koristiti samo u sljedećoj kategoriji:" + few: "Moguće je koristiti samo u sljedećim kategorijama:" + other: "Moguće je koristiti samo u sljedećim kategorijama:" + edit_synonyms: "Uredi sinonime" + add_synonyms_label: "Dodaj sinonime:" add_synonyms: "Dodaj" + add_synonyms_explanation: + one: "Svaka lokacija koja trenutno koristi ovu oznaku bit će zamjenjena sa %{tag_name}. Jeste li sigurni da želite izvršiti tu promjenu?" + few: "Svaka lokacija koja trenutno koristi ove oznake bit će zamjenjena sa %{tag_name}. Jeste li sigurni da želite izvršiti tu promjenu?" + other: "Svaka lokacija koja trenutno koristi ove oznake bit će zamjenjena sa %{tag_name}. Jeste li sigurni da želite izvršiti tu promjenu?" + add_synonyms_failed: "Sljedeće oznake nije moguće dodati kao sinonime: %{tag_names}. Osigurajte prvo da nemaju već postojeće sinonime i da nisu sinonimi neke druge oznake." + remove_synonym: "Odstrani sinonim" + delete_synonym_confirm: 'Jeste li sigurni da želite izbrisati sinonim "%{tag_name}"?' delete_tag: "Izbriši oznaku" delete_confirm: one: "Jeste li sigurni da želite izbrisati ovu oznaku i ukloniti je iz teme %{count} kojoj je dodijeljen?" few: "Jeste li sigurni da želite izbrisati ovu oznaku i ukloniti je iz tema {{count}} kojima je dodijeljena?" other: "Jeste li sigurni da želite izbrisati ovu oznaku i ukloniti je iz tema {{count}} kojima je dodijeljena?" delete_confirm_no_topics: "Jeste li sigurni da želite izbrisati ovu oznaku?" + delete_confirm_synonyms: + one: "Pripadajući sinonim će također biti obrisan." + few: "Pripadajućih {{count}} sinonima će također biti obrisano." + other: "Pripadajućih {{count}} sinonima će također biti obrisano." rename_tag: "Promjeni ime Oznake" rename_instructions: "Odaberite novo ime za oznaku:" sort_by: "Sortiraj po:" @@ -2854,6 +2967,7 @@ bs_BA: description: "You will be notified only if someone mentions your @name or replies to your post." muted: title: "Utišan" + description: "Nećete bit obavješteni bilo čime o ovoj temi sa ovom oznakom, i neće se pojavljivati na tabularu nepročitanih tema." groups: title: "Označite grupe" about: "Dodajte oznake grupama da biste ih lakše upravljali." @@ -2865,6 +2979,7 @@ bs_BA: parent_tag_description: "Oznake iz ove grupe ne mogu se koristiti ako roditeljska oznaka nije prisutna." one_per_topic_label: "Ograničite jednu oznaku po temi iz ove grupe" new_name: "Nova Grupa Oznaka" + name_placeholder: "Ime grupe oznaka" save: "Sačuvaj" delete: "Delete" confirm_delete: "Jeste li sigurni da želite izbrisati ovu grupu oznaka?" @@ -4123,11 +4238,14 @@ bs_BA: filter: "Pretraga (URL ili vanjski URL)" reseed: modal: + title: "Zamijeni tekst" categories: "Kategorije" topics: "Teme" + replace: "Zamijeni" wizard_js: wizard: done: "Urađeno" + finish: "Završi" back: "Prethodno" next: "Iduće" step: "%{current} od %{total}" diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index 0f69a6d177..74b0aab013 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -1230,9 +1230,6 @@ ca: enabled: "El lloc web és en mode només de lectura. Continueu navegant, però de moment estan desactivades les accions de respondre, 'm'agrada' i altres." login_disabled: "S'ha desactivat l'inici de sessió mentre aquest lloc web es trobi en mode només de lectura." logout_disabled: "S'ha desactivat el tancament de sessió mentre aquest lloc web es trobi en mode només de lectura." - too_few_topics_and_posts_notice: "Comencem la discussió! Hi ha %{currentTopics} temes i %{currentPosts} publicacions. Els visitants en necessiten més per a llegir i respondre. Recomanem almenys %{requiredTopics} temes i %{requiredPosts} publicacions. Sols l'equip responsable pot veure aquest missatge." - too_few_topics_notice: "Comencem la discussió! Hi ha %{currentTopics} / temes. Els visitants en necessiten més per a llegir i respondre. Recomanem almenys %{requiredTopics} temes. Sols l'equip responsable pot veure aquest missatge." - too_few_posts_notice: "Comencem la discussió! Hi ha %{currentPosts} publicacions. Els visitants en necessiten més per a llegir i respondre. Recomanem almenys %{requiredPosts} publicacions. Sols l'equip responsable pot veure aquest missatge." logs_error_rate_notice: reached_hour_MF: "{relativeAge} - {rate, plural, one {# error/hora} other {# errors/hora} s'ha arribat al límit de configuració del lloc web de {limit, plural, one {# error/hora} other {# errors/hora}}." reached_minute_MF: "{relativeAge} - {rate, plural, one {# error/minut} other {# errors/minut}} s'ha arribat al límit de configuració del lloc web de {limit, plural, one {# error/minut} other {# errors/minut}}." @@ -2562,6 +2559,7 @@ ca: help: "Aquest tema no és visible; no es mostrarà en la llista de temes i només és accessible amb un enllaç directe." personal_message: title: "Aquest tema és un missatge personal" + help: "Aquest tema és un missatge personal" posts: "Publicacions" posts_long: "hi ha {{number}} publicacions a aquest tema" posts_likes_MF: | diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 8af7ff9135..87ac724689 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -2539,6 +2539,7 @@ da: help: "Dette emne er ulistet; det vil ikke blive vist i listen over emner og kan kun tilgås med et direkte link" personal_message: title: "Dette emne er en privat besked" + help: "Dette emne er en privat besked" posts: "Indlæg" posts_long: "{{number}} indlæg i dette emne" posts_likes_MF: | diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 66f021b825..d39417f661 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -153,6 +153,7 @@ de: banner: enabled: "hat dieses Banner erstellt, %{when}. Es wird oberhalb jeder Seite angezeigt, bis es vom Benutzer weggeklickt wird." disabled: "hat dieses Banner entfernt, %{when}. Es wird nicht mehr oberhalb jeder Seite angezeigt." + forwarded: "hat die obige E-Mail weitergeleitet" topic_admin_menu: "Themen Aktionen" wizard_required: "Willkommen bei deinem neuen Discourse! Lass uns mit dem Setup-Assistenten ✨ starten" emails_are_disabled: "Die ausgehende E-Mail-Kommunikation wurde von einem Administrator global deaktiviert. Es werden keinerlei Benachrichtigungen per E-Mail verschickt." @@ -579,6 +580,7 @@ de: request: "Anfrage" message: "Nachricht" confirm_leave: "Willst du die Gruppe wirklich verlassen?" + allow_membership_requests: "Erlaube Benutzern, Mitgliedschaftsanfragen an Gruppenbesitzer zu senden (erfordert, öffentlich sichtbare Gruppen)" membership_request_template: "Benutzerdefinierte Vorlage, das Benutzern angezeigt wird, die eine Mitgliedschaftsanfrage senden" membership_request: submit: "Anfrage abschicken" @@ -1259,9 +1261,8 @@ de: enabled: "Diese Website befindet sich im Nur-Lesen-Modus. Du kannst weiterhin Inhalte lesen, aber das Erstellen von Beiträgen, Vergeben von Likes und Durchführen einiger weiterer Aktionen ist derzeit nicht möglich." login_disabled: "Die Anmeldung ist deaktiviert während sich die Website im Nur-Lesen-Modus befindet." logout_disabled: "Die Abmeldung ist deaktiviert während sich die Website im Nur-Lesen-Modus befindet." - too_few_topics_and_posts_notice: "Lass die Diskussion beginnen! Es gibt %{currentTopics} Themen und %{currentPosts} Beiträge. Besucher brauchen mehr zum Lesen und Beantworten – wir empfehlen mindestens %{requiredTopics} Themen und %{requiredPosts} Beiträge. Diese Nachricht ist nur für das Team sichtbar." - too_few_topics_notice: "Lass die Diskussion beginnen! Es gibt %{currentTopics} Themen. Besucher brauchen mehr zum Lesen und Beantworten – wir empfehlen mindestens %{requiredTopics} Themen. Diese Nachricht ist nur für das Team sichtbar." - too_few_posts_notice: "Lass die Diskussion beginnen! Es gibt %{currentPosts} Beiträge. Besucher brauchen mehr zum Lesen und Beantworten – wir empfehlen mindestens %{requiredPosts} Beiträge. Diese Nachricht ist nur für das Team sichtbar." + too_few_topics_and_posts_notice_MF: >- + Lass' die Diskussion beginnen! Es {currentTopics, plural, one {ist # Thema} other {sind # Themen}} und {currentPosts, plural, one {# Beitrag} other {# Beiträge}} vorhanden. Besucher brauchen mehr zum Lesen und Beantworten – wir empfehlen mindestens {requiredTopics, plural, one {# Thema} other {# Themen}} und {requiredPosts, plural, one {# Beitrag} other {# Beiträge}}. Dieser Hinweis wird nur Teammitgliedern gezeigt. logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {# Fehler/Stunde} other {# errors/hour}} hat die Grenze der Webseiten-Einstellung von {limit, plural, one {# Fehler/Stunde} other {# Fehler/Stunde}} erreicht." reached_minute_MF: "{relativeAge}{rate, plural, one {# Fehler/Minute} other {# Fehler/Minute}} hat die Grenze der Webseiten-Einstellung von {limit, plural, one {# Fehler/Minute} other {# Fehler/Minute}} erreicht." @@ -1332,6 +1333,8 @@ de: reset: "Passwort zurücksetzen" complete_username: "Wenn ein Benutzerkonto dem Benutzernamen %{username} entspricht, solltest du in Kürze eine E-Mail mit Anweisungen zum Zurücksetzen deines Passwortes erhalten." complete_email: "Wenn ein Benutzerkonto der E-Mail %{email} entspricht, solltest du in Kürze eine E-Mail mit Anweisungen zum Zurücksetzen deines Passwortes erhalten." + complete_username_found: "Wir haben ein zum Benutzernamen %{username} gehörendes Konto gefunden. Du solltest in Kürze eine E-Mail mit Anweisungen zum Zurücksetzen deines Passwortes erhalten. " + complete_email_found: "Wir haben ein zu %{email} gehörendes Benutzerkonto gefunden. Du solltest in Kürze eine E-Mail mit Anweisungen zum Zurücksetzen deines Passwortes erhalten. " complete_username_not_found: "Es gibt kein Konto mit dem Benutzernamen %{username}" complete_email_not_found: "Es gibt kein Benutzerkonto für %{email}" help: "E-Mail nicht angekommen? Bitte prüfe zuerst deinen Spam-Ordner.

Nicht sicher, welche E-Mail-Adresse du verwendet hast? Gib eine E-Mail-Adresse ein und wir werden dir sagen, ob sie hier existiert.

Falls du keinen Zugriff mehr auf die hinterlegte E-Mail-Adresse deines Kontos hast, kontaktiere bitte unser hilfsbereites Team.

" @@ -1464,6 +1467,7 @@ de: min_content_not_reached: one: "Wähle mindestens einen Eintrag aus." other: "Wähle mindestens {{count}} Einträge aus." + invalid_selection_length: "Es müssen wenigstens{{count}} Zeichen markiert sein." date_time_picker: from: Von to: An @@ -1553,6 +1557,7 @@ de: view_new_post: "Sieh deinen neuen Beitrag an." saving: "Wird gespeichert" saved: "Gespeichert!" + saved_draft: "Beitrags-Entwurf vorhanden. Tippen, um fortzusetzen." uploading: "Wird hochgeladen…" show_preview: "Vorschau anzeigen »" hide_preview: "« Vorschau ausblenden" @@ -1602,6 +1607,7 @@ de: reply_as_new_topic: label: Antworte als verknüpftes Thema desc: "Erstelle ein neues Thema, das auf dieses Thema verweist" + confirm: "Du hast einen neuen Themen-Entwurf gespeichert. Wenn du ein verlinktes Thema erstellst, wird er überschrieben." reply_as_private_message: label: Neue Nachricht desc: Erstelle eine neue Nachricht @@ -2414,6 +2420,10 @@ de: name: "Name" name_placeholder: "Gib dem Lesezeichen einen Namen, um dein Gedächtnis zu unterstützen" set_reminder: "Erstelle eine Erinnerung" + actions: + delete_bookmark: + name: "Lesezeichen löschen" + description: "Entfernt das Lesezeichen von deinem Profil und beendet alle Erinnerungen für dieses Lesezeichen" category: can: "kann… " none: "(keine Kategorie)" @@ -2615,6 +2625,7 @@ de: help: "Dieses Thema ist unsichtbar. Es wird in keiner Themenliste angezeigt und kann nur mit einem direkten Link betrachtet werden." personal_message: title: "Dieses Thema ist eine persönliche Nachricht" + help: "Dieses Thema ist eine persönliche Nachricht" posts: "Beiträge" posts_long: "dieses Thema enthält {{number}} Beiträge" posts_likes_MF: | @@ -2834,6 +2845,7 @@ de: choose_for_topic: "optionale Schlagwörter" info: "Info" default_info: "Dieses Schlagwort ist nicht auf Kategorien beschränkt und hat keine Synonyme." + category_restricted: "Dieser Tag ist auf Kategorien begrenzt, für die du keine Zugriffsberechtigung hast." synonyms: "Synonyme" synonyms_description: "Wenn die folgenden Schlagwörter verwendet werden, werden sie durch %{base_tag_name} ersetzt." tag_groups_info: @@ -4202,7 +4214,8 @@ de: description: Das gleiche Abzeichen vielen Benutzern auf einmal verleihen. no_badge_selected: "Bitte wähle ein Abzeichen, um anzufangen." perform: "Verleihe Abzeichen an Benutzer" - success: Deine CSV Datei wurde empfangen und die Benutzer werden ihr Abzeichen in Kürzle bekommen. + upload_csv: Lade eine CSV-Datei mit entweder den E-Mail Adressen oder den Benutzernamen hoch + success: Deine CSV Datei wurde empfangen und die Benutzer werden ihr Abzeichen in Kürze bekommen. replace_owners: Entferne das Abzeichen von vorherigen Eigentümern emoji: title: "Emoji" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 1211978b66..127c964d04 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1372,9 +1372,22 @@ en: enabled: "This site is in read only mode. Please continue to browse, but replying, likes, and other actions are disabled for now." login_disabled: "Login is disabled while the site is in read only mode." logout_disabled: "Logout is disabled while the site is in read only mode." - too_few_topics_and_posts_notice: "Let's start the discussion! There are %{currentTopics} topics and %{currentPosts} posts. Visitors need more to read and reply to – we recommend at least %{requiredTopics} topics and %{requiredPosts} posts. Only staff can see this message." - too_few_topics_notice: "Let's start the discussion! There are %{currentTopics} topics. Visitors need more to read and reply to – we recommend at least %{requiredTopics} topics. Only staff can see this message." - too_few_posts_notice: "Let's start the discussion! There are %{currentPosts} posts. Visitors need more to read and reply to – we recommend at least %{requiredPosts} posts. Only staff can see this message." + too_few_topics_and_posts_notice_MF: >- + Let's start the discussion! + There {currentTopics, plural, one {is # topic} other {are # topics}} and + {currentPosts, plural, one {# post} other {# posts}}. Visitors need more to + read and reply to – we recommend at least {requiredTopics, plural, one {# topic} other {# topics}} + and {requiredPosts, plural, one {# post} other {# posts}}. Only staff can see this message. + too_few_topics_notice_MF: >- + Let's start the discussion! + There {currentTopics, plural, one {is # topic} other {are # topics}}. Visitors need more to + read and reply to – we recommend at least {requiredTopics, plural, one {# topic} other {# topics}}. + Only staff can see this message. + too_few_posts_notice_MF: >- + Let's start the discussion! + There {currentPosts, plural, one {is # post} other {are # posts}}. Visitors need more to + read and reply to – we recommend at least {requiredPosts, plural, one {# post} other {# posts}}. + Only staff can see this message. logs_error_rate_notice: # keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details reached_hour_MF: "{relativeAge}{rate, plural, one {# error/hour} other {# errors/hour}} reached site setting limit of {limit, plural, one {# error/hour} other {# errors/hour}}." @@ -2861,6 +2874,7 @@ en: help: "This topic is unlisted; it will not be displayed in topic lists, and can only be accessed via a direct link" personal_message: title: "This topic is a personal message" + help: "This topic is a personal message" posts: "Posts" posts_long: "there are {{number}} posts in this topic" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 78d165658c..f103b397ae 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -1261,9 +1261,6 @@ es: enabled: "Este sitio está en modo de solo lectura. Puedes continuar navegando pero algunas acciones como responder o dar me gusta no están disponibles por ahora." login_disabled: "Iniciar sesión está desactivado mientras el foro se encuentre en modo de solo lectura." logout_disabled: "Cerrar sesión está desactivado mientras el sitio se encuentre en modo de solo lectura." - too_few_topics_and_posts_notice: "¡Comencemos la discusión! Hay %{currentTopics} temas y %{currentPosts} publicaciones. Los visitantes necesitan más cosas para leer y responder. Recomendamos al menos %{requiredTopics} temas y %{requiredPosts} publicaciones. Solo el staff puede ver este mensaje." - too_few_topics_notice: "¡Comencemos la discusión! Hay %{currentTopics} temas. Los visitantes necesitan más cosas para leer y responder. Recomendamos al menos %{requiredTopics} temas. Solo el staff puede ver este mensaje." - too_few_posts_notice: "¡Comencemos la discusión! Hay %{currentPosts} publicaciones. Los visitantes necesitan más cosas para leer y responder. Recomendamos al menos %{requiredPosts} publicaciones. Solo el staff puede ver este mensaje." logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {# error/hour} otros {# errors/hour}} alcanzó el límite de la configuración del sitio de {limit, plural, one {# error/hour} otros {# errors/hour}}." reached_minute_MF: "{relativeAge}{rate, plural, one {# error/minute} otros {# errors/minute}} alcanzó el límite de la configuración del sitio de {limit, plural, one {# error/minute} otros {# errors/minute}}." @@ -2626,6 +2623,7 @@ es: help: "Este tema es invisible. No se mostrará en la lista de temas y solo se le puede acceder mediante un enlace directo" personal_message: title: "Este tema es un mensaje personal" + help: "Este tema es un mensaje personal" posts: "Publicaciones" posts_long: "Hay {{number}} publicaciones en este tema" posts_likes_MF: | @@ -4218,8 +4216,8 @@ es: description: Concede la misma medalla a muchos usuarios a la vez. no_badge_selected: "Por favor, selecciona una medalla para empezar." perform: "Conceder medalla a los usuarios" - upload_csv: Sube un archivo CSV con correos electrónicos o nombres de usuario. - aborted: Sube un archivo CSV que contenga correos electrónicos o nombres de usuario. + upload_csv: Sube un archivo CSV con correos electrónicos o nombres de usuario + aborted: "Por favor, sube un archivo CSV que contenga correos electrónicos o nombres de usuario" success: Se recibió el CSV y los usuarios recibirán la medalla dentro de poco. replace_owners: Quitar medalla de los anteriores portadores emoji: diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 50bbf323e2..2cae3f58ea 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -27,6 +27,7 @@ fa_IR: millions: "{{number}} میلیون" dates: time: "h:mm a" + time_short_day: "h:mm a" timeline_date: "MMM YYYY" long_no_year: "MMM D h:mm a" long_no_year_no_time: "MMM D" @@ -152,6 +153,8 @@ fa_IR: banner: enabled: "این در %{when} یک بنر شد، تا زمانی که کاربر آن را ببندد بالای صفحه خواهد ماند." disabled: "این بنر در %{when} حذف شده. و دیگر در بالای صفحات نمایش داده نمی‌شود." + forwarded: "ایمیل بالا را ارسال کرد" + topic_admin_menu: "اقدامات موضوع" wizard_required: "به دیسکورس خوش آمدید! برای شروع نصب کلیک کنید ✨" emails_are_disabled: "تمام ایمیل های خروجی بصورت کلی توسط مدیر قطع شده است. هیچگونه ایمیل اگاه سازی ارسال نخواهد شد." bootstrap_mode_enabled: "برای راه‌اندازی راحت‌تر سایت جدید، حالت خود‌راه‌انداز فعال شده. به تمام کاربران سطح اعتماد 1 اعطا می‌شود و ایمیل خلاصه روزانه به صورت پیشفرض برای ایشان فعال می‌شود. این امکان بصورت خودکار وقتی تعداد کاربران از %{min_users} بیشتر شود، غیر فعال خواهد شد." @@ -261,9 +264,19 @@ fa_IR: bookmarks: created: "شما این نوشته را نشانک زده‌اید" not_bookmarked: "این نوشته را نشانک بزنید" + created_with_reminder: "شما بر روی این پست نشانک همراه با یادآوری گذاشته اید در %{date}" remove: "پاک کردن نشانک" confirm_clear: "آیا مطمئنید می‌خواهید همه‌ی نشانک‌های خود را از این موضوع پاک کنید؟" save: "ذخیره" + no_timezone: 'شما هنوز منطقه زمانی مشخص نکرده اید. شما قادر به تنظیم کردن یادآوری نیستید. یکی را در پروفایل تان تنظیم کنید.' + reminders: + at_desktop: "بار آینده من پشت میزم هستم" + later_today: "امروز کمی بعد
{{date}}" + next_business_day: "روز کاری آینده
{{date}}" + tomorrow: "فردا
{{date}}" + next_week: "هفته ی آینده
{{date}}" + next_month: "ماه آینده
{{date}}" + custom: "درج تاریخ و ساعت" drafts: resume: "از سر گیری" remove: "پاک کردن" @@ -309,19 +322,37 @@ fa_IR: install_banner: "ایامایل هستید تا ، %{title} را بر‌روی دستگاه شما نصب کند؟" choose_topic: none_found: "موضوعی یافت نشد." + title: + search: "جستجوی یک موضوع" + placeholder: "جستجوی بر اساس عنوان، لینک و یا آیدی" choose_message: none_found: "پیامی پیدا نشد" + title: + search: "جستجو برای یک پیام" + placeholder: "عنوان پیام، لینک و یا آیدی آن را اینجا وارد کنید" review: order_by: "به ترتیب" in_reply_to: "در پاسخ به" explain: why: "توضیح دهید که چرا این مورد در صف پایان یافت" + title: "امتیازدهی قابل تجدید نظر" formula: "فرمول" + subtotal: "جمع جزء" total: "مجموع" + min_score_visibility: "حداقل امتیاز برای دیده شدن" + score_to_hide: "امتیاز برای پنهان کردن پست" + take_action_bonus: + name: "اقدام كرد" + title: "زمانی که یکی از کارمندان انتخاب می کند که اقدام کند، پرچم یک امتیاز می دهد." user_accuracy_bonus: name: "دقت کاربر" + title: "کاربرانی که پرچم آن ها به طور گروهی پذیرفته شده است، یک امتیاز دریافت می کنند." trust_level_bonus: name: "سطح اعتماد" + title: "موارد قابل بررسی که توسط کاربرانی با میزان اعتماد بیشتر ایجاد شده اند، اولویت بالاتری دارند." + type_bonus: + name: "امتیاز را وارد کنید" + title: "موارد قابل بررسی خاص می توانند کارمندان دارای امتیاز شوند تا اولویت آن ها را بیشتر کند." claim_help: optional: "می‌توانید این مورد را درخواست کنید تا دیگران را از بازنگری آن بازنگه دارید." required: "شما قبل از بازنگری موارد باید آن‌ها را درخواست دهید ." @@ -355,6 +386,8 @@ fa_IR: deleted_post: "(نوشته حذف شده)" deleted_user: "(کاربر حذف شده)" user: + bio: "بیوگرافی" + website: "وبسایت" username: "نام‌کاربری" email: "ایمیل" name: "نام" @@ -546,6 +579,8 @@ fa_IR: leave: "ترک کردن" request: "درخواست" message: "پیام" + confirm_leave: "آیا از ترک این گروه مطمئن هستید؟" + allow_membership_requests: "به کاربرها اجازه ی ارسال درخواست عضویت به صاحبان گروه را بدهید\n(گروه باید برای عموم قابل دیدن باشد)" membership_request_template: "الگوی سفارشی برای نمایش به کاربران هنگام ارسال درخواست عضویت" membership_request: submit: "ارسال درخواست" @@ -724,14 +759,19 @@ fa_IR: activity_stream: "فعالیت" preferences: "تنظیمات" feature_topic_on_profile: + open_search: "انتخاب یک موضوع نو" + title: "انتخاب یک موضوع" + search_label: "جستجوی یک موضوع بر اساس عنوان" save: "ذخیره" clear: title: "واضح" + warning: "آیا از پاکسازی موضوع برجسته ی خود مطمئن هستید؟" profile_hidden: "صفحه نمایه این کاربر مخفی است" expand_profile: "باز کردن" collapse_profile: "جمع کردن" bookmarks: "نشانک‌ها" bio: "درباره من" + timezone: "منطقه ی زمانی" invited_by: "دعوت شده توسط" trust_level: "سطح اعتماد" notifications: "آگاه‌سازی‌ها" @@ -758,6 +798,7 @@ fa_IR: enable_quoting: "فعال کردن نقل قول گرفتن از متن انتخاب شده" enable_defer: "تاخیر را برای علامت زدن مباحث به عنوان خوانده نشده فعال کن" change: "تغییر" + featured_topic: "مبحث برجسته" moderator: "{{user}} یک مدیر است" admin: "{{user}} یک مدیر ارشد است" moderator_tooltip: "این کاربر یک مدیر است" @@ -865,6 +906,7 @@ fa_IR: copied_to_clipboard: "کپی شد" copy_to_clipboard_error: "خطا در کپی اطلاعات" remaining_codes: "شما {{count}}کد پشتیبان باقی مانده دارید." + use: "از کد پشتیبان استفاده کنید" enable_prerequisites: "شما باید قبل از ساخت کد‌های پشتیبانی یک تایید هویت دوعاملی اصلی را فعال کنید " codes: title: "کد پشتیبان تولید شد" @@ -872,6 +914,7 @@ fa_IR: second_factor: title: "احراز هویت دو مرحله ای" enable: "مدیریت تایید هوییت دوعاملی" + forgot_password: "فراموشی گذرواژه؟" confirm_password_description: "لطفا رمز عبور خود را تایید کنید تا ادامه دهیم." name: "نام" label: "کد" @@ -885,6 +928,7 @@ fa_IR: extended_description: | احراز هویت دو عامله امنیت بیشتری به حساب کاربری شما میدهد، چرا که یک توکن یک بار مصرف علاوه بر رمز عبور خود خواهید داشت. توکن ها در ابزارهای اندروید و IOS قابل تولید هستند. oauth_enabled_warning: "دقت کنید وقتی احراز هویت دوعامله فعال شود، ورود با حساب شبکه های اجتماعی به حساب کاربری شما از کار میفتد." + use: "از اپلیکیشن احراز هویت استفاده کنید" enforced_notice: "شما باید احراز هویت دو عامله را قبل از دسترسی به سایت فعال کنید" disable: "غیرفعال" disable_title: "غیر فعال کردن تایید دوعاملی" @@ -933,6 +977,8 @@ fa_IR: change_card_background: title: "پس زمینه کارت کابر" instructions: "تصاویر پس زمینه در مرکز قرار خواهند گرفت و عرض پیشفرض آن 590 پیکسل است" + change_featured_topic: + title: "مبحث برجسته" email: title: "ایمیل" primary: "آدرس ایمیل اصلی" @@ -2475,6 +2521,7 @@ fa_IR: help: "این موضوع از فهرست خارج شد؛ و در فهرست موضوعات نمایش داده نخواهد شد، و فقط از طریق لینک مستقیم در دسترس خواهد بود. " personal_message: title: "این تاپیک یک پیام‌خصوصی است" + help: "این تاپیک یک پیام‌خصوصی است" posts: "نوشته‌ها" posts_long: "این موضوع {{number}} نوشته دارد" posts_likes_MF: | diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 2f9baea226..7b4b32cd72 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -1260,9 +1260,6 @@ fi: enabled: "Sivusto on vain luku -tilassa. Voit jatkaa selailua, mutta vastaaminen, tykkääminen ja muita toimintoja on toistaiseksi poissa käytöstä." login_disabled: "Et voi kirjautua sisään, kun sivusto on vain luku -tilassa." logout_disabled: "Et voi kirjautua ulos, kun sivusto on vain luku -tilassa." - too_few_topics_and_posts_notice: "Laitetaanpa keskustelu alulle! Tällä hetkellä palstalla on %{currentTopics} ketjua ja %{currentPosts} viestiä. Uusia kävijöitä varten tarvitaan lisää keskusteluita, joita voivat lukea ja joihin vastata. Suosittelemme vähintään %{requiredTopics} ketjua ja %{requiredPosts} viestiä. Vain henkilökunta näkee tämän viestin." - too_few_topics_notice: "Laitetaanpa keskustelu alulle! Tällä hetkellä palstalla on %{currentTopics} ketjua. Uusia kävijöitä varten tarvitaan lisää keskusteluita, joita voivat lukea ja joihin vastata. Suosittelemme vähintään %{requiredTopics} ketjua. Vain henkilökunta näkee tämän viestin." - too_few_posts_notice: "Laitetaanpa keskustelu alulle! Tällä hetkellä palstalla on %{currentPosts} viestiä. Uusia kävijöitä varten tarvitaan lisää keskusteluita, joita voivat lukea ja joihin vastata. Suosittelemme vähintään %{requiredPosts} viestiä. Vain henkilökunta näkee tämän viestin." logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {# virhe/tunti} other {# virhettä/tunti}} saavutti sivustoasetuskynnyksen {limit, plural, one {# virhe/tunti} other {# virhettä/tunti}}." reached_minute_MF: "{relativeAge}{rate, plural, one {# virhe/minuutti} other {# virhettä/minuutti}} saavutti sivustoasetuskynnyksen {limit, plural, one {# virhe/minuutti} other {# virhettä/minuutti}}." @@ -2625,6 +2622,7 @@ fi: help: "Tämä ketju on poistettu listauksista; sitä ei näytetä ketjulistauksissa vaan siihen pääsee vain suoran linkin kautta" personal_message: title: "Tämä ketju on yksityiskeskustelu" + help: "Tämä ketju on yksityiskeskustelu" posts: "Viestejä" posts_long: "tässä ketjussa on {{number}} viestiä" posts_likes_MF: | diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 253b3c6321..a4f35a7890 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -1262,9 +1262,6 @@ fr: enabled: "Le site est en mode lecture seule. Vous pouvez continer à naviguer, mais les réponses, J'aime et autre interactions sont désactivées pour l'instant." login_disabled: "La connexion est désactivée quand le site est en lecture seule." logout_disabled: "La déconnexion est désactivée quand le site est en lecture seule." - too_few_topics_and_posts_notice: "Commençons la discussion! Il y a / %{currentTopics} sujets et %{currentPosts} messages. Les visiteurs ont besoin de plus à consulter et répondre – %{requiredTopics} sujets et %{requiredPosts} messages sont recommandés. Seul le personnel peut voir ce message." - too_few_topics_notice: "Commençons la discussion! Il y a / %{currentTopics} sujets. Les visiteurs ont besoin de plus à consulter et répondre – %{requiredTopics} sujets sont recommandés. Seul le personnel peut voir ce message." - too_few_posts_notice: "Commençons la discussion! Il y a / %{currentPosts} messages. Les visiteurs ont besoin de plus à consulter et répondre – %{requiredPosts} messages sont recommandés. Seul le personnel peut voir ce message." logs_error_rate_notice: reached_hour_MF: "{relativeAge} – {rate, plural, one {# erreur/heure} other {# erreurs/heure}} arrive à la limite paramétrée de {limit, plural, one {# erreur/heure} other {# erreurs/heure}}." reached_minute_MF: "{relativeAge} – {rate, plural, one {# erreur/minute} other {# erreurs/minute}} arrive à la limite paramétrée de {limit, plural, one {# erreur/minute} other {# erreurs/minute}}." @@ -2625,6 +2622,7 @@ fr: help: "Ce sujet n'apparait plus dans la liste des sujets et sera seulement accessible via un lien direct" personal_message: title: "Ce sujet est un message personnel" + help: "Ce sujet est un message personnel" posts: "Messages" posts_long: "il y a {{number}} messages dans ce sujet" posts_likes_MF: |2 diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 852a7968bf..c6c9b2b68e 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -302,7 +302,7 @@ he: user_count: "משתמשים" active_user_count: "משתמשים פעילים" contact: "יצירת קשר" - contact_info: "במקרה של ארוע קריטי או דחוף המשפיע על האתר, נא ליצור אתנו קשר דרך: %{contact_info}." + contact_info: "במקרה של בעיה קריטית או דחופה המשפיעה על אתר זה, נא ליצור אתנו קשר דרך: %{contact_info}." bookmarked: title: "סימנייה" clear_bookmarks: "מחיקת סימניות" @@ -332,24 +332,24 @@ he: new_private_message: "טיוטת הודעה פרטית חדשה" topic_reply: "טיוטת תשובה" abandon: - confirm: "כבר קיימת טיוטה בנושא זה, האם את/ה בטוח/ה שאת/ה רוצה לזנוח אותה?" - yes_value: "כן, נטוש" + confirm: "כבר קיימת טיוטה בנושא זה. לנטוש אותה?" + yes_value: "כן, לנטוש" no_value: "לא, שמור" topic_count_latest: - one: "ראה נושא {{count}} חדש או מעודכן" - two: "ראה {{count}} נושאים חדשים או מעודכנים" - many: "ראה {{count}} נושאים חדשים או מעודכנים" - other: "ראה {{count}} נושאים חדשים או מעודכנים" + one: "הצגת נושא {{count}} חדש או עדכני" + two: "הצגת {{count}} נושאים חדשים או עדכניים" + many: "הצגת {{count}} נושאים חדשים או עדכניים" + other: "הצגת {{count}} נושאים חדשים או עדכניים" topic_count_unread: - one: "ראה נושא {{count}} שלא נקרא" - two: "ראה {{count}} נושאים שלא נקראו" - many: "ראה {{count}} נושאים שלא נקראו" - other: "ראה {{count}} נושאים שלא נקראו" + one: "הצגת נושא {{count}} שלא נקרא" + two: "הצגת {{count}} נושאים שלא נקראו" + many: "הצגת {{count}} נושאים שלא נקראו" + other: "הצגת {{count}} נושאים שלא נקראו" topic_count_new: - one: "ראה נושא {{count}} חדש" - two: "ראה {{count}} נושאים חדשים" - many: "ראה {{count}} נושאים חדשים" - other: "ראה {{count}} נושאים חדשים" + one: "הצגת נושא {{count}} חדש" + two: "הצגת {{count}} נושאים חדשים" + many: "הצגת {{count}} נושאים חדשים" + other: "הצגת {{count}} נושאים חדשים" preview: "תצוגה מקדימה" cancel: "ביטול" save: "שמירת השינויים" @@ -380,7 +380,7 @@ he: search: "חיפוש אחר נושא" placeholder: "נא להקליד כאן את כותרת הנושא, הכתובת או את המזהה" choose_message: - none_found: "לא נמצאו הודעות" + none_found: "לא נמצאו הודעות." title: search: "חיפוש אחר הודעה" placeholder: "נא להקליד כאן את כותרת ההודעה, הכתובת או את המזהה" @@ -486,7 +486,7 @@ he: two: "{{count}} תגובות" many: "{{count}} תגובות" other: "{{count}} תגובות" - edit: "ערוך" + edit: "עריכה" save: "שמירה" cancel: "ביטול" new_topic: "אישור הפריט הזה ייצור נושא חדש" @@ -512,7 +512,7 @@ he: conversation: view_full: "הצגת הדיון המלא" scores: - about: "ניקוד זה מחושב בהתאם למידת האמון של המדווח, הדיוק בדיגולים הקודמים ועדיפות הפריט המדווח." + about: "ניקוד זה מחושב בהתאם לדרגת האמון של המדווח, הדיוק בסימונים הקודמים ועדיפות הפריט המדווח." score: "ניקוד" date: "תאריך" type: "סוג" @@ -600,7 +600,7 @@ he: member_added: "הוסיף" member_requested: "בקשה התקבלה ב־" add_members: - title: "הוסף חברים" + title: "הוספת חברים" description: "נהל את החברות של קבוצה זו" usernames: "שמות משתמשים" requests: @@ -612,11 +612,11 @@ he: denied: "נדחה" undone: "הבקשה נמשכה" manage: - title: "נהל" + title: "ניהול" name: "שם" full_name: "שם מלא" - add_members: "הוסף חברים" - delete_member_confirm: "להסיר את '%{username}' מהקבוצה '%{group}'?" + add_members: "הוספת חברים" + delete_member_confirm: "להסיר את ‚%{username}’ מהקבוצה ‚%{group}’?" profile: title: פרופיל interaction: @@ -655,7 +655,7 @@ he: allow_membership_requests: " לאפשר למשתמשים לשלוח בקשות חברות לבעלי הקבוצה (נדרשת קבוצה גלויה לכלל)" membership_request_template: "תבנית מותאמת אישית שתוצג למשתמשים בעת שליחת בקשת חברות" membership_request: - submit: "שלח בקשה" + submit: "הגשת בקשה" title: "בקש להצטרף ל%{group_name}" reason: "תן לבעלי הקבוצה לדעת למה אתה שייך לקבוצה זו" membership: "חברות" @@ -811,13 +811,13 @@ he: user: said: "{{username}}:" profile: "פרופיל" - mute: "השתק" + mute: "השתקה" edit: "עריכת העדפות" download_archive: button_text: "להוריד הכל" confirm: "להוריד את הפוסטים שלך?" - success: "הורדה החלה, תקבלו הודעה כאשר התהליך הסתיים." - rate_limit_error: "ניתן להוריד פוסטים פעם ביום, אנא נסו שוב מחר." + success: "ההורדה החלה, תישלח אליך הודעה עם סיום התהליך." + rate_limit_error: "ניתן להוריד פוסטים פעם אחת ביום, נא לנסות שוב מחר." new_private_message: "הודעה חדשה" private_message: "הודעה" private_messages: "הודעות" @@ -844,7 +844,7 @@ he: search_label: "חיפוש אחר נושא לפי כותרת" save: "שמירה" clear: - title: "נקה" + title: "ניקוי" warning: "למחוק את הנושאים המומלצים שלך?" profile_hidden: "הפרופיל הציבורי של משתמש זה מוסתר" expand_profile: "הרחב" @@ -874,7 +874,7 @@ he: theme_default_on_all_devices: "הגדרת ערכת עיצוב זו כבררת המחדל לכל המכשירים שלי" text_size_default_on_all_devices: "הפוך את גודל הטקסט הזה לברירת המחדל בכל המכשירים שלי" allow_private_messages: "אפשר למשתמשים אחרים לשלוח לי הודעות פרטיות" - external_links_in_new_tab: "פתח את כל הקישורים החיצוניים בעמוד חדש" + external_links_in_new_tab: "פתיחת כל הקישורים החיצוניים בלשונית חדשה" enable_quoting: "הפעלת תגובת ציטוט לטקסט מסומן" enable_defer: "הפעלת דחייה לאחר כך כדי לסמן נושאים כלא נקראו" change: "שנה" @@ -887,7 +887,7 @@ he: suspended_notice: "המשתמש הזה מושעה עד לתאריך: {{date}}." suspended_permanently: "משתמש זה מושעה." suspended_reason: "הסיבה: " - github_profile: "גיטהאב" + github_profile: "GitHub" email_activity_summary: "סיכום פעילות" mailing_list_mode: label: "מצב רשימת תפוצה" @@ -919,12 +919,12 @@ he: muted_categories_instructions: "לא תקבל הודעה בנוגע לנושאים חדשים בקטגוריות אלה, והם לא יופיעו בקטגוריות או בדפים האחרונים." muted_categories_instructions_dont_hide: "לא תישלחנה אליך התראות על שום דבר בנוגע לנושאים בקטגוריות האלו." no_category_access: "בתור פיקוח יש לך גישה מוגבלת לקטגוריות, שמירה מנוטרלת." - delete_account: "מחק את החשבון שלי" + delete_account: "מחיקת החשבון שלי" delete_account_confirm: "להסיר את החשבון? לא ניתן לבטל פעולה זו!" - deleted_yourself: "חשבונך נמחק בהצלחה." - delete_yourself_not_allowed: "נא לפנות לחבר סגל אם ברצונך למחוק את חשבונך." + deleted_yourself: "החשבון שלך נמחק בהצלחה." + delete_yourself_not_allowed: "נא לפנות לחבר סגל אם ברצונך למחוק את החשבון שלך." unread_message_count: "הודעות" - admin_delete: "מחק" + admin_delete: "מחיקה" users: "משתמשים" muted_users: "מושתק" muted_users_instructions: "להשבית כל התראה ממשתמשים אלו" @@ -953,10 +953,10 @@ he: archive: "ארכיון" groups: "הקבוצות שלי" bulk_select: "בחר הודעות" - move_to_inbox: "העבר לדואר נכנס" + move_to_inbox: "העברה לדואר נכנס" move_to_archive: "ארכיון" failed_to_move: "בעיה בהעברת ההודעות שנבחרו (אולי יש תקלה בהתחברות?)" - select_all: "בחרו הכל" + select_all: "לבחור הכול" tags: "תגיות" preferences_nav: account: "חשבון" @@ -1013,7 +1013,7 @@ he: disable: "נטרול" disable_title: "נטרול אימות דו־שלבי" disable_confirm: "לנטרל את כל האימותים הדו־שלביים?" - edit: "ערוך" + edit: "עריכה" edit_title: "עריכת אימות דו־שלבי" edit_description: "שם אימות דו־שלבי" enable_security_key_description: "כשמפתח האבטחה הפיזי שלך מוכן יש ללחוץ על כפתור הרישום שלהלן." @@ -1361,9 +1361,6 @@ he: enabled: "אתר זה נמצא במצב קריאה בלבד. אנא המשיכו לשוטט, אך תגובות, לייקים, ופעולות נוספות כרגע אינם מאופשרים." login_disabled: "הכניסה מנוטרלת בזמן שהאתר במצב קריאה בלבד." logout_disabled: "היציאה מנוטרלת בזמן שהאתר במצב של קריאה בלבד." - too_few_topics_and_posts_notice: "הבה נתחיל להתדיין! כרגע ישנם %{currentTopics} נושאים ו־%{currentPosts} פוסטים. המבקרים זקוקים ליותר תוכן כדי לקרוא ולהגיב להם - אנו ממליצים על %{requiredTopics} נושאים ו־%{requiredPosts} פוסטים לפחות. רק הסגל יכול לראות את ההודעה הזאת." - too_few_topics_notice: "הבה נתחיל להתדיין! כרגע ישנם %{currentTopics} נושאים – המבקרים זקוקים ליותר תוכן כדי להגיב - אנו ממליצים על %{requiredTopics} נושאים לפחות. רק הסגל יכול לראות את ההודעה הזאת." - too_few_posts_notice: "הבה נתחיל להתדיין! כרגע ישנם %{currentPosts} פוסטים. המבקרים זקוקים ליותר תוכן כדי להגיב - אנו ממליצים על %{requiredPosts} פוסטים לפחות. רק הסגל יכול לראות את ההודעה הזאת." logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {שגיאה אחת בשעה הגיעה} other {# שגיאות בשעה הגיעו}} למגבלת האתר שהיא {limit, plural, one {שגיאה אחת בשעה} other {# שגיאות בשעה}}." reached_minute_MF: "{relativeAge}{rate, plural, one {שגיאה אחת בדקה הגיעה} other {# שגיאות בדקה הגיעו}} למגבלת האתר שהיא {limit, plural, one {שגיאה אחת בדקה} other {# שגיאות בדקה}}." @@ -1381,7 +1378,7 @@ he: day: "יום" first_post: פוסט ראשון mute: השתק - unmute: בטל השתקה + unmute: ביטול השתקה last_post: פורסמו time_read: נקרא time_read_recently: "%{time_read} לאחרונה" @@ -1414,7 +1411,7 @@ he: private_message_info: title: "הודעה" invite: "הזמינו אחרים..." - edit: "הוסף או הסר..." + edit: "הוספה או הסרה…" leave_message: "האם אתה באמת רוצה לעזוב את ההודעה הזו?" remove_allowed_user: "להסיר את {{name}} מהודעה זו?" remove_allowed_group: "להסיר את {{name}} מהודעה זו?" @@ -1620,7 +1617,7 @@ he: saved_local_draft_tip: "נשמר מקומית" similar_topics: "הנושא שלך דומה ל..." drafts_offline: "טיוטות מנותקות" - edit_conflict: "ערוך קונפליקט." + edit_conflict: "עריכת סתירה" group_mentioned_limit: "זהירות! ציינת {{group}}, אך לקבוצה זו יש יותר חברים משהוגדר למנהל המערכת הגבלה של עד {{max}} משתמשים. אף אחד לא יקבל הודעה" group_mentioned: one: "על ידי אזכור {{group}}, אתם עומדים ליידע אדם אחד – אתם בטוחים?" @@ -1652,7 +1649,7 @@ he: create_pm: "הודעה" create_whisper: "לחישה" create_shared_draft: "צור טיוטה משותפת" - edit_shared_draft: "ערוך טויטה משותפת" + edit_shared_draft: "עריכת טיוטה משותפת" title: "או לחצו Ctrl+Enter" users_placeholder: "הוספת משתמש" title_placeholder: " במשפט אחד, במה עוסק הדיון הזה?" @@ -1709,7 +1706,7 @@ he: composer_actions: reply: השב draft: טיוטה - edit: ערוך + edit: עריכה reply_to_post: label: "תגובה לפוסט %{postNumber} ע\"י %{postUsername}" desc: תגובה לפוסט ספיציפי @@ -1990,7 +1987,7 @@ he: help: "החזרת הודעה לדואר נכנס" edit_message: help: "ערוך פוסט ראשון של ההודעה" - title: "ערוך הודעה" + title: "עריכת הודעה" defer: help: "סימון כלא נקראו" title: "אחר כך" @@ -2055,7 +2052,7 @@ he: group_join: "עליך להצטרף לקבוצה `{{name}}` כדי לצפות בנושא הזה" group_request_sent: "בקשת החברות שלך נשלחה לקבוצה הזו. ניידע אותך כשהיא תתקבל." unread_indicator: "אף אחד מהחברים לא קרא את הפוסט האחרון של הנושא הזה עדיין." - read_more_MF: "יש { UNREAD, plural, =0 {} one { 1 שלא נקרא } other { # שלא נקראו } } { NEW, plural, =0 {} one { {BOTH, select, true{ו} false {} other{}} נושא חדש אחד} other { {BOTH, select, true{ו} false {} other{}} # נושאים חדשים} } נותרים, or {CATEGORY, select, true {עיין בנושאים אחרים ב {catLink}} false {{latestLink}} other {}}" + read_more_MF: "יש { UNREAD, plural, =0 {} one { 1 שלא נקרא } other { # שלא נקראו } } { NEW, plural, =0 {} one { {BOTH, select, true{ו} false {} other{}} נושא חדש אחד} other { {BOTH, select, true{ו} false {} other{}} # נושאים חדשים} } נותרים, או {CATEGORY, select, true {עיין בנושאים אחרים ב־{catLink}} false {{latestLink}} other {}}" browse_all_categories: עיינו בכל הקטגוריות view_latest_topics: הצגת נושאים אחרונים suggest_create_topic: "למה לא ליצור נושא חדש?" @@ -2616,7 +2613,7 @@ he: none: "(ללא קטגוריה)" all: "כל הקטגוריות" choose: "קטגוריה…" - edit: "ערוך" + edit: "עריכה" edit_dialog_title: "עריכה: %{categoryName}" view: "הצגת נושאים בקטגוריה" general: "כללי" @@ -2635,7 +2632,7 @@ he: min_tags_from_required_group_label: "מס׳ תגיות:" required_tag_group_label: "קבוצת תגיות:" topic_featured_link_allowed: "אפשרו קישורים מומלצים בקטגוריה זו" - delete: "מחק קטגוריה" + delete: "מחיקת קטגוריה" create: "קטגוריה חדשה" create_long: "יצירת קטגוריה חדשה" save: "שמירת קטגוריה" @@ -2657,7 +2654,7 @@ he: delete_error: "ארעה שגיאה במחיקת הקטגוריה." list: "הצג קטגוריות" no_description: "אנא הוסיפו תיאור לקטגוריה זו." - change_in_category_topic: "ערוך תיאור" + change_in_category_topic: "עריכת תיאור" already_used: "הצבע הזה בשימוש על ידי קטגוריה אחרת" security: "אבטחה" special_warning: "אזהרה: קטגוריה זו הגיעה מראש והגדרות האבטחה שלה אינן ניתנות לשינוי. אם אתם מעוניינים להשתמש בקטגוריה זו, מחקו אותה במקום להשתמש בה מחדש." @@ -2679,7 +2676,7 @@ he: default_view: "תצוגת ברירת מחדל לנושאים:" default_top_period: "פרק זמן דיפולטיבי להובלה" allow_badges_label: "לאפשר הענקת עיטורים בקטגוריה זו" - edit_permissions: "ערוך הרשאות" + edit_permissions: "עריכת הרשאות" reviewable_by_group: "בנוסף לסגל, פוסטים ודגלים בקטגוריה הזאת יכולים להיות נתונים גם לסקירתם של:" review_group_name: "שם הקבוצה" require_topic_approval: "לדרוש אישור מפקח לכל הנושאים החדשים" @@ -2822,6 +2819,7 @@ he: help: "נושא זה מוסתר; הוא לא יוצג ברשימות הנושאים, וזמין רק באמצעות קישור ישיר." personal_message: title: "הנושא הזה הוא הודעה אישית" + help: "הנושא הזה הוא הודעה אישית" posts: "פוסטים" posts_long: "יש {{number}} פוסטים בנושא הזה" posts_likes_MF: | @@ -2885,10 +2883,10 @@ he: unread: title: "לא-נקראו" title_with_count: - one: "לא נקרא (%{count})" + one: "לא נקראה (%{count})" two: "לא-נקראו ({{count}})" many: "לא-נקראו ({{count}})" - other: "לא-נקראו ({{count}})" + other: "לא נקראו ({{count}})" help: "נושאים שאתם כרגע צופים או עוקבים אחריהם עם פוסטים שלא נקראו" lower_title_with_count: one: "לא נקרא (%{count})" @@ -3097,7 +3095,7 @@ he: add_synonyms_failed: "לא ניתן להוסיף את התגיות הבאות בתור מילים נרדפות: %{tag_names}. נא לוודא שאין להן מילים נרדפות ושאינן כבר מילים נרדפות של תגית אחרת." remove_synonym: "הסרת מילה נרדפת" delete_synonym_confirm: 'למחוק את המילה הנרדפת „%{tag_name}”?' - delete_tag: "מחק תגית" + delete_tag: "מחיקת תגית" delete_confirm: one: "למחוק את התגית הזו ולהסיר אותה מהנושא אליו היא מוקצית?" two: "למחוק את התגית הזו ולהסיר אותה משני הנושאים אליהן היא מוקצית?" @@ -3346,14 +3344,14 @@ he: primary: "קבוצה ראשית" no_primary: "(אין קבוצה ראשית)" title: "קבוצות" - edit: "ערוך קבוצות" + edit: "עריכת קבוצות" refresh: "רענן" - about: "ערוך את חברות הקבוצה שלך והשמות כאן" + about: "עריכת חברות הקבוצה שלך והשמות כאן" group_members: "חברי הקבוצה" - delete: "מחק" + delete: "מחיקה" delete_confirm: "להסיר קבוצה זו?" delete_failed: "לא ניתן להסיר קבוצה זו. אם זו קבוצה אוטומטית, היא בלתי ניתנת למחיקה." - delete_owner_confirm: "הסרת הרשאות מנהל עבור '%{username}'?" + delete_owner_confirm: "להסיר את הרשאות הניהול של ‚%{username}’?" add: "הוספה" custom: "מותאם" automatic: "אוטומטי" @@ -3548,7 +3546,7 @@ he: title: "שליחת הודעה בדוא״ל עם קישור להורדה" alert: "קישור להורדת גיבוי זה נשלח אליכם." destroy: - title: "הסר את הגיבוי" + title: "הסרת הגיבוי" confirm: "להשמיד את הגיבוי הזה?" restore: is_disabled: "שחזור אינו מאופשר לפי הגדרות האתר." @@ -3587,11 +3585,11 @@ he: new: "חדש" new_style: "סגנון חדש" install: "התקנה" - delete: "מחק" + delete: "מחיקה" delete_confirm: 'למחוק את „%{theme_name}”?' color: "צבע" opacity: "שקיפות" - copy: "העתק" + copy: "העתקה" copy_to_clipboard: "העתקה ללוח" copied_to_clipboard: "הועתק ללוח" copy_to_clipboard_error: "שגיאה בהעתקת מידע ללוח" @@ -3603,7 +3601,7 @@ he: body: "גוף" none_selected: "בחרו תבנית דואר אלקטרוני לעריכה." revert: "ביטול שינויים" - revert_confirm: "האם ברצונכם לבטל את השינויים?" + revert_confirm: "לבטל את השינויים שלך?" theme: theme: "ערכת עיצוב" component: "רכיב" @@ -3891,8 +3889,8 @@ he: topic_id: "זהות (ID) נושא" post_id: "מזהה פוסט" category_id: "מזהה קטגוריה" - delete: "מחק" - edit: "ערוך" + delete: "מחיקה" + edit: "עריכה" save: "שמור" screened_actions: block: "חסום" @@ -3916,7 +3914,7 @@ he: no_previous: "אין ערך קודם." deleted: "אין ערך חדש. הרשומה נמחקה." actions: - delete_user: "מחק משתמש" + delete_user: "מחיקת משתמש" change_trust_level: "שנוי דרגת אמון" change_username: "שינוי שם משתמש/ת" change_site_setting: "שנוי הגדרות אתר" @@ -4018,7 +4016,7 @@ he: form: label: "חדש:" ip_address: "כתובת IP" - add: "הוסף" + add: "הוספה" filter: "חיפוש" roll_up: text: "גלגול (Roll up)" @@ -4085,7 +4083,7 @@ he: invalid: "סליחה, אך אינך מורשה להתחזות למשתמש הזה." users: title: "משתמשים" - create: "הוסף מנהל" + create: "הוספת מנהל" last_emailed: "נשלח בדואר אלקטרוני לאחרונה" not_found: "סליחה, שם המשתמש הזה אינו קיים במערכת שלנו." id_not_found: "מצטערים, זהות המשתמש/ת אינה קיימת במערכת שלנו." @@ -4142,13 +4140,13 @@ he: silence_message_placeholder: "(להשאיר ריק כדי לשלוח את הודעת בררת המחדל)" suspended_until: "(עד %{until})" cant_suspend: "אי אפשר להשעות את המשתמש הזה." - delete_all_posts: "מחק את כל הפוסטים" + delete_all_posts: "מחיקת כל הפוסטים" delete_posts_progress: "פוסטים נמחקים…" delete_posts_failed: "אירעה תקלה בעת מחיקת הפוסטים." penalty_post_actions: "מה לעשות עם הפוסט המשויך?" penalty_post_delete: "למחוק את הפוסט" penalty_post_delete_replies: "למחוק את הפוסט על תגובותיו" - penalty_post_edit: "לערוך את הפוסט" + penalty_post_edit: "עריכת הפוסט" penalty_post_none: "לא לעשות דבר" penalty_count: "ספירת עונשין" clear_penalty_history: @@ -4202,7 +4200,7 @@ he: anonymize_confirm: "באמת<\\b> להפוך חשבון זה לאלמוני? פעולה זו תשנה את שם המשתמש ואת כתובת הדוא״ל ותאפס את כל נתוני הפרופיל." anonymize_yes: "כן, נא להפוך חשבון זה לאלמוני" anonymize_failed: "אירעה תקלה בהפיכת חשבון זה לאלמוני." - delete: "מחק משתמש/ת" + delete: "מחיקת משתמש" delete_forbidden_because_staff: "לא ניתן למחוק מנהלים ומפקחים." delete_posts_forbidden_because_staff: "לא ניתן להסיר את כל הפוסטים של מנהלי מערכת ומפקחים." delete_forbidden: @@ -4335,7 +4333,7 @@ he: search: "חפשו טקסט שברצונכם לערוך" title: "טקסט" edit: "ערוך" - revert: "בטל שינויים" + revert: "ביטול שינויים" revert_confirm: "לבטל את השינויים שלך?" go_back: "חזרה לחיפוש" recommended: "אנו ממליצים לערוך את הטקסט הבא כדי שיתאים לצרכים שלך:" @@ -4349,7 +4347,7 @@ he: title: "הגדרות" no_results: "לא נמצאו תוצאות." more_than_30_results: "יש למעלה מ־50 תוצאות. נא למקד את החיפוש שלך." - clear_filter: "נקה" + clear_filter: "ניקוי" add_url: "הוספת כתובת URL" add_host: "הוספת שרת" add_group: "הוספת קבוצה" @@ -4413,7 +4411,7 @@ he: granted_at: הוענק ב reason_help: (קישור לפוסט או לנושא) save: שמור - delete: מחק + delete: מחיקה delete_confirm: "למחוק את העיטור הזה?" revoke: שלול reason: סיבה diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index 3cf3589187..a58d8484b1 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -27,6 +27,7 @@ it: millions: "{{number}}M" dates: time: "h:mm a" + time_short_day: "ddd, HH:mm" timeline_date: "MMM YYYY" long_no_year: "D MMM h:mm a" long_no_year_no_time: "D MMM" @@ -56,7 +57,7 @@ it: other: "%{count}m" about_x_hours: one: "%{count}ora" - other: "%{count}ore" + other: "%{count} ore" x_days: one: "%{count}giorno" other: "%{count}giorni" @@ -77,7 +78,7 @@ it: medium: x_minutes: one: "%{count} min" - other: "%{count}min" + other: "%{count} min" x_hours: one: "%{count} ora" other: "%{count} ore" @@ -88,10 +89,10 @@ it: medium_with_ago: x_minutes: one: "%{count} minuto fa" - other: "%{count}minuti fa" + other: "%{count} minuti fa" x_hours: one: "%{count} ora fa" - other: "%{count}ore fa" + other: "%{count} ore fa" x_days: one: "%{count} giorno fa" other: "%{count}giorni fa" @@ -107,7 +108,7 @@ it: other: "%{count}giorni dopo" x_months: one: "%{count} mese dopo" - other: "%{count}mesi dopo" + other: "%{count} mesi dopo" x_years: one: "%{count} anno dopo" other: "%{count}anni dopo" @@ -152,6 +153,8 @@ it: banner: enabled: "lo ha reso un annuncio il %{when}. Apparirà in cima ad ogni pagina finché non verrà chiuso dall'utente. " disabled: "ha rimosso questo banner il %{when}. Non apparirà più in cima ad ogni pagina." + forwarded: "inoltrato l'e-mail qui sopra" + topic_admin_menu: "azioni argomento" wizard_required: "Benvenuto al tuo nuovo Discourse! Inizia la procedura guidata di configurazione ✨" emails_are_disabled: "Tutte le email in uscita sono state disabilitate a livello globale da un amministratore. Non sarà inviato nessun tipo di notifica via email." bootstrap_mode_enabled: "Per aiutarti ad avviare il tuo nuovo sito, adesso sei in modalità bootstrap. Tutti i nuovi utenti saranno automaticamente promossi al livello di esperienza 1 e riceveranno il riepilogo quotidiano degli aggiornamenti via email. Questa modalità sarà disattivata automaticamente quando avrai più di %{min_users}utenti iscritti." @@ -261,9 +264,19 @@ it: bookmarks: created: "hai inserito questo messaggio nei segnalibri" not_bookmarked: "aggiungi ai segnalibri" + created_with_reminder: "hai aggiunto questo messaggio ai segnalibri con un promemoria il%{date}" remove: "Rimuovi Segnalibro" confirm_clear: "Sei certo di voler rimuovere tutti i segnalibri da questo argomento?" save: "Salva" + no_timezone: 'Non hai ancora impostato un fuso orario. Non sarai in grado di impostare promemoria. Creane uno nel tuo profilo .' + reminders: + at_desktop: "La prossima volta che sarò al mio desktop" + later_today: "Più tardi, oggi
{{date}}" + next_business_day: "Il prossimo giorno lavorativo
{{date}}" + tomorrow: "Domani
{{date}}" + next_week: "La prossima settimana
{{date}}" + next_month: "Il prossimo mese
{{date}}" + custom: "Data e ora personalizzate" drafts: resume: "Riprendi" remove: "Rimuovi" @@ -309,8 +322,14 @@ it: install_banner: "Vuoi installare %{title} su questo dispositivo?" choose_topic: none_found: "Nessun argomento trovato." + title: + search: "Cerca un argomento" + placeholder: "digita qui il titolo dell'argomento, l'URL o l'id" choose_message: none_found: "Nessun messaggio trovato." + title: + search: "Cerca un messaggio" + placeholder: "digita qui il titolo del messaggio, l'URL o l'id" review: order_by: "Ordina per" in_reply_to: "in risposta a" @@ -347,7 +366,7 @@ it: delete: "Elimina" settings: saved: "Salvato" - save_changes: "Salva Modifiche" + save_changes: "Salva le modifiche" title: "Impostazioni" priorities: title: "Priorità Revisionabili" @@ -368,6 +387,7 @@ it: deleted_user: "(utente eliminato)" user: bio: "Biografia" + website: "Sito web" username: "Nome utente" email: "Email" name: "Nome" @@ -559,6 +579,8 @@ it: leave: "Abbandona" request: "Richiesta" message: "Messaggio" + confirm_leave: "Sei sicuro di voler abbandonare questo gruppo?" + allow_membership_requests: "Consenti agli utenti di inviare richieste di ammissione ai proprietari dei gruppi (richiede un gruppo visibile pubblicamente)" membership_request_template: "Modello personalizzato da mostrare agli utenti quando inviano una richiesta di adesione" membership_request: submit: "Invia Richiesta" @@ -737,9 +759,13 @@ it: activity_stream: "Attività" preferences: "Opzioni" feature_topic_on_profile: + open_search: "Seleziona un nuovo argomento" + title: "Seleziona un argomento" + search_label: "Cerca argomento per titolo" save: "Salva" clear: title: "Pulisci" + warning: "Sei sicuro di voler cancellare l'argomento in primo piano?" profile_hidden: "Il profilo pubblico di questo utente è nascosto." expand_profile: "Espandi" collapse_profile: "Raggruppa" @@ -772,6 +798,7 @@ it: enable_quoting: "Abilita \"rispondi quotando\" per il testo evidenziato" enable_defer: "Abilita il rinvio per contrassegnare gli argomenti come non letti" change: "cambia" + featured_topic: "Argomento in primo piano" moderator: "{{user}} è un moderatore" admin: "{{user}} è un amministratore" moderator_tooltip: "Questo utente è un moderatore" @@ -795,7 +822,7 @@ it: warning: "Modalità Mailing List attiva. Le impostazioni per la notifica via email verranno ignorate." tag_settings: "Etichette" watched_tags: "Osservate" - watched_tags_instructions: "Osserverai automaticamente tutti gli argomenti con queste etichette. Verrai notificato di tutti i nuovi messaggi e argomenti, e accanto all'argomento apparirà anche un conteggio dei nuovi messaggi." + watched_tags_instructions: "Osserverai automaticamente tutti gli argomenti con questi tag. Verrai notificato di tutti i nuovi messaggi e argomenti, e accanto all'argomento apparirà anche un conteggio dei nuovi messaggi." tracked_tags: "Seguite" tracked_tags_instructions: "Seguirai automaticamente tutti gli argomenti con queste etichette. Accanto all'argomento apparirà il conteggio dei nuovi messaggi." muted_tags: "Silenziati" @@ -809,8 +836,8 @@ it: watched_first_post_tags: "Osservando Primo Messaggio" watched_first_post_tags_instructions: "Riceverai la notifica per il primo messaggio di ogni nuovo argomento con queste etichette." muted_categories: "Silenziate" - muted_categories_instructions: "Non riceverai notifiche relative a nuovi Argomenti in queste Categorie, e non appariranno nella pagina delle Categorie o dei Recenti." - muted_categories_instructions_dont_hide: "Non riceverai alcuna notifica per i nuovi Argomenti creati in questa categoria." + muted_categories_instructions: "Non riceverai notifiche riguardanti i contenuti di queste categorie, e non appariranno nelle pagine delle Categorie o dei Recenti." + muted_categories_instructions_dont_hide: "Non riceverai alcuna notifica riguardante nuovi argomenti in queste categorie." no_category_access: "Come moderatore hai accesso limitato alla categoria, il salvataggio è disabilitato." delete_account: "Cancella il mio account" delete_account_confirm: "Sei sicuro di voler cancellare il tuo account in modo permanente? Questa azione non può essere annullata!" @@ -929,7 +956,7 @@ it: error: "Si è verificato un errore durante la modifica del valore." change_username: title: "Cambia Utente" - confirm: "Sei assolutamente certo di voler cambiare il tuo nome utente?" + confirm: "Sei sicuro di voler davvero cambiare il tuo nome utente?" taken: "Spiacenti, questo nome utente è già riservato." invalid: "Nome utente non valido: usa solo lettere e cifre" change_email: @@ -955,6 +982,9 @@ it: change_card_background: title: "Sfondo Scheda Utente" instructions: "Le immagini di sfondo saranno centrate e per difetto avranno un'ampiezza di 590px." + change_featured_topic: + title: "Argomento in primo piano" + instructions: "Un collegamento a questo argomento sarà sulla tua scheda utente e profilo." email: title: "Email" primary: "Email principale" @@ -1090,6 +1120,7 @@ it: search: "digita per cercare inviti..." title: "Inviti" user: "Utente Invitato" + sent: "Ultimo inviato" none: "Nessun invito da visualizzare." truncated: one: "Mostro il primo invito." @@ -2559,6 +2590,7 @@ it: help: "Questo argomento è invisibile; non verrà mostrato nella liste di argomenti ed è possibile accedervi solo tramite collegamento diretto" personal_message: title: "Questo Argomento è un Messaggio Personale" + help: "Questo Argomento è un Messaggio Personale" posts: "Messaggi" posts_long: "ci sono {{number}} messaggi in questo argomento" posts_likes_MF: | diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 3e3da3ffae..9cbacc231d 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -1261,9 +1261,12 @@ nl: enabled: "Deze website bevindt zich in alleen-lezenmodus. U kunt doorgaan met browsen, maar berichten beantwoorden, likes geven en andere acties zijn momenteel uitgeschakeld." login_disabled: "Aanmelden is uitgeschakeld zolang de website zich in alleen-lezenmodus bevindt." logout_disabled: "Afmelden is uitgeschakeld zolang de website zich in alleen-lezenmodus bevindt." - too_few_topics_and_posts_notice: "Laten we de discussie starten! Er zijn %{currentTopics} topics en %{currentPosts} berichten. Bezoekers hebben er meer nodig om te lezen en op te antwoorden – minstens %{requiredTopics} topics en %{requiredPosts} berichten wordt aanbevolen. Alleen stafleden kunnen dit bericht zien." - too_few_topics_notice: "Laten we de discussie starten! Er zijn %{currentTopics} topics. Bezoekers hebben er meer nodig om te lezen en op te antwoorden – minstens %{requiredTopics} topics wordt aanbevolen. Alleen stafleden kunnen dit bericht zien." - too_few_posts_notice: "Laten we de discussie starten! Er zijn %{currentPosts} berichten. Bezoekers hebben er meer nodig om te lezen en op te antwoorden – minstens %{requiredPosts} berichten wordt aanbevolen. Alleen stafleden kunnen dit bericht zien." + too_few_topics_and_posts_notice_MF: >- + Laten we de discussie starten! Er {currentTopics, plural, one {is # topic} other {zijn # topics}} en {currentPosts, plural, one {# bericht} other {# berichten}}. Bezoekers hebben er meer nodig om te lezen en op te antwoorden – minstens {requiredTopics, plural, one {# topic} other {# topics}} en {requiredPosts, plural, one {# bericht} other {# berichten}} wordt aanbevolen. Alleen stafleden kunnen dit bericht zien. + too_few_topics_notice_MF: >- + Laten we de discussie starten! Er {currentTopics, plural, one {is # topic} other {zijn # topics}}. Bezoekers hebben er meer nodig om te lezen en op te antwoorden – minstens {requiredTopics, plural, one {# topic} other {# topics}} wordt aanbevolen. Alleen stafleden kunnen dit bericht zien. + too_few_posts_notice_MF: >- + Laten we de discussie starten! Er {currentPosts, plural, one {is # bericht} other {zijn # berichten}}. Bezoekers hebben er meer nodig om te lezen en op te antwoorden – minstens {requiredPosts, plural, one {# bericht} other {# berichten}} wordt aanbevolen. Alleen stafleden kunnen dit bericht zien. logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {# fout/uur} other {# fouten/uur}} heeft de limiet voor de website-instelling van {limit, plural, one {# fout/uur} other {# fouten/uur}} bereikt." reached_minute_MF: "{relativeAge}{rate, plural, one {# fout/minuut} other {# fouten/minuut}} heeft de limiet voor de website-instelling van {limit, plural, one {# fout/minuut} other {# fouten/minuut}} bereikt." @@ -2626,6 +2629,7 @@ nl: help: "Dit topic is niet zichtbaar; het verschijnt niet in topiclijsten en kan alleen via een rechtstreekse koppeling worden benaderd" personal_message: title: "Dit topic is een persoonlijk bericht" + help: "Dit topic is een persoonlijk bericht" posts: "Berichten" posts_long: "er zijn {{number}} berichten in dit topic" posts_likes_MF: | diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 7f44991962..a6bfdba8c2 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -1304,9 +1304,6 @@ pl_PL: enabled: "Strona jest w trybie tylko-do-odczytu. Możesz nadal przeglądać serwis, ale operacje takie jak postowanie, lajkowanie i inne są wyłączone." login_disabled: "Logowanie jest zablokowane, gdy strona jest w trybie tylko do odczytu." logout_disabled: "Wylogowanie jest zablokowane gdy strona jest w trybie tylko do odczytu." - too_few_topics_and_posts_notice: "Rozpocznijmy dyskusję! Istnieje %{currentTopics} tematów i %{currentPosts} postów. Goście muszą więcej czytać i odpowiadać - zalecamy przynajmniej %{requiredTopics} tematów i %{requiredPosts} posty. Tylko pracownicy mogą zobaczyć tę wiadomość." - too_few_topics_notice: "Zacznijmy dyskusję! Jest %{currentTopics} tematów. Odwiedzający potrzebują więcej do przeczytania i odpowiedzi - zalecamy co najmniej %{requiredTopics} tematów. Tylko pracownicy mogą zobaczyć tę wiadomość." - too_few_posts_notice: "Zacznijmy dyskusję! Jest %{currentPosts} postów. Odwiedzający potrzebują więcej, aby przeczytać i odpowiedzieć - zalecamy co najmniej %{requiredPosts} postów. Tylko pracownicy mogą zobaczyć tę wiadomość." learn_more: "dowiedz się więcej…" all_time: "łącznie" all_time_desc: "łącznie utworzonych tematów" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index bdc87c1e8d..a792d11c64 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -1257,9 +1257,6 @@ pt_BR: enabled: "Este site está em modo de somente leitura. Por favor, continue a navegar, mas respostas, curtidas e outras ações estão desabilitadas por enquanto." login_disabled: "O login é desabilitado enquanto o site está em modo de somente leitura." logout_disabled: "O logout é desabilitado enquanto o site está em modo de somente leitura." - too_few_topics_and_posts_notice: "Vamos iniciar a discussão!%{currentTopics} tópicos e %{currentPosts} postagens. Os visitantes precisam de mais informações para ler e responder - nós recomendamos pelo menos %{requiredTopics} tópicos e %{requiredPosts} posts. Somente funcionários podem ver esta mensagem." - too_few_topics_notice: "Vamos iniciar a discussão!%{currentTopics} tópicos. Os visitantes precisam de mais informações para ler e responder - nós recomendamos pelo menos %{requiredTopics} tópicos. Somente funcionários podem ver esta mensagem." - too_few_posts_notice: "Vamos inicar a discussão!%{currentPosts} postagens. Os visitantes precisam de mais informações para ler e responder - nós recomendamos pelo menos %{requiredPosts} postagens. Somente funcionários podem ver esta mensagem." logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {# erro/hora} other {# erros/hora}} alcançou o limite de configuração do site de {limit, plural, one {# erro/hora} other {# erros/hora}}." reached_minute_MF: "{relativeAge}{rate, plural, one {# erro/minuto} other {# erros/minuto}} alcançou o limite de configuração do site de {limit, plural, one {# erro/minuto} other {# erros/minuto}}." @@ -2613,6 +2610,7 @@ pt_BR: help: "Este tópico não está listado; ele não será exibido em listas de tópicos e só pode ser acessado por meio de um link direto" personal_message: title: "Este tópico é uma mensagem pessoal" + help: "Este tópico é uma mensagem pessoal" posts: "Postagens" posts_long: "há {{number}} mensagens neste tópico" posts_likes_MF: | diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 987c958f7a..72064bfd92 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -426,7 +426,7 @@ ru: title: "Обзорные приоритеты" moderation_history: "История модерации" view_all: "Посмотреть все" - grouped_by_topic: "Сгруппированы по темам" + grouped_by_topic: "Сгруппированные по темам" none: "Нет элементов для премодерации." view_pending: "просмотр в ожидании" topic_has_pending: @@ -1361,9 +1361,12 @@ ru: enabled: "Сайт работает в режиме \"только для чтения\". Сейчас вы можете продолжать просматривать сайт, но другие действия будут недоступны. " login_disabled: "Вход отключён, пока сайт в режиме «только для чтения»" logout_disabled: "Выход отключён, пока сайт в режиме «только для чтения»" - too_few_topics_and_posts_notice: "Давайте приступим к обсуждению! Есть %{currentTopics} тем и %{currentPosts} постов. Пользователи должны больше читать и отвечать – мы рекомендуем, по крайней мере %{requiredTopics} тем и %{requiredPosts} постов. Только сотрудники могут видеть это сообщение." - too_few_topics_notice: "Давайте приступим к обсуждению! Есть %{currentTopics} тем. Пользователи должны больше читать и отвечать – мы рекомендуем, по крайней мере %{requiredTopics} тем. Только сотрудники могут видеть это сообщение." - too_few_posts_notice: "Давайте приступим к обсуждению! Есть %{currentPosts} сообщений. Пользователям нужно больше читать и отвечать - мы рекомендуем хотя бы %{requiredPosts} сообщений. Только сотрудники могут видеть это сообщение." + too_few_topics_and_posts_notice_MF: >- + Давайте приступим к обсуждению! Есть {currentTopics, plural, one {# тема} few {# темы} other {# тем}} и {currentPosts, plural, one {# сообщение} few {# сообщения} other {# сообщений}}. Пользователи должны больше читать и отвечать – мы рекомендуем, по крайней мере {requiredTopics, plural, one {# тему} few {# темы} other {# тем}} и {requiredPosts, plural, one {# сообщение} few {# сообщения} other {# сообщений}}. Только сотрудники могут видеть это сообщение. + too_few_topics_notice_MF: >- + Давайте приступим к обсуждению! Есть {currentTopics, plural, one {# тема} few {# темы} other {# тем}}. Пользователи должны больше читать и отвечать – мы рекомендуем, по крайней мере {requiredTopics, plural, one {# тему} few {# темы} other {# тем}}. Только сотрудники могут видеть это сообщение. + too_few_posts_notice_MF: >- + Давайте приступим к обсуждению! Есть {currentPosts, plural, one {# сообщение} few {# сообщения} other {# сообщений}}. Пользователи должны больше читать и отвечать – мы рекомендуем, по крайней мере {requiredPosts, plural, one {# сообщение} few {# сообщения} other {# сообщений}}. Только сотрудники могут видеть это сообщение. logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {# error/hour} или {# errors/hour}} достигнут предел настройки сайта {limit, plural, one {# error/hour} или {# errors/hour}}." reached_minute_MF: "{relativeAge}{rate, plural, one {# error/minute} или {# errors/minute}} достигнут предел настройки сайта {limit, plural, one {# error/minute} или {# errors/minute}}." @@ -2055,7 +2058,7 @@ ru: group_join: "Вам нужно присоединиться к группе `{{name}}` чтобы увидеть эту тему." group_request_sent: "Ваш запрос на членство в группе был отправлен. Вам сообщат, когда будет одобрено." unread_indicator: "Никто еще не дочитал до конца этой темы." - read_more_MF: "У вас осталось { UNREAD, plural, =0 {} one { 1 непрочитанная } other { # непрочитанных } } { NEW, plural, =0 {} one { {BOTH, select, true{и } false { } other{}} 1 новая тема} other { {BOTH, select, true{и } false { } other{}} # новых тем} }, вы также можете {CATEGORY, select, true {посмотреть другие темы в разделе {catLink}} false {{latestLink}} other {}}" + read_more_MF: "У вас осталось { UNREAD, plural, =0 {} one { 1 непрочитанная } few { 1 непрочитанные } other { # непрочитанных } } { NEW, plural, =0 {} one { {BOTH, select, true{и } false { } other{}} 1 новая тема} other { {BOTH, select, true{и } false { } other{}} # новых тем} }, вы также можете {CATEGORY, select, true {посмотреть другие темы в разделе {catLink}} false {{latestLink}} other {}}" browse_all_categories: Просмотреть все разделы view_latest_topics: посмотреть последние темы suggest_create_topic: "Почему бы вам не создать новую тему?" @@ -2615,7 +2618,7 @@ ru: can: "может… " none: "(вне раздела)" all: "Все разделы" - choose: "разделе…" + choose: "Выберите раздел…" edit: "Изменить" edit_dialog_title: "Редактировать: %{categoryName}" view: "Просмотр тем по разделам" @@ -2747,7 +2750,7 @@ ru: notify_action: "Сообщение" official_warning: "Официальное предупреждение" delete_spammer: "Удалить спамера" - delete_confirm_MF: "Вы собираетесь удалить {POSTS, plural, one {1 сообщение} other {# сообщений}} и {TOPICS, plural, one {1 тему} other {# темы}} этого пользователя, а так же удалить его учётную запись, добавить его IP-адрес {ip_address} и его почтовый адрес {email} в чёрный список. Вы действительно действительно ваши помыслы чисты и действия не продиктованы гневом?" + delete_confirm_MF: "Вы собираетесь удалить {POSTS, plural, one {1 сообщение} few {# сообщения} other {# сообщений}} и {TOPICS, plural, one {1 тему} few {# темы} other {# тем}} этого пользователя, а так же удалить его учётную запись, добавить его IP-адрес {ip_address} и его почтовый адрес {email} в чёрный список. Вы действительно действительно ваши помыслы чисты и действия не продиктованы гневом?" yes_delete_spammer: "Да, удалить спамера" ip_address_missing: "(не доступно)" hidden_email_address: "(скрыто)" @@ -2822,10 +2825,11 @@ ru: help: "Тема исключена из всех списков тем и доступна только по прямой ссылке" personal_message: title: "Эта тема является личным сообщением" + help: "Эта тема является личным сообщением" posts: "Сообщ." posts_long: "{{number}} сообщений в теме" posts_likes_MF: | - В этой теме {count, plural, one {1 сообщение} other {# сообщений}} {ratio, select, + В этой теме {count, plural, one {1 сообщение} few {# сообщения} other {# сообщений}} {ratio, select, low {с высоким рейтингом симпатий} med {с очень высоким рейтингом симпатий} high {с чрезвычайно высоким рейтингом симпатий} @@ -4154,10 +4158,10 @@ ru: clear_penalty_history: title: "Очистить историю штрафов" description: "пользователи со штрафами не могут достичь TL3" - delete_all_posts_confirm_MF: "Вы собираетесь удалить {POSTS, plural, one {1 сообщение} other {# сообщений}} и {TOPICS, plural, one {1 тему} other {# тем}}. Вы уверены?" + delete_all_posts_confirm_MF: "Вы собираетесь удалить {POSTS, plural, one {1 сообщение} few {# сообщения} other {# сообщений}} и {TOPICS, plural, one {1 тему} few {# темы} other {# тем}}. Вы уверены?" silence: "Заблокировать" unsilence: "Разблокировать" - silenced: "Заброкирован?" + silenced: "Заблокирован?" moderator: "Модератор?" admin: "Администратор?" suspended: "Заморожен?" diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml index 51e69cbe17..3c84ec4ce6 100644 --- a/config/locales/client.sl.yml +++ b/config/locales/client.sl.yml @@ -2631,6 +2631,7 @@ sl: help: "Ta tema je izločena; ne bo se prikazovala na seznamih tem in se jo lahko dostopa samo preko neposredne povezave." personal_message: title: "Ta tema je zasebno sporočilo" + help: "Ta tema je zasebno sporočilo" posts: "Prispevki" posts_long: "{{number}} prispevkov v tej temi" posts_likes_MF: | diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index b58f1d3327..268b3d9cb1 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -796,7 +796,7 @@ sv: allow_private_messages: "Tillåt andra användare att skicka mig personliga meddelanden" external_links_in_new_tab: "Öppna alla externa länkar i en ny flik" enable_quoting: "Aktivera citatsvar för markerad text" - enable_defer: "Aktivera uppskjutning för att markera ämnen som lästa" + enable_defer: "Aktivera fördröjning för att markera ämnen som lästa" change: "ändra" featured_topic: "Utvalt Ämne" moderator: "{{user}} är en moderator" @@ -811,10 +811,9 @@ sv: email_activity_summary: "Aktivitetssammanfattning" mailing_list_mode: label: "Utskicksläge" - enabled: "Aktivera utskicksläge" + enabled: "Aktivera mottagning av e-post för varje nytt inlägg" instructions: | - Den här inställningen åsidosätter aktivitetssummeringen.
- Tystade ämnen och kategorier är inte inkluderade i de här e-postmeddelandena. + Den här inställningen åsidosätter aktivitetssummeringen.
Tystade ämnen och kategorier är inte inkluderade i de här e-postmeddelandena. individual: "Skicka ett e-postmeddelande för varje nytt inlägg." individual_no_echo: "Jag vill ha ett mail när nya poster publiceras" many_per_day: "Skicka ett e-postmeddelande för varje nytt inlägg (ungefär {{dailyEmailEstimate}} per dag)" @@ -851,7 +850,7 @@ sv: ignored_users: "Ignorerade" ignored_users_instructions: "Tysta alla inlägg och notifieringar från dessa användare." tracked_topics_link: "Visa" - automatically_unpin_topics: "Avklistra automatiskt ämnen när jag når botten." + automatically_unpin_topics: "Avklistra automatiskt ämnen när jag når botten" apps: "Appar" revoke_access: "Återkalla åtkomst" undo_revoke_access: "Ångra återkallelse av åtkomst" @@ -1262,9 +1261,12 @@ sv: enabled: "Webbplatsen är i skrivskyddat läge. Du kan fortsätta bläddra på sidan, men att skriva inlägg, gilla och andra interaktioner är inaktiverade för tillfället." login_disabled: "Det går inte att logga in medan siten är i skrivskyddat läge." logout_disabled: "Det går inte att logga ut medan webbplatsen är i skrivskyddat läge. " - too_few_topics_and_posts_notice: "Låt oss påbörja diskussionen! Det finns %{currentTopics} ämnen och %{currentPosts} inlägg. Besökare behöver mer att läsa och svara på – vi rekommenderar åtminstone %{requiredTopics} ämnen och %{requiredPosts} inlägg. Enbart personal kan se detta meddelande." - too_few_topics_notice: "Låt oss påbörja diskussionen! Det finns %{currentTopics} ämnen. Besökare behöver mer att läsa och svara på – vi rekommenderar åtminstone %{requiredTopics} ämnen. Enbart personal kan se detta meddelande." - too_few_posts_notice: "Låt oss påbörja diskussionen! Det finns %{currentPosts} inlägg. Besökare behöver mer att läsa och svara på – vi rekommenderar åtminstone %{requiredPosts} inlägg. Enbart personal kan se detta meddelande." + too_few_topics_and_posts_notice_MF: >- + Låt oss påbörja diskussionen! Det {currentTopics, plural, one {finns # ämne} other {finns # ämnen}} och {currentPosts, plural, one {# inlägg} other {# inlägg}}. Besökare behöver mer att läsa och svara på – vi rekommenderar åtminstone {requiredTopics, plural, one {# ämne} other {# ämnen}} och {requiredPosts, plural, one {# inlägg} other {# inlägg}}. Enbart personal kan se detta meddelande.  + too_few_topics_notice_MF: >- + Låt oss påbörja diskussionen! Det {currentTopics, plural, one {finns # ämne} other {finns # ämnen}}. Besökare behöver mer att läsa och svara på – vi rekommenderar åtminstone {requiredTopics, plural, one {# ämne} other {# ämnen}}. Enbart personal kan se detta meddelande.  + too_few_posts_notice_MF: >- + Låt oss påbörja diskussionen! Det {currentTopics, plural, one {finns # inlägg} other {finns # inlägg}}. Besökare behöver mer att läsa och svara på – vi rekommenderar åtminstone {requiredTopics, plural, one {# inlägg} other {# inlägg}}. Enbart personal kan se detta meddelande.  logs_error_rate_notice: reached_hour_MF: "{relativeAge} - {rate, plural, one {# error/hour} other {# errors/hour}} har uppnått webbplatsinställningarnas gräns på {limit, plural, one {# error/hour} other {# errors/hour}}." reached_minute_MF: "{relativeAge}{rate, plural, one {# error/minute} other {# errors/minute}} har uppnått weplatsinställningarnas gräns på {limit, plural, one {# error/minute} other {# errors/minute}}." @@ -2627,6 +2629,7 @@ sv: help: "Det här ämnet är olistat; det kommer inte visas i ämneslistorna och kan bara nås via en direktlänk" personal_message: title: "Detta ämne är ett personligt meddelande" + help: "Detta ämne är ett personligt meddelande" posts: "Inlägg" posts_long: "det finns {{number}} inlägg i detta ämne" posts_likes_MF: | diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 7f9f612a6e..90724174ad 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -1258,9 +1258,6 @@ tr_TR: enabled: "Bu site salt-okunur modda. Lütfen taramaya devam et, ancak yanıtlama, beğenme ve diğer eylemler şu an için devre dışı durumda. " login_disabled: "Site salt-okunur modda iken giriş işlemi devre dışı bırakılır ." logout_disabled: "Site salt-okunur modda iken çıkış işlemi yapılamaz." - too_few_topics_and_posts_notice: "Haydi tartışma başlasın!Burada %{currentTopics} konu ve %{currentPosts} gönderi var. Ziyaretçilerin okuması ve yanıtlaması için tavsiye ettiğimiz en az %{requiredTopics} konu ve %{requiredPosts} gönderi. Bu mesajı yalnızca yetkili görebilir." - too_few_topics_notice: "Haydi tartışma başlasın!Burada %{currentTopics} konu var. Ziyaretçilerin okuması ve yanıtlaması için tavsiye ettiğimiz en az %{requiredTopics} konudur. Bu mesajı yalnızca yetkili görebilir." - too_few_posts_notice: "Haydi tartışma başlasın!Burada %{currentPosts} gönderi var. Ziyaretçilerin okuması ve yanıtlaması için tavsiye ettiğimiz en az %{requiredPosts} gönderidir. Bu iletiyi yalnızca yetkili görebilir." logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {# hata/saat} diğeri {# hata/saat}} site limitlerine ulaştı {limit, plural, one {# hata/saat} other {# hata/saat}}." reached_minute_MF: "{relativeAge}{rate, plural, one {# hata/saat} {# hata/dakika}} site limitlerine ulaştı {limit, plural, one {# hata/dakika} other {# hata/dakika}}." @@ -2617,6 +2614,7 @@ tr_TR: help: "Bu konu listelenmemiş; konu listelerinde görüntülenmeyecek ve sadece doğrudan bir bağlantı üzerinden erişilebilecek" personal_message: title: "Bu konu kişisel bir mesajdır" + help: "Bu konu kişisel bir mesajdır" posts: "Gönderiler" posts_long: "bu konuda {{number}} gönderi var" posts_likes_MF: | diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 98f2e06b18..2ffafbd745 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -1361,9 +1361,6 @@ uk: enabled: "Сайт працює в режимі \"тільки для читання\". Зараз ви можете продовжувати переглядати сайт, але інші дії будуть недоступні. " login_disabled: "Вхід вимкнено, поки сайт перебуває в режимі лише для читання." logout_disabled: "Вихід відключений, поки сайт в режимі «тільки для читання»" - too_few_topics_and_posts_notice: "Давайте почнемо обговорення! Есть %{currentTopics} тем та %{currentPosts} постів. Користувачі повинні більше читати та відповідати - ми рекомендуємо, принаймні %{requiredTopics} тем та %{requiredPosts} постів. Тільки співробітники можуть бачити це повідомлення." - too_few_topics_notice: "Давайте почнемо обговорення! Есть %{currentTopics} тем. Користувачі повинні більше читати та відповідати - ми рекомендуємо, принаймні %{requiredTopics} тем. Тільки співробітники можуть бачити це повідомлення." - too_few_posts_notice: "Давайте почнемо обговорення! Есть %{currentPosts} постов. Користувачам потрібно більше читати та відповідати - ми рекомендуємо хоча б %{requiredPosts} постів. Тільки співробітники можуть бачити це повідомлення." logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {# error/hour} або {# errors/hour}} досягнуто максимально дозволене значення {limit, plural, one {# error/hour} або {# errors/hour}}." reached_minute_MF: "{relativeAge}{rate, plural, one {# error/minute} або {# errors/minute}} досягнуто максимально дозволене значення {limit, plural, one {# error/minute} або {# errors/minute}}." @@ -2822,6 +2819,7 @@ uk: help: "Тема виключена з усіх списків тем та доступна тільки за прямим посиланням" personal_message: title: "Ця тема є особистим повідомленням" + help: "Ця тема є особистим повідомленням" posts: "Дописи" posts_long: "тема містить {{number}} дописів" posts_likes_MF: | diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 4f19680a0f..8187555885 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -1216,9 +1216,6 @@ ur: enabled: "یہ سائٹ صرف پڑھنے کے مَوڈ میں ہے۔ براہِ مہربانی براؤز کرتے رہئیے، لیکن جواب دینا، لائکس دینا، اور دیگر اعمال ابھی کے لئے غیر فعال ہیں۔" login_disabled: "جب تک سائٹ صرف پڑھنے کے مَوڈ میں ہے لاگ اِن غیر فعال رہے گا۔" logout_disabled: "جب تک سائٹ صرف پڑھنے کے مَوڈ میں ہے لاگ آؤٹ غیر فعال رہے گا۔" - too_few_topics_and_posts_notice: "چلیں اِس بحث کو شروع کریں! %{currentTopics} ٹاپکس اور %{currentPosts} پوسٹس موجود ہیں۔ زائرین کو پڑھنے اور اُن کا جواب دینے کیلئے مذید اور کی ضرورت ہے – ہم کم از کم %{requiredTopics} ٹاپکس اور %{requiredPosts} پوسٹس کی تجویز دیتے ہیں۔ صرف سٹاف اِس پیغام کو دیکھ سکتے ہیں۔" - too_few_topics_notice: "چلیں اِس بحث کو شروع کریں! %{currentTopics} ٹاپکس موجود ہیں۔ زائرین کو پڑھنے اور اُن کا جواب دینے کیلئے مذید اور کی ضرورت ہے – ہم کم از کم %{requiredTopics} ٹاپکس کی تجویز دیتے ہیں۔ صرف سٹاف اِس پیغام کو دیکھ سکتے ہیں۔" - too_few_posts_notice: "چلیں اِس بحث کو شروع کریں! %{currentPosts} پوسٹس موجود ہیں۔ زائرین کو پڑھنے اور اُن کا جواب دینے کیلئے مذید اور کی ضرورت ہے – ہم کم از کم %{requiredPosts} پوسٹس کی تجویز دیتے ہیں۔ صرف سٹاف اِس پیغام کو دیکھ سکتے ہیں۔" learn_more: "اورجانیے..." all_time: "کُل" all_time_desc: "کُل ٹاپک بنائے گئے" @@ -2526,6 +2523,7 @@ ur: help: "یہ ٹاپک غیر کسی فہرست میں نہیں ہے؛ یہ ٹاپکس کی فہرست میں ظاہر نہیں ہو گا، اور صرف ایک براہ راست لنک کے ذریعے اِس تک رسائی ہو سکے گی" personal_message: title: "یہ ٹاپک ایک ذاتی پیغام ہے" + help: "یہ ٹاپک ایک ذاتی پیغام ہے" posts: "پوسٹس" posts_long: "اِس ٹاپک میں {{number}} پوسٹس ہیں" posts_likes_MF: | diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 173236d6fd..f69a16b7d1 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -1073,6 +1073,7 @@ vi: no_links: "Không có liên kết" most_liked_by: "Được thích nhiều nhất bởi" most_liked_users: "Like nhiều nhất" + no_likes: "Chưa có lượt thích." topics: "Chủ đề" replies: "Trả lời" ip_address: @@ -1381,6 +1382,7 @@ vi: posted: '{{username}} gửi bài trong "{{topic}}" - {{site_title}}' linked: '{{username}} liên quan đến bài viết của bạn từ "{{topic}}" - {{site_title}}' titles: + liked: "lượt thích mới" watching_first_post: "chủ đề mới" upload_selector: title: "Thêm một ảnh" @@ -1402,7 +1404,7 @@ vi: latest_post: "Bài viết mới nhất" latest_topic: "Chủ đề mới" most_viewed: "Xem nhiều nhất" - most_liked: "Like nhiều nhất" + most_liked: "Thích nhiều nhất" select_all: "Chọn tất cả" clear_all: "Xóa tất cả" too_short: "Từ khoá tìm kiếm của bạn quá ngắn." @@ -1797,7 +1799,7 @@ vi: controls: reply: "bắt đầu soản trả lời cho bài viết này" like: "like bài viết này" - has_liked: "bạn đã like bài viết này" + has_liked: "bạn đã thích bài viết này" undo_like: "hủy like" edit: "sửa bài viết này" edit_action: "Sửa" @@ -1834,6 +1836,10 @@ vi: notify_moderators: "đã thông báo với BQT" notify_user: "đã gửi tin nhắn" bookmark: "đã đánh dấu bài này" + like: + other: "thích này" + like_capped: + other: "và {{count}} người khác thích nầy" by_you: off_topic: "Bạn đã đánh dấu cái nfay là chủ đề đóng" spam: "Bạn đã đánh dấu cái này là rác" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index fa59219490..7ad3ead0be 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -1211,9 +1211,6 @@ zh_CN: enabled: "站点正处于只读模式。你可以继续浏览,但是回复、赞和其他操作暂时被禁用。" login_disabled: "只读模式下不允许登录。" logout_disabled: "站点在只读模式下无法登出。" - too_few_topics_and_posts_notice: "让我们开始讨论吧!现有%{currentTopics}个主题何%{currentPosts}个回帖。用户需要更多阅读与回复 —— 我们推荐至少%{requiredTopics}个主题和%{requiredPosts}个帖子。此消息仅管理人员可见。" - too_few_topics_notice: "让我们开始讨论吧!现在有%{currentTopics}个主题。 用户需要进行更多阅读与回复 – 我们推荐至少%{requiredTopics} 个主题。 此消息仅管理员可见。" - too_few_posts_notice: "让我们开始讨论吧!现在有%{currentPosts}个主题。 用户需要进行更多的阅读或回复 – 我们推荐至少%{requiredPosts} 个主题。 此消息仅管理人员可见。" logs_error_rate_notice: reached_hour_MF: "{relativeAge}{rate, plural, one {# error/hour} other {# errors/hour}}达到了站点设置中的限制{limit, plural, one {# error/hour} other {# errors/hour}}。" reached_minute_MF: "{relativeAge}1 – {rate, plural, one {# error/minute} other {# errors/minute}}已经达到站点设置限制 {limit, plural, one {# error/minute} other {# errors/minute}}。" @@ -2528,6 +2525,7 @@ zh_CN: help: "本主题被设置为不显示在主题列表中,只能通过链接来访问" personal_message: title: "此主题是一条私信" + help: "此主题是一条私信" posts: "帖子" posts_long: "本主题有 {{number}} 个帖子" posts_likes_MF: | diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 587b124bc2..7c129f04f2 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -1086,7 +1086,6 @@ ar: desktop_category_page_style: "أسلوب العرض لصفحة الأقسام." category_colors: "قائمة الألوان المسموحة لتمييز الأقسام بترميز الhexadecimal." category_style: "أسلوب العرض لشارة القسم." - max_image_size_kb: "الحجم الاقص لتحميل صوره بالكيلو بايت.هذا يجب ضبطه في nginx (client_max_body_size) /اباتشي او البروكسي" max_attachment_size_kb: "أقصى حجم لتحميل المرفقات الملفات بالكيلوبايت. وهذا يجب أن يتم تكوينه في nginx (client_max_body_size) / أباتشي أو الوكيل. " authorized_extensions: "قائمة بالملفات المسموح رفعها (أستخدم '*' للسماح لجميع الأنواع)" max_similar_results: "عدد المواضيع المشابهة لتُعرض فوق المحرّر عند إنشاء موضوع جديد. المقارنة تعتمد عنوان الموضوع ومتنه." diff --git a/config/locales/server.be.yml b/config/locales/server.be.yml index 355eb61462..ddc531f97a 100644 --- a/config/locales/server.be.yml +++ b/config/locales/server.be.yml @@ -1161,10 +1161,8 @@ be: desktop_category_page_style: "Візуальны стыль для" category_colors: "Спіс шаснаццатковых значэнняў колеру дапускаецца для катэгорый." category_style: "Візуальны стыль для катэгорыі значкоў." - max_image_size_kb: "Максімальны памер загружанага выявы ў кбайт. Гэта павінна быць сканфігуравана ў Nginx (client_max_body_size)" theme_authorized_extensions: "Спіс пашырэнняў файлаў, дазволеных для загрузкі тэмы (выкарыстанне «*», каб ўключыць усе тыпы файлаў)" max_similar_results: "Як шмат падобных да тых, каб паказаць вышэй рэдактараў пры стварэнні новай тэмы. Параўнанне заснавана на назве і цела." - max_image_megapixels: "Максімальную колькасць мегапікселяў, дазволеныя для малюнка." title_prettify: "Прадухіліць агульныя назвы памылак друку і памылкі, у тым ліку вялікіх літар, малыя літары першага сімвал, шматразовы! і ?, за дадатковую плату. у канцы, і г.д." topic_views_heat_low: "Пасля гэтага шмат праглядаў, поле выгляд злёгку падсвятляецца." topic_views_heat_medium: "Пасля гэтага шмат праглядаў, поле праглядаў умерана падсвятляецца." diff --git a/config/locales/server.bg.yml b/config/locales/server.bg.yml index 934881ea2e..c835d6fdc6 100644 --- a/config/locales/server.bg.yml +++ b/config/locales/server.bg.yml @@ -174,6 +174,13 @@ bg: too_late_to_edit: "Това мнение е създадено много отдавна. То вече не може да се променя или да бъде изтрито." revert_version_same: "Текущата версия е същата като версията, към която се опитвате да се върнете." excerpt_image: "изображение" + bookmarks: + reminders: + later_today: "По-късно днес
{{date}}" + next_business_day: "Следващият работен ден
{{date}}" + tomorrow: "Утре
{{date}}" + next_week: "Следващата седмица
{{date}}" + next_month: "Следващият месец
{{date}}" groups: errors: invalid_domain: "'%{domain}' е невалиден домейн." @@ -459,6 +466,7 @@ bg: reports: default: labels: + count: Брой day: Ден post_edits: labels: @@ -469,6 +477,7 @@ bg: user_flagging_ratio: labels: user: Потребител + score: Точки moderators_activity: labels: moderator: Модератор @@ -802,7 +811,6 @@ bg: min_title_similar_length: "Минимална дължина на заглавието на темата, като тя ще бъде проверена за наличие на подобна." category_colors: "Списък от шестнадесетични цветови стойности разрешени за категориите." category_style: "Визуален стил за категорията със значки." - max_image_size_kb: "Максимален размер за качване на избражение в kB. Това също трябва да се конфигурира в nginx конфигурацията (client_max_body_size), apache или прокси сървъра." max_attachment_size_kb: "Максимална големина на прикачените файлове в kB. Това също трябва да се конфигурира в nginx (client_max_body_size), apache или прокси сървъра." authorized_extensions: "Списък от разширения на файлове разрешени за качване (използвайте '*' за да разрешите всички)" max_similar_results: "Брой подобни теми показвани на потребителя при създаването на нова тема. Сравнението е базирано на заглавието и текста на темата." @@ -1352,6 +1360,12 @@ bg: confirm_title: "Включени известявания - %{site_title}" confirm_body: "Успех! Известяването е включено." reviewables: + priorities: + medium: "Среден" + high: "Висок" + sensitivity: + medium: "Среден" + high: "Висок" actions: agree_and_suspend: title: "Преустанови потребител" diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index c382e4a5c1..1d8c0b4e2b 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -151,6 +151,15 @@ bs_BA: latest: "Latest topics" too_late_to_edit: "That post was created too long ago. It can no longer be edited or deleted." excerpt_image: "slika" + bookmarks: + reminders: + at_desktop: "Sljedeći put sam za svojim desktop kompjuterom" + later_today: "Danas, malo kasnije
{{date}}" + next_business_day: "Sljedeći radni dan
{{date}}" + tomorrow: "Sutradan
{{date}}" + next_week: "Sljedeće sedmice
{{date}}" + next_month: "Naredni mjesec
{{date}}" + custom: "Ciljano vrijeme i datum" groups: default_names: everyone: "everyone" @@ -499,12 +508,14 @@ bs_BA: http_5xx_reqs: xaxis: "Day" http_total_reqs: + title: "Suma" xaxis: "Day" time_to_first_response: xaxis: "Day" yaxis: "Prosječno vrijeme (hours)" topics_with_no_response: xaxis: "Day" + yaxis: "Suma" mobile_visits: xaxis: "Day" yaxis: "Number of visits" @@ -652,7 +663,6 @@ bs_BA: body_min_entropy: "The minimum entropy (unique characters, non-english count for more) required for a post body." min_title_similar_length: "The minimum length of a title before it will be checked for similar topics." category_colors: "A list of hexadecimal color values allowed for categories." - max_image_size_kb: "The maximum image upload size in kB. This must be configured in nginx (client_max_body_size) / apache or proxy as well." max_attachment_size_kb: "The maximum attachment files upload size in kB. This must be configured in nginx (client_max_body_size) / apache or proxy as well." authorized_extensions: "A list of file extensions allowed for upload (use '*' to enable all file types)" max_similar_results: "How many similar topics to show above the editor when composing a new topic. Comparison is based on title and body." @@ -738,6 +748,11 @@ bs_BA: autoclosed_disabled: "This topic is now opened. New replies are allowed." autoclosed_disabled_lastpost: "Ova tema je sada otvorena. Novi postovi su dozvoljeni." login: + security_key_description: "Kada ste pripremili vaš fizikalni sigurnosni ključ (physical security key), pritisnite ispod dugme Prijava pomoću sigurnosnog ključa." + security_key_alternative: "Pokušaj na drugi način" + security_key_authenticate: "Prijava pomoću sigurnosnog ključa" + security_key_not_allowed_error: "Ovaj proces prijave pomoću sigurnosnog ključa je ili vremenski istekao ili je odkazan." + security_key_no_matching_credential_error: "Nisu pronađeni korisnički podatci koristeći navedeni sigurnosni ključ." not_approved: "Your account hasn't been approved yet. You will be notified by email when you are ready to log in." incorrect_username_email_or_password: "Netačan nadimak, email ili šifra" wait_approval: "Hvala vam što ste se prijavili. We will notify you when your account has been approved." @@ -941,6 +956,7 @@ bs_BA: title: "Oznake" finish_installation: register: + button: "Registriraj" help: "Registriraj novi račun kako bi počeo" resend_email: title: "Pošalji ponovo email aktivacije" diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml index e9af238012..2cf54be443 100644 --- a/config/locales/server.ca.yml +++ b/config/locales/server.ca.yml @@ -1608,13 +1608,11 @@ ca: desktop_category_page_style: "Estil visual per a la pàgina /categories." category_colors: "Llista de valors de color hexadecimal permesos per a categories." category_style: "Estil visual per a insígnies de categoria." - max_image_size_kb: "Mida màxima de càrrega d'imatge en kB. Cal configurar-ho en nginx (client_max_body_size) / apache o en el proxy també." max_attachment_size_kb: "Mida màxima de càrrega de fitxers adjunts en kB. Cal configurar-ho en nginx (client_max_body_size) / apache o en el proxy també." authorized_extensions: "Llista d'extensions de fitxer permeses per a carregar (feu servir '*' per a habilitar tota mena de fitxers)" authorized_extensions_for_staff: "Llista d'extensions permeses als usuaris de l'equip responsable en carregar fitxers a més de la llista definida en el paràmetre `authorized_extensions` de la configuració. (Feu servir \"*\" per a habilitar tots els tipus de fitxers.)" theme_authorized_extensions: "Llista d'extensions de fitxers permeses per a càrregues d'aparences (utilitzeu '*' per a permetre tots els tipus de fitxers)" max_similar_results: "Quants temes semblants es mostren per damunt de l'editor quan es redacta un tema nou. La comparació es basa en el títol i el cos." - max_image_megapixels: "Nombre màxim de megapíxels permesos en una imatge." title_prettify: "Evita errades tipogràfiques freqüents en el títol, incloent-hi tot en majúscula, primer caràcter en minúscula, exclamacions i interrogants múltiples, punts extres al final, etc." title_remove_extraneous_space: "Elimina els espais blancs de davant de la puntuació final." automatic_topic_heat_values: 'Actualitza automàticament els paràmetres de "topic views heat" i "topic post like heat" basant-se en l''activitat del lloc web. ' diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 23d4e29b32..e463bdcec1 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -1625,13 +1625,11 @@ de: desktop_category_page_style: "Visueller Stil für die /categories Seite" category_colors: "Liste hexadezimaler Farbwerte, die als Kategoriefarben erlaubt sind." category_style: "Visueller Stil für Kategorie-Abzeichen." - max_image_size_kb: "Maximale Größe eines hochgeladenen Bilds in kB. Dieser Wert muss auch in Nginx (client_max_body_size), Apache oder anderen Proxies entsprechend konfiguriert werden." max_attachment_size_kb: "Maximale Größe hochgeladener Dateianhänge in kB. Dieser Wert muss auch in Nginx (client_max_body_size), Apache oder anderen Proxies entsprechend konfiguriert werden." authorized_extensions: "Liste von erlaubten Dateiendungen für hochgeladene Dateien ('*' um alle Dateiendungen zu erlauben)" authorized_extensions_for_staff: "Eine Liste von erlaubten Dateiendungen für den Upload von Dateien durch Team-Benutzer, zusätzlich zu der in der Seiteninstellung `authorized_extensions` definierten Liste. (Der Wert „*“ erlaubt alle Dateitypen.)" theme_authorized_extensions: "Liste von erlaubten Dateiendungen für hochgeladene Themes (verwende '*', um alle Dateitypen zu erlauben)" max_similar_results: "Anzahl ähnlicher Themen, die beim Erstellen eines neuen Themas über dem Editor angezeigt werden. Ähnlichkeit wird an Hand des Titels und Inhalts bestimmt." - max_image_megapixels: "Maximale erlaubte Auflösung für Bilder (in Megapixeln)." title_prettify: "Verhindert gängige Fehler im Titel, wie reine Grossschreibung, Kleinbuchstaben am Anfang, mehrere ! und ?, überflüssiger . am Ende, etc." title_remove_extraneous_space: "Entferne führende Leerzeichen vor dem Ende-Zeichen." automatic_topic_heat_values: 'Update die "Themenansicht-Erhitzung" und "Themenbeitrags ''Gefällt mir'' Erhitzung" Einstellungen automatisch basierend auf der Seiten-Aktivität.' @@ -1856,7 +1854,7 @@ de: default_categories_tracking: "Liste der standardmäßig gefolgten Kategorien." default_categories_muted: "Liste der standardmäßig stummgeschalteten Kategorien." default_categories_watching_first_post: "Liste von Kategorien, in denen der erste Beiträge in jedem neuen Thema automatisch beobachtet wird." - mute_all_categories_by_default: "Setze die Standard E-Mail Benachrichtigungs-Stufe für alle Kategorien auf stummgeschaltet. Benötigt die Zustimmung der Benutzer, damit sie in \"neueste\" und Kategorien-Seiten erscheinen. Um den Standard für anonyme Benutzer anzupassen, müssem 'default_categories_' Einstellungen gesetzt werden." + mute_all_categories_by_default: "Setze die Standard E-Mail Benachrichtigungs-Stufe für alle Kategorien auf stummgeschaltet. Benötigt die Zustimmung der Benutzer, damit sie in \"neueste\" und Kategorien-Seiten erscheinen. Um den Standard für anonyme Benutzer anzupassen, müssen 'default_categories_' Einstellungen gesetzt werden." default_tags_watching: "Liste der Tags, die standardmäßig beobachtet werden." default_tags_tracking: "Liste der Tags, die standardmäßig verfolgt werden." default_tags_muted: "Liste der Tags, die standardmäßig stummgeschaltet werden." @@ -3036,9 +3034,19 @@ de: confirm_new_email: title: "E-Mail-Adresse bestätigen (an neue)" subject_template: "[%{email_prefix}] Bestätige deine neue E-Mail-Adresse" + text_body_template: | + Bestätige bitte deine neue E-Mail Adresse für %{site_name} durch einen Klick auf den folgenden Link: + + %{base_url}/u/confirm-new-email/%{email_token} confirm_old_email: title: "E-Mail-Adresse bestätigen (an alte)" subject_template: "[%{email_prefix}] Bestätige deine aktuelle E-Mail-Adresse" + text_body_template: | + Bevor wir die E-Mail Adresse ändern können, benötigen wir deine Bestätigung, dass du den aktuellen E-Mail Account kontrollierst. Sobald du das getan hast, schicken wir dir eine Anfrage, die neue E-Mail Adresse zu bestätigen. + + Bestätige deine aktuelle E-Mail Adresse für %{site_name}, indem du auf den folgenden Link klickst: + + %{base_url}/u/confirm-old-email/%{email_token} notify_old_email: title: "Benachrichtigung an alte E-Mail-Adresse" subject_template: "[%{email_prefix}] Deine E-Mail-Adresse wurde geändert" diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index 41f1446c85..8263d27680 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -1065,12 +1065,10 @@ el: desktop_category_page_style: "Οπτικό στυλ για τη σελίδα /Categories." category_colors: "Μια λίστα με τιμές δεκαεξαδικού χρώματος, που επιτρέπεται για τις κατηγορίες." category_style: "Οπτικό στυλ για τα εμβλημάτα κατηγορίας ." - max_image_size_kb: "Το μέγιστο μέγεθος εικόνας για μεταφόρτωση σε kB. Αυτό πρέπει να ρυθμιστεί στο nginx (client_max_body_size) / apache ή και στο proxy." max_attachment_size_kb: "Το μέγιστο μέγεθος συννημένων αρχείων για μεταφόρτωση σε kB. Αυτό πρέπει να ρυθμιστεί στο nginx (client_max_body_size) / apache ή και στο proxy." authorized_extensions: "Μια λίστα με επεκτάσεις αρχείου οι οποιες επιτρέπονται για ανέβασμα (use '*' to enable all file types)" theme_authorized_extensions: "Μια λίστα από επεκτάσεις οι οποίες επιτρέπονται για τις μεταφορτώσεις θεμάτων ('*' για ενεργοποίηση όλων των τύπων αρχείων)" max_similar_results: "Πόσα παρόμοια νήματα να εμφανίζονται πάνω από τον εκδότη, όταν δημιουργείς ένα νέο νήμα. Η σύγκριση βασίζεται στον τίτλο και το σώμα." - max_image_megapixels: "Ο μέγιστος αριθμός megapixel που επιτρέπεται σε μία εικόνα. " title_prettify: "Απέφυγε συνηθισμένα τυπογραφικά λάθη στους τίτλους, όπως όλα τα γράμματα να είναι κεφαλαία, το πρώτο γράμμα της λέξης να είναι μικρό, πολλαπλά ! και ;, επιπλέον . στο τελος, κτλ. " topic_views_heat_low: "Μετά από τόσες προβολές, το πεδίο των προβολών είναι ελάχιστα τονισμένο. " topic_views_heat_medium: "Μετά από τόσες προβολές, το πεδίο των προβολών είναι μέτρια τονισμένο. " diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 00e24df01c..c4db42fb1c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1175,6 +1175,7 @@ en: labels: level: Level description: "Number of users grouped by trust level." + description_link: "https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/" users_by_type: title: "Users per Type" xaxis: "Type" diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index b6df9f32bc..a8d10b3571 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -223,7 +223,7 @@ es: request_membership: "Solicitar membresía" join_group: "Unirse al grupo" deleted_topic: "¡Ups! Este tema se ha eliminado y ya no está disponible." - delete_topic_failed: "Se produjo un error al borrar el tema. Por favor, contacta al administrador del sitio." + delete_topic_failed: "Se ha producido un error al borrar el tema. Por favor, contacta al administrador del sitio." reading_time: "Tiempo de lectura" likes: "Me gusta" too_many_replies: @@ -1645,13 +1645,11 @@ es: desktop_category_page_style: "Estilo visual de la página de /categorías." category_colors: "Una lista de valores en hexadecimal de los colores para las categorías." category_style: "Estilo visual de las etiquetas de categoría." - max_image_size_kb: "El tamaño máximo, en kB, de las imágenes que se pueden subir. Debe ser configurado también en nginx (client_max_body_size) / apache o proxy." max_attachment_size_kb: "El tamaño máximo, en kB, de los archivos que se pueden adjuntar. Debe ser configurado también en nginx (client_max_body_size) / apache o proxy." authorized_extensions: "Una lista de las extensiones de archivo que se permite subir (usa «*» para habilitar todos los tipos de archivo)" authorized_extensions_for_staff: "Una lista de las extensiones de archivo que se le permite subir a miembros del staff sumada a la lista definida en la configuración `authorized_extensions`. (Usa «*» para habilitar todos los tipos)" theme_authorized_extensions: "Una lista de las extensiones de archivo que se permite subir (usa «*» para habilitar todos los tipos)" max_similar_results: "Cantidad de temas similares que se muestran encima del editor cuando se está creando un nuevo tema. La comparación se basa en el título y el cuerpo del tema." - max_image_megapixels: "Número máximo de megapíxeles permitidos en una imagen." title_prettify: "Prevenir errores comunes en el título, incluidos los títulos escritos todo mayúsculas, con la primera letra en minúscula, con múltiples signos ! o ?, . adicional al final, etc." title_remove_extraneous_space: "Eliminar los espacios en blanco delante de la puntuación final." automatic_topic_heat_values: 'Actualizar automáticamente la configuración de «temperatura de vistas del tema» y «temperatura de me gusta de publicaciones del tema» según la actividad del sitio.' diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index be10625aab..fec91a39a9 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -219,6 +219,15 @@ fa_IR: too_late_to_edit: "آن فرسته خیلی وقت پیش ساخته شده. دیگر امکان ویرایش یا حذف آن وجود ندارد." revert_version_same: "نسخه فعلی همان نسخه‌ای است که می‌خواهید به آن برگردید." excerpt_image: "تصویر" + bookmarks: + reminders: + at_desktop: "بار آینده من پشت میزم هستم" + later_today: "امروز کمی بعد
{{date}}" + next_business_day: "روز کاری آینده
{{date}}" + tomorrow: "فردا
{{date}}" + next_week: "هفته ی آینده
{{date}}" + next_month: "ماه آینده
{{date}}" + custom: "درج تاریخ و ساعت" groups: errors: can_not_modify_automatic: "نمی‌توانید گروه‌های خودکار را تغییر دهید." @@ -1048,12 +1057,10 @@ fa_IR: desktop_category_page_style: "سبک بصری برای صفحه‌ی دسته‌بندی" category_colors: "لیست مقادیر رنگ هگزا دسیمال مجاز برای دسته‌بندی‌ها." category_style: "سبک بصری برای نشان دسته‌بندی" - max_image_size_kb: "حداکثر سایز عکس بارگزاری شده با واحد کیلوبایت. این باید در پیکربندی nginx باشد (client_max_body_size) / همچنین apache یا proxy." max_attachment_size_kb: "حداکثر سایز فایل پیوست شده به واحد کیلوبایت. این باید در پیکربندی nginx باشد (client_max_body_size) / همچنین apache یا proxy." authorized_extensions: "لیست پسوند‌های مجاز فایل برای بارگذاری ( از '*' برای اجازه به تمام فایل‌ها استفاده کنید)" theme_authorized_extensions: "لیست پسوند‌های قابل بارگذاری برای قالب (از '*' برای اجازه به تمامی فایل‌ها استفاده کنید)" max_similar_results: "چند موضوع مشابه زیر ویرایشگر وقتی که موضوع جدید ایجاد می شود، نمایش داده شود. مقایسه بر اساس عنوان و متن است. " - max_image_megapixels: "حداکثر اندازه تصاویر با واحد مگاپیکسل." title_prettify: "از غلط های املایی و اشتباهات رایج جلوگیری کن٬ از جمله همه حروف بزرگ٬‌ حرف کوچک اولین کاراکتر، ! و ؟ چندگانه . اضافه در پایان و غیره." topic_views_heat_low: "بعد از این تعداد بازدید، فیلد تعداد بازدید بسیار پر‌رنگ می‌شود." topic_views_heat_medium: "بعد از این تعداد بازدید، فیلد تعداد بازدید پر‌رنگ می‌شود." diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 41032c4de6..cc70ad1c0b 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -1635,13 +1635,11 @@ fi: desktop_category_page_style: "/Keskustelualueet-sivun visuaalinen tyyli." category_colors: "Lista alueiden sallituista väriarvoista, heksadesimaaleina." category_style: "Aluemerkin tyyli." - max_image_size_kb: "Liitetyn kuvan suurin sallittu koko kilotavuissa. Tämä pitää asettaa myös nginxin (client_max_body_size) / apachen tai proxyn asetuksista." max_attachment_size_kb: "Liitetyn tiedoston suurin sallittu koko kilotavuissa. Tämä pitää asettaa myös nginxin (client_max_body_size) / apachen tai proxyn asetuksista." authorized_extensions: "Liitetiedostojen sallitut tiedostopäätteet (käytä '*' salliaksesi kaikki tiedostotyypit)" authorized_extensions_for_staff: "Luettelo tiedostopäätteistä, jotka ovat sallittuja henkilökunnan jäsenille niiden lisäksi, jotka on määritelty sivustoasetuksella \"authorized_extensions\". (salli asettamalla * kaikki tiedostotyypit)" theme_authorized_extensions: "Teemalatausten liitetiedostojen sallitut tiedostopäätteet (käytä '*' salliaksesi kaikki tiedostotyypit)" max_similar_results: "Kuinka monta samankaltaista ketjua näytetään viestikentän päällä uutta ketjua aloitettaessa. Vertailu perustuu sekä otsikkoon että leipätekstiin." - max_image_megapixels: "Kuvan enimmäiskoko megapikseleinä." title_prettify: "Estä yleiset kirjoitusvirheet otsikossa, kuten pelkät isot kirjaimet, pieni ensimmäinen kirjain, useat !- ja ?-merkit ym." title_remove_extraneous_space: "Poista lopettavia välimerkkejä edeltävät tyhjät merkit." automatic_topic_heat_values: 'Päivitä "topic views heat" ja "topic post like heat" -asetuksia sivuston aktiivisuuden perusteella automaattisesti.' diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index ed0ecfc0ea..ed4897d43e 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -1617,13 +1617,11 @@ fr: desktop_category_page_style: "Style visuel de la page /categories." category_colors: "Une liste de couleurs en hexadécimale autorisées pour les catégories." category_style: "Style visuel pour les badges de catégorie." - max_image_size_kb: "La taille maximale des images en Ko. Doit être configuré dans nginx (client_max_body_size) / apache ou proxy aussi." max_attachment_size_kb: "La taille maximale des fichiers envoyés en Ko. Doit être configurer dans nginx (client_max_body_size) / apache ou proxy aussi." authorized_extensions: "Une liste d'extensions de fichier autorisées pour les envois sur le serveur (mettre '*' pour autoriser tous les types)" authorized_extensions_for_staff: "Une liste des extensions de fichiers autorisées pour le téléchargement pour les responsables en plus de la liste définie dans le paramètre `authorized_extensions'. (utilisez '*' pour activer tous les types de fichiers)" theme_authorized_extensions: "Une liste d'extensions de fichier autorisées pour les envois de thème (mettre « * » pour autoriser tous les types de fichier)" max_similar_results: "Combien de sujets similaires sont afficher lorsqu'un utilisateur est en train de créer un nouveau sujet. La comparaison se base sur le titre et le contenu." - max_image_megapixels: "Nombre maximum autorisé de mégapixels pour une image." title_prettify: "Corrige les coquilles les plus communes dans les titres (intégralité du titre en majuscule, première lettre en minuscule, de multiples ! et ?, un . inutile à la fin, etc.)" title_remove_extraneous_space: "Supprimez les espaces devant les signes de ponctuation de fin." automatic_topic_heat_values: 'Mettez à jour automatiquement les paramètres "Sujet affiché" et "Sujet similaire" en fonction de l''activité du site.' diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 3b1fe8298c..7d39b64201 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -1141,6 +1141,7 @@ he: labels: level: דרגה description: "מספר המשתמשים בקיבוץ לפי דרגת אמון." + description_link: "https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/" users_by_type: title: "משתמשים לפי סוג" xaxis: "סוג" @@ -3715,7 +3716,7 @@ he: name: ברוכים הבאים description: קיבלו לייק long_description: | - עיטור זוה מוענק עם קבלת הלייק הראשון שלך על פוסט. מזל טוב, פרסמת משהו שחבריך לקהילה חשבו שהוא מעניין, מגניב, או שימושי! + עיטור זה מוענק עם קבלת הלייק הראשון שלך על פוסט. מזל טוב, פרסמת משהו שחבריך לקהילה חשבו שהוא מעניין, מגניב, או שימושי! autobiographer: name: אוטוביוגרפים description: "פרטי הפרופיל הושלמו" diff --git a/config/locales/server.hy.yml b/config/locales/server.hy.yml index b8e1e33ac1..661caa18a8 100644 --- a/config/locales/server.hy.yml +++ b/config/locales/server.hy.yml @@ -1428,13 +1428,11 @@ hy: desktop_category_page_style: "Արտաքին ոճ /կատեգորիաներ էջի համար:" category_colors: "Կատեգորիաների համար թույլատրելի գույների տանսվեցերորդական արժեքների ցանկ:" category_style: "Կատեգորիայի կրծքանշանների համար արտաքին ոճը:" - max_image_size_kb: "Նկարի վերբեռնման առավելագույն չափը կԲ-ով: Սա պետք է կարգավորվի nginx (client_max_body_size) / apache կամ proxy -ում:" max_attachment_size_kb: "Կցված ֆայլերի վերբեռնման առավելագույն չափը կԲ-երով: Սա պետք էկլարգավորվի nginx-ում (client_max_body_size) / apache -ում, ինչպես նաև՝ proxy -ում:" authorized_extensions: "Վերբեռնման համար թույլատրելի ֆայլերի ընդլայնումների ցանկը (օգտագործեք '*' ֆայլերի բոլոր տիպերը միացնելու համար)" authorized_extensions_for_staff: "Ֆայլերի ընդլայնումների ցանկ, որոնք թույլատրելի են վերբեռնման համար անձնակազմի անդամ օգտատերերի համար, ի հավելումն `authorized_extensions` կայքի կարգավորման մեջ սահմանված ցանկի: (օգտագործեք '*' ֆայլերի բոլոր տիպերը միացնելու համար)" theme_authorized_extensions: "Թեմայի վերբեռնումների համար թույլատրելի ֆայլերի ընդլայնումների ցանկ (օգտագործեք '*' բոլոր ֆայլերի տիպերը միացնելու համար)" max_similar_results: "Քանի նման թեմաներ ցուցադրել խմբագրիչի վերևում՝ նոր թեմա ստեղծելիս: Համեմատությունը հիմնված է վերնագրի և տեքստի վրա:" - max_image_megapixels: "Նկարի համար թույլատրելի մեգապիկսելների առավելագույն քանակը:" title_prettify: "Արգելել սովորական վերնագրի վրիպակները և սխալները՝ ներառյալ բոլոր մեծատառերը, առաջին սիմվոլի փոքրատառ լինելը, բազմակի ! և ? , լրացուցիչ . վերջում և այլն:" topic_views_heat_low: "Այս քանակի դիտումներից հետո դիտումների դաշտը թեթևակի ընդգծվում է:" topic_views_heat_medium: "Այս քանակի դիտումներից հետո դիտումների դաշտը չափավոր ընդգծվում է:" diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index e907c54035..bb53cb61cb 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -49,6 +49,7 @@ it: about_json_values: "about.json contiene valori non validi: %{errors}" git: "Errore nella clonazione del repository git, accesso negato o repository non trovato" unpack_failed: "Errore estranedo il contenuto del file" + file_too_big: "Il file non compresso è troppo grande." unknown_file_type: "Il file che hai caricato non sembra essere un tema di Discourse valido." errors: component_no_user_selectable: "I componenti del tema non possono essere selezionabili dall'utente" @@ -208,6 +209,7 @@ it: email_template_cant_be_modified: "Questo modello di email non può essere modificato." invalid_whisper_access: "I Sussurri potrebbero non essere abilitati, oppure non hai il permesso di creare Messaggi Sussurri." not_in_group: + title_topic: "Devi essere membro di un gruppo per visualizzare questa argomento." join_group: "Partecipa al Gruppo" reading_time: "Tempo di lettura" likes: "Mi piace" @@ -236,7 +238,9 @@ it: replies: one: "%{count} risposta" other: "%{count} risposte" + last_reply: "Ultima risposta" created: "Creazione" + new_topic: "Inizia un nuovo argomento" no_mentions_allowed: "Spiacenti, non puoi menzionare altri utenti." too_many_mentions: one: "Spiacenti, puoi menzionare al massimo un utente in un messaggio." @@ -304,6 +308,17 @@ it: edit_conflict: "Il messaggio è stato modificato da un altro utente e i tuoi cambiamenti non possono essere salvati." revert_version_same: "La versione attuale è la stessa versione che stai cercando di ripristinare." excerpt_image: "immagine" + bookmarks: + errors: + already_bookmarked_post: "Non puoi aggiungere più di un segnalibro allo stesso messaggio." + reminders: + at_desktop: "La prossima volta che sarò al mio desktop" + later_today: "Più tardi, oggi
{{date}}" + next_business_day: "Il prossimo giorno lavorativo
{{date}}" + tomorrow: "Domani
{{date}}" + next_week: "La prossima settimana
{{date}}" + next_month: "Il prossimo mese
{{date}}" + custom: "Data e ora personalizzate" groups: success: bulk_add: @@ -694,6 +709,9 @@ it: error: "Si è verificato un errore durante la modifica del tuo indirizzo email. Forse l'indirizzo è già in uso?" error_staged: "Si è verificato un errore durante il cambio di indirizzo email. L'indirizzo è già stato usato da un utente temporaneo." already_done: "Spiacenti, il collegamento di conferma non è più valido. Hai forse già cambiato email?" + confirm: "Conferma" + authorizing_new: + title: "Conferma il tuo nuovo indirizzo email" associated_accounts: revoke_failed: "Impossibile revocare il tuo account con %{provider_name}." connected: "(connesso)" @@ -1542,13 +1560,11 @@ it: desktop_category_page_style: "Stile visuale per la pagina /categorie." category_colors: "Un elenco di valori esadecimali di colori permessi per le categorie." category_style: "Stile grafico dei distintivi relativi alle categorie." - max_image_size_kb: "Dimensione massima in kB per caricare immagini degli utenti. Deve essere configurata anche in nginx (client_max_body_size) / apache o nel proxy." max_attachment_size_kb: "Dimensione massima dei file che gli utenti possono caricare, in kB. Configura il limite anche in nginx (client_max_body_size) / apache o nel proxy." authorized_extensions: "Una lista di estensioni dei file che è permesso caricare (usa '*' per permettere tutti i tipi di file) " authorized_extensions_for_staff: "Un elenco di estensioni di file consentite per il caricamento da parte dello staff in aggiunta all'elenco definito nell'impostazione del sito `authorized_extensions`. (usa '*'; per abilitare tutti i tipi di file)" theme_authorized_extensions: "Una lista di estensioni dei file permessi per i caricamenti dei temi (usa '*' per abilitare tutti i tipi di file)" max_similar_results: "Quanti argomenti simili mostrare sopra l'editor quando si scrive un nuovo argomento. Il paragone viene fatto sul titolo e sul corpo." - max_image_megapixels: "Massimo numero di megapixel consentito per un'immagine." title_prettify: "Evita refusi ed errori comuni nei titoli, incluso il testo tutto maiuscolo, il primo carattere minuscolo, troppi caratteri ! e ?, puntini aggiuntivi alla fine della parola ecc." title_remove_extraneous_space: "Rimuovi gli spazi bianchi davanti alla punteggiatura." automatic_topic_heat_values: 'Aggiorna automaticamente le impostazioni "topic views heat" e "topic post like heat" in base all''attività del sito.' diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index 0b29e4d7ad..8224b63b54 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -914,7 +914,6 @@ ja: min_title_similar_length: "類似トピックのチェックに必要な最小タイトル長" category_colors: "カテゴリに利用可能な色 (16進数指定) のリスト" category_style: "カテゴリバッジのスタイル" - max_image_size_kb: "アップロード可能な画像の最大サイズ (kB) nginx 側での設定 (client_max_body_size) / apache または proxy における設定も同時に行う必要があります" max_attachment_size_kb: "添付可能なファイルの最大サイズ (kB) nginx 側での設定 (client_max_body_size) / apache または proxy における設定も同時に行う必要があります" authorized_extensions: "アップロード可能なファイルの拡張子のリスト('*'で全ての拡張子が有効になります)" max_similar_results: "新規トピック編集中に類似トピックをいくつ表示するか。比較はタイトルと本文に基づきます" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index cc17fc67af..e7eeba1664 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -1112,12 +1112,10 @@ ko: desktop_category_page_style: "/categories 페이지의 비주얼 스타일" category_colors: "허용된 카테고리에 사용될 hexadecimal 색상 값의 리스트" category_style: "카테고리 뱃지 시각 스타일" - max_image_size_kb: "최대 이미지 업로드 사이즈(kB). 이 설정은 꼭 nginx / apache와 proxy에도 적용해야 합니다." max_attachment_size_kb: "최대 첨부파일 업로드 사이즈(kB). 이 설정은 꼭 nginx / apache와 proxy에도 적용해야 합니다." authorized_extensions: "파일 업로드에 허용되는 확장자 리스트 ('*'을 사용하면 모든 타입의 파일이 가능합니다.)" theme_authorized_extensions: "테마 업로드에 허용되는 확장자 목록('*'를 입력하면 모든 확장자 허용)" max_similar_results: "새로운 글타래를 작성할 때, 에디터 위에 보여줄 비슷한 글타래들의 개수. 제목과 본문을 바탕으로 비교합니다." - max_image_megapixels: "이미지의 최대 메가픽셀수" title_prettify: "일반적인 제목의 오타 및 오류를 수정해준다. 모두 대문자로 쓰거나, 첫자가 소문자이거나(영문), 복수의 !, ? 혹은 마침표(.)가 중복으로 들어간 것 등" topic_views_heat_low: "글타래가 연하게 하이라이트 되기 위한 조회수" topic_views_heat_medium: "글타래가 적당하게 하이라이트 되기 위한 조회수" diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 9c775f66e6..adebe50938 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -1073,6 +1073,7 @@ nl: labels: level: Niveau description: "Aantal gebruikers gegroepeerd op vertrouwensniveau." + description_link: "https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/" users_by_type: title: "Gebruikers per type" xaxis: "Type" diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 8a94e48fad..3e43f69002 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -1450,12 +1450,10 @@ pl_PL: desktop_category_page_style: "Styl wizualny dla strony kategorii." category_colors: "Lista kolorów w formacie hex do użycia w etykietach kategorii." category_style: "Styl etykiet kategorii" - max_image_size_kb: "Maksymalny rozmiar wgrywanego pliku graficznego w kB. Należy to także skonfigurować dla serwera nginx (client_max_body_size) / Apache lub dla proxy." max_attachment_size_kb: "Maksymalny rozmiar załącznika w kB. Należy to także skonfigurować dla serwera nginx (client_max_body_size) / Apache lub dla proxy." authorized_extensions: "Lista dozwolonych rozszerzeń plików (gwiazdka \"*\" oznacza wszystkie typy plików)" theme_authorized_extensions: "Lista dozwolonych rozszerzeń plików dla przesyłania szablonów (gwiazdka \"*\" oznacza wszystkie typy plików)" max_similar_results: "Liczba podobnych wątków, jaka zostanie wyświetlona nad oknem edytora podczas tworzenia nowego wątku. Wątki są porównywane na podstawie tytułów i treści." - max_image_megapixels: "Maksymalna rozdzielczość pliku graficznego w megapikselach." title_prettify: "Prevent common title typos and errors, including all caps, lowercase first character, multiple ! and ?, extra . at end, etc." topic_views_heat_low: "Po tylu wyświetleniach liczba wyświetleń zostanie lekko wyróżniona." topic_views_heat_medium: "Po tylu wyświetleniach liczba wyświetleń zostanie wyróżniona." diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index b0c3546265..069248ad52 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -1036,7 +1036,6 @@ pt: desktop_category_page_style: "Estilo visual para a página de categorias." category_colors: "Lista de valores hexadecimais das cores permitidas nas categorias." category_style: "Estilo visível para distintivos de categorias." - max_image_size_kb: "Tamanho máximo da imagem carregada em kB. Este deverá ser configurado em nginx (client_max_body_size) / apache ou também proxy." max_attachment_size_kb: "Tamanho máximo dos anexos carregados em kB. Este deverá ser configurado em nginx (client_max_body_size) / apache ou também proxy." authorized_extensions: "Lista de extensões permitidas para carregamento (utilizar '*' para ativar todos os tipos de ficheiros)" max_similar_results: "Quantos tópicos semelhantes a serem exibidos acima do editor ao compor um novo tópico. A comparação é baseada no título e no corpo." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index c69c1982c9..34f2e9b252 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -1584,13 +1584,11 @@ pt_BR: desktop_category_page_style: "Estilo visual para a página / categorias." category_colors: "Uma lista de valores hexadecimais de cor permitidos para categorias." category_style: "Estilo visual para emblemas de categoria." - max_image_size_kb: "O tamanho máximo de upload de imagem em kB. Isso precisa ser configurado no nginx (client_max_body_size) / apache ou também proxy." max_attachment_size_kb: "O tamanho máximo de upload de arquivos anexos em kB. Isso precisa ser configurado no nginx (client_max_body_size) / apache ou também proxy." authorized_extensions: "Uma lista de extensões de arquivo permitidas para upload (use '*' para permitir todos os tipos de arquivos)" authorized_extensions_for_staff: "Uma lista de extensões de arquivo permitidas para upload para usuários da equipe, além da lista definida na configuração do site `authorized_extensions`. (use '*' para ativar todos os tipos de arquivo)" theme_authorized_extensions: "Uma lista de extensões de arquivo permitidas para uploads de temas (use '*' para ativar todos os tipos de arquivos)" max_similar_results: "Quantos tópicos semelhantes exibir acima do editor quando compondo um novo tópico. A comparação é baseada no título e no corpo." - max_image_megapixels: "Número máximo de megapixels permitidos em uma imagem." title_prettify: "Prevenir erros comuns em títulos, incluindo caps-lock ligado, primeira letra minúscula, excesso de ! e ?, pontos extras no final, etc." title_remove_extraneous_space: "Remover os espaços em branco iniciais à frente da pontuação final." automatic_topic_heat_values: 'Atualizar automaticamente as configurações "visualizações de tópico populares" e "curtidas de postagens de tópicos populares" com base na atividade do site.' diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 1b8f5186ec..9026cd79c8 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -993,7 +993,6 @@ ro: desktop_category_page_style: "Stil vizual pentru pagina /categories ." category_colors: "O listă hexazecimală de valori de culori permise pentru categorii." category_style: "Stil vizual pentru categoria ecusoane." - max_image_size_kb: "Mărimea maximă a unei imagini încărcate în kB. Aceasta trebuie configurată în nginx (client_max_body_size) / apache sau proxy." max_attachment_size_kb: "Mărimea maximă a unui atașament de fișiere încărcate în Kb. Aceasta trebuie configurata în nginx (client_max_body_size) / apache sau proxy." authorized_extensions: "O listă de extensii de fișiere permise la încărcare (folosiți '*' pentru activarea tuturor tipurilor de fișiere)" max_similar_results: "Câte subiecte similare vor fi afișate deasupra editorului atunci când se compune un subiect nou. Comparația se face după titlu și pe conținut." diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index 836ad7403f..6699cf541d 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -1524,11 +1524,9 @@ ru: desktop_category_page_style: "Визуальный стиль для страницы категории." category_colors: "Список шестнадцатеричных кодов цветов, разрешенных для разделов." category_style: "Стили для выделения разделов." - max_image_size_kb: "Максимальный размер загружаемых картинок в килобайтах. Убедитесь, что вы также настроили ограничение в nginx (client_max_body_size) / apache или прокси." max_attachment_size_kb: "Максимальный размер загружаемых файлов в килобайтах. Убедитесь, что вы также настроили ограничение в nginx (client_max_body_size) / apache или прокси." authorized_extensions: "Список расширений файлов, разрешенных к загрузке. Используйте '*', чтобы разрешить любые типы файлов." max_similar_results: "Количество похожих тем, показываемых пользователю во время создания новой темы. Сравнение выполняется на основании названия и текста темы." - max_image_megapixels: "Максимально допустимое количество мегапикселей для изображения." title_prettify: "Предотвращать распространенные опечатки и ошибки, включая КАПС, первый строчный символ, множественные ! и ?, лишние . в конце предложения и т.д." title_remove_extraneous_space: "Удалите начальные пробелы перед конечной пунктуацией." topic_views_heat_low: "После этого количества просмотров, поле просмотров слегка подсвечивается." diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index d7dde1eb51..d530c26d6c 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -943,7 +943,6 @@ sk: min_title_similar_length: "Minimálna dĺžka nadpisu na spustenie kontroly na podobné témy." category_colors: "Zoznam hexadecimálnych hodnôt farieb povolených pre kategórie." category_style: "Vizuálny štýl pre odznaky za kategórie." - max_image_size_kb: "Mmaximálna veľkosť vloženého obrázku v kB. Rovnako musí byť nastavená v nginx (client_max_body_size) / apache alebo na proxy." max_attachment_size_kb: "Mmaximálna veľkosť vloženej prílohy v kB. Rovnako musí byť nastavená v nginx (client_max_body_size) / apache alebo na proxy." authorized_extensions: "Zoznam povolených príloh súborov ktoré je umožnené vkladať (použite '*' na vkladanie súborov všetkých typov)" max_similar_results: "Koľko podobných tém sa má zobraziť nad editorom ak vytvára novú tému. Porovnanie je založené na nadpise a tele správy." diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index 424e657d82..5487ad6248 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -755,7 +755,6 @@ sq: min_title_similar_length: "The minimum length of a title before it will be checked for similar topics." category_colors: "A list of hexadecimal color values allowed for categories." category_style: "Visual style for category badges." - max_image_size_kb: "The maximum image upload size in kB. This must be configured in nginx (client_max_body_size) / apache or proxy as well." max_attachment_size_kb: "The maximum attachment files upload size in kB. This must be configured in nginx (client_max_body_size) / apache or proxy as well." authorized_extensions: "A list of file extensions allowed for upload (use '*' to enable all file types)" max_similar_results: "How many similar topics to show above the editor when composing a new topic. Comparison is based on title and body." diff --git a/config/locales/server.sr.yml b/config/locales/server.sr.yml index 839bc4d1b3..ebfd0ef55d 100644 --- a/config/locales/server.sr.yml +++ b/config/locales/server.sr.yml @@ -218,7 +218,6 @@ sr: max_replies_in_first_day: "Maksimalan broj odgovora koje korisnik može da kreira u prvih 24 sata nakon kreiranja svoje prve poruke." pending_users_reminder_delay: "Obavesti moderatore ako novi korisnik čeka na odobrenje duže nego ovoliko časova. Podesiti na -1 da bi se onemogućile notifikacije." maximum_session_age: "Korisnik će ostati ulogovan n časova od prethodnog logovanja" - max_image_megapixels: "Maksimalan dozvoljeni broj megapiksela za slike." topic_views_heat_low: "Nakon ovoliko pregleda, polje pregleda je blago označeno." topic_views_heat_medium: "Nakon ovoliko pregleda, polje pregleda je umereno označeno." topic_views_heat_high: "Nakon ovoliko pregleda, polje pregleda je jako označeno." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 2c3d28517b..68dc3866d4 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -1053,6 +1053,7 @@ sv: labels: level: Nivå description: "Antalet användare fördelat på förtroendenivå." + description_link: "https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/" users_by_type: title: "Användare per typ" xaxis: "Typ" diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index fd09188ce4..8df329d753 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -1633,13 +1633,11 @@ tr_TR: desktop_category_page_style: "/categories sayfasının görsel biçimi." category_colors: "Kategoriler için izin verilen onaltılı renk değerlerinin listesi" category_style: "Kategori rozetleri için görsel biçim." - max_image_size_kb: "Yüklenebilecek resmin KB cinsinden en fazla büyüklüğü. Bu nginx (client_max_body_size) / apache veya proxyde de ayarlanmalı." max_attachment_size_kb: "Yüklenebilecek dosyaların KB cinsinden en fazla büyüklüğü. Bu nginx (client_max_body_size) / apache veya proxyde de ayarlanmalı." authorized_extensions: "Yüklenebilecek dosya uzantılarının listesi (tüm dosya türlerini etkinleştirmek için '*' kullanın)" authorized_extensions_for_staff: "`Yetkili_uzanımlar` site ayarında tanımlanan listeye ek olarak yetkili kullanıcılar için yüklenmeye izin verilen dosya uzantılarının listesi. (tüm dosya türlerini etkinleştirmek için '*' kullanın)" theme_authorized_extensions: "Tema yüklemeleri için izin verilen dosya uzantılarının listesi (tüm dosya türlerini etkinleştirmek için '*' kullanın)" max_similar_results: "Yeni bir konu oluştururken, düzenleyicinin üzerinde gösterilecek benzer konuların sayısı. Karşılaştırmalar başlık ve içerik üzerinden yapılır." - max_image_megapixels: "Bir görüntü için izin verilen maksimum megapiksel sayısı." title_prettify: "Tümü büyük harf, ilk karakteri küçük harf, çoklu ! ve ?, sonda ekstra . kullanımı gibi, sık yapılan yazım hatalarını önle." title_remove_extraneous_space: "Noktalama işaretlerinin önündeki ön boşlukları kaldırın." automatic_topic_heat_values: 'Site etkinliğine dayalı olarak "En çok görüntülenen konular" ve "En popüler konu gönderileri" ayarlarını otomatik olarak güncelleyin.' diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 693bbd4517..5c240cf6e0 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -1633,13 +1633,11 @@ uk: desktop_category_page_style: "Візуальний стиль для сторінки категорії." category_colors: "Список шістнадцятирічних кодів кольорів, дозволених для категорій." category_style: "Стилі для виділення розділів." - max_image_size_kb: "Максимальний розмір завантажуваних картинок в кілобайтах. Переконайтеся, що ви також налаштували обмеження в nginx (client_max_body_size) / apache або проксі." max_attachment_size_kb: "Максимальний розмір завантажуваних файлів в кілобайтах. Переконайтеся, що ви також налаштували обмеження в nginx (client_max_body_size) / apache або проксі." authorized_extensions: "Список розширень файлів, дозволених до завантаження. Використовуйте '*', щоб дозволити будь-які типи файлів." authorized_extensions_for_staff: "Список розширень файлів, дозволених для завантаження для користувачів персоналу на додатково до списку, визначеного в налаштуваннях сайту як `authorized_extensions`. (використовувати '*', щоб увімкнути всі типи файлів)" theme_authorized_extensions: "Список розширень файлів, дозволених для завантаження в темі (використовуйте '*', щоб увімкнути всі типи файлів)" max_similar_results: "Кількість схожих тим, що показуються користувачеві під час створення нової теми. Порівняння виконується на підставі назви і тексту теми." - max_image_megapixels: "Максимально допустима кількість мегапікселів для зображення." title_prettify: "Запобігати типовим опискам та помилкам, таким як: всі літери - великі, перша літера - мала, багаторазові ! та ?, надлишкові . в кінці, тощо." title_remove_extraneous_space: "Видаліть початкові пробіли перед кінцевою пунктуацією." automatic_topic_heat_values: 'Автоматично оновлювати параметри "topic views heat" та "topic post like heat" на основі активності сайту.' diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml index 368e734edb..38a8762336 100644 --- a/config/locales/server.ur.yml +++ b/config/locales/server.ur.yml @@ -1564,13 +1564,11 @@ ur: desktop_category_page_style: " /categories صفحے کیلئے بصری سٹائل۔" category_colors: "زُمرہ جات کیلئے ہیکسا ڈَیسیمل رنگوں کے اقدار کی ایک فہرست۔" category_style: "زُمرہ بیَجوں کیلئے بصری سٹائل۔" - max_image_size_kb: "kB میں تصویر اَپ لوڈ کا زیادہ سے زیادہ سائز۔ یہ اِنجَن٘ اَیکس (client_max_body_size) / اَپَیچی یا پراکسی میں بھی ترتیب دیا جانا لازمی ہے۔" max_attachment_size_kb: "kB میں اٹیچمنٹ فائل اَپ لوڈ کا زیادہ سے زیادہ سائز۔ یہ اِنجَن٘ اَیکس (client_max_body_size) / اَپَیچی یا پراکسی میں بھی ترتیب دیا جانا لازمی ہے۔" authorized_extensions: "اپ لوڈ کیلئے اجازت یافتہ فائل اَیکسٹَینشَنز کی فہرست (تمام فائل اقسام کی اجازت دینے کیلئے '*' کا استعمال کریں)" authorized_extensions_for_staff: "سٹاف صارفین کیلئے `authorized_extensions` سائٹ ترتیب میں بیان کردہ فہرست کے علاوہ اَپ لوڈ کیلئے اجازت یافتہ فائل اَیکسٹَینشَنز کی فہرست۔ (تمام فائل اقسام کی اجازت دینے کیلئے '*' کا استعمال کریں)" theme_authorized_extensions: "تھِیم اپ لوڈز کیلئے اجازت یافتہ فائل اَیکسٹَینشَنز کی فہرست (تمام فائل اقسام کی اجازت دینے کیلئے '*' کا استعمال کریں)" max_similar_results: "ایک نیا ٹاپک کمپوز کرتے وقت کتنے اُسی جیسے دوسرے ٹاپک اَیڈیٹر کے اوپر دکھائے جائیں۔ موازنہ عنوان اور متن پر مبنی ہے۔" - max_image_megapixels: "ایک تصویر کیلئے مَیگا پِکسل کی زیادہ سے زیادہ عدد۔" title_prettify: "عام عنوان ٹائپینگ کی غلطیوں کو روکیں، بشمول تمام کَیپس، سب سے پہلے حرف کا لوئر کََیس ہونا، کئی! اور؟، آخر میں اضافی ۔، وغیرہ" title_remove_extraneous_space: "آخری وقفی علامت کے آگے سے خالی جگہیں ہٹا دیں۔" automatic_topic_heat_values: 'سائٹ کی سرگرمیوں پر مبنی "ٹاپک وِیوز گرمی" اور "ٹاپک پوسٹ لائیک گرمی" کی ترتیبات خود بخود اپ ڈیٹ کریں۔' diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index d56aaaf092..454c36a78b 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -852,7 +852,6 @@ vi: min_title_similar_length: "Chiều dài tối thiểu của tiêu đề trước khi kiểm tra trùng chủ đề." category_colors: "Danh sách mã màu hexa cho phép cho danh mục." category_style: "Style trực quan cho phù hiệu danh mục." - max_image_size_kb: "Kích cỡ ảnh upload tối đa theo Kb. Thiết lập này phải được cấu hình cả trong nginx (client_max_body_size) / apache hoặc trong proxy." max_attachment_size_kb: "Kích thước file tải lên tối đa tính theo kB. đã cấu hình trong nginx (client_max_body_size) / apache hoặc proxy." authorized_extensions: "Danh sách định dạng file cho phép tải lên (sử dụng '*' để cho phép tất cả loại tập tin)" max_similar_results: "Số lượng chủ đề tương tự hiển thị phía trên bộ soạn thảo khi soạn chủ đề mới, so sánh dựa trên tiêu đề và nội dung." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index a0f9bcc900..af53b87eb5 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -274,6 +274,7 @@ zh_CN: pm_reached_recipients_limit: "抱歉,私信中的收信人不能超过%{recipients_limit}。" removed_direct_reply_full_quotes: "自动删除整个上篇帖子的引用。" secure_upload_not_allowed_in_public_topic: "抱歉,无法在公开的主题中使用以下安全上传:%{upload_filenames}。" + create_pm_on_existing_topic: "抱歉,你无法在现有主题上创建PM。" just_posted_that: "太类似于你最近发表的内容" invalid_characters: "包含无效字符" is_invalid: "似乎不清楚,这是一个完整的句子? " @@ -1600,13 +1601,13 @@ zh_CN: desktop_category_page_style: "/categories 页面的视觉样式。" category_colors: "设置分类颜色的十六进制色彩值列表。" category_style: "分类图标的视觉样式。" - max_image_size_kb: "允许用户上传的最大文件大小(以kB为单位)。确保也在nginx(client_max_body_size),apache 或代理中进行限制文件大小的配置。" + max_image_size_kb: "以kB为单位的上传图像最大尺寸。必须同样在nginx(client_max_body_size)/ apache或代理中配置。大于或小于client_max_body_size的图片将被在上传时调整至适当的大小。" max_attachment_size_kb: "允许用户上传的最大文件大小(单位:KB)——同时在 nginx(client_max_body_size),apache 或代理中配置他们。" authorized_extensions: "允许上传文件的扩展名列表('*' 表示允许所有文件类型)" authorized_extensions_for_staff: "除了在“authorized_extensions”站点设置中定义的列表之外,还运行管理人员上传的文件扩展名列表。(使用'*'启用所有文件类型)" theme_authorized_extensions: "主题上传可用的文件扩展名列表(“*” 可允许所有文件)" max_similar_results: "当用户撰写新主题时,显示多少类似主题给用户。比较是根据标题和内容进行的。" - max_image_megapixels: "图片允许的最大像素。" + max_image_megapixels: "以百万像素为单位单个图像允许的最大值。高于最大值百万像素的图像将被拒绝。" title_prettify: "防止常见标题里的错别字和错误,包括全部大写,第一个字符小写,多个'!'和'?',结尾多余的'.'等等。" title_remove_extraneous_space: "删除结尾标点前的空格。" automatic_topic_heat_values: '基于站点活动自动更新“主题观点热度”和“主题帖子赞的主题热度”设置。' @@ -1646,6 +1647,8 @@ zh_CN: reviewable_claiming: "是否需要审核可审核的内容才能对其进行操作?" reviewable_default_topics: "默认以主题为分组显示可审核内容" reviewable_default_visibility: "满足此优先级前不要显示可审核条目" + high_trust_flaggers_auto_hide_posts: "新用户的发帖被信任等级3+的用户标记为垃圾后将被自动隐藏。" + cooldown_hours_until_reflag: "用户需要等待多长时间才能重新标记一个帖子。" reply_by_email_enabled: "启用通过邮件回复。" reply_by_email_address: "通过邮件回复的回复地址模板,例如:%%{reply_key}@reply.example.com 或 replies+%%{reply_key}@example.com" alternative_reply_by_email_addresses: "通过邮件回复的回复地址模板,例如:%%{reply_key}@reply.example.com|replies+%%{reply_key}@example.com" @@ -1741,6 +1744,7 @@ zh_CN: ignored_users_message_gap_days: "当一个用户被很多用户忽视时再次通知版主要等待多久。" clean_up_inactive_users_after_days: "非活跃用户(信任等级0且没有任何帖子)被删除前等待的天数。设为0则关闭清理。" user_selected_primary_groups: "允许用户设置其主要群组" + max_notifications_per_user: "每个用户的最大通知数量,如超出,旧的通知将被删除。每周执行一次。设为0以关闭。" user_website_domains_whitelist: "用户网站要属于这些域名中。用 | 分割。" allow_profile_backgrounds: "允许用户上传个人资料背景图片。" sequential_replies_threshold: "在被提醒回复了太多连续的回复前,用户在主题中可以连续回复的帖子的数量。" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 78dc673ab1..2221178662 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -1480,13 +1480,11 @@ zh_TW: desktop_category_page_style: "/categories 頁面的視覺樣式。" category_colors: "設定分類顏色的十六進制色彩值列表。" category_style: "分類圖標的視覺樣式。" - max_image_size_kb: "允許使用者上傳的最大圖片大小(以kB為單位)。確保也在nginx(client_max_body_size),apache 或代理中進行限制文件大小的設定。" max_attachment_size_kb: "允許使用者上傳的最大文件大小(以kB為單位)。確保也在nginx(client_max_body_size),apache 或代理中進行限制文件大小的設定。" authorized_extensions: "允許上傳文件的副檔名列表('*' 表示允許所有文件類型)" authorized_extensions_for_staff: "除了在“authorized_extensions” 可以設定網站可允許上傳副檔名的清單之外,也允許管理員(有權限的員工)為使用者上載檔案副檔名清單。(使用 星字號 * 以啟用所有檔案類型)" theme_authorized_extensions: "允許佈景主題上傳的檔案副檔名列表(使用星字號 * 啟用所有文件類型)" max_similar_results: "當使用者撰寫新主題時,顯示多少類似主題給使用者。比較根據標題和內容進行。" - max_image_megapixels: "圖像所允許的最大百萬畫素數質。" title_prettify: "防止常見標題裡的錯別字和錯誤,包括全部大寫,首字小寫,多個!和?,結尾多餘的. 等等。" topic_views_heat_low: "多少次瀏覽後,該視圖將稍稍高亮。" topic_views_heat_medium: "多少次瀏覽後,該視圖將明顯高亮。" diff --git a/config/routes.rb b/config/routes.rb index 5d61ccc630..8fd2d19727 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -140,7 +140,6 @@ Discourse::Application.routes.draw do get 'users/:id/:username/tl3_requirements' => 'users#show' post "users/sync_sso" => "users#sync_sso", constraints: AdminConstraint.new - post "users/invite_admin" => "users#invite_admin", constraints: AdminConstraint.new resources :impersonate, constraints: AdminConstraint.new diff --git a/lefthook.yml b/lefthook.yml index f3305fb786..8dae834398 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -4,6 +4,10 @@ pre-commit: rubocop: glob: "*.rb" run: bundle exec rubocop {staged_files} + prettier: + glob: "*.{js,es6}" + exclude: "vendor/*|fixtures|public/javascripts|shims.js|ember-addons|template-lintrc|locale/*|test_helper|run-qunit" + run: yarn prettier --list-different {staged_files} eslint: glob: "*.{js,es6}" exclude: "vendor/*|fixtures|public/javascripts|shims.js|ember-addons|template-lintrc|locale/*|test_helper|run-qunit" diff --git a/lib/backup_restore.rb b/lib/backup_restore.rb index 6259044dcb..4775fdd612 100644 --- a/lib/backup_restore.rb +++ b/lib/backup_restore.rb @@ -23,7 +23,8 @@ module BackupRestore factory: BackupRestore::Factory.new( user_id: user_id, client_id: opts[:client_id] - ) + ), + disable_emails: opts.fetch(:disable_emails, true) ) start! restorer diff --git a/lib/backup_restore/database_restorer.rb b/lib/backup_restore/database_restorer.rb index b3cb9ab0c8..d0880c97ed 100644 --- a/lib/backup_restore/database_restorer.rb +++ b/lib/backup_restore/database_restorer.rb @@ -134,7 +134,7 @@ module BackupRestore log "Migrating the database..." log Discourse::Utils.execute_command( - { "SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0" }, + { "SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0", "SKIP_OPTIMIZE_ICONS" => "1" }, "rake db:migrate", failure_message: "Failed to migrate database.", chdir: Rails.root diff --git a/lib/discourse.rb b/lib/discourse.rb index 57e8778e90..7241dc1c35 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -45,6 +45,39 @@ module Discourse logs.join("\n".freeze) end + def self.atomic_write_file(destination, contents) + begin + return if File.read(destination) == contents + rescue Errno::ENOENT + end + + FileUtils.mkdir_p(File.join(Rails.root, 'tmp')) + temp_destination = File.join(Rails.root, 'tmp', SecureRandom.hex) + + File.open(temp_destination, "w") do |fd| + fd.write(contents) + fd.fsync() + end + + File.rename(temp_destination, destination) + + nil + end + + def self.atomic_ln_s(source, destination) + begin + return if File.readlink(destination) == source + rescue Errno::ENOENT, Errno::EINVAL + end + + FileUtils.mkdir_p(File.join(Rails.root, 'tmp')) + temp_destination = File.join(Rails.root, 'tmp', SecureRandom.hex) + execute_command('ln', '-s', source, temp_destination) + File.rename(temp_destination, destination) + + nil + end + private class CommandRunner diff --git a/lib/file_store/to_s3_migration.rb b/lib/file_store/to_s3_migration.rb index fe2edd5a53..456e428657 100644 --- a/lib/file_store/to_s3_migration.rb +++ b/lib/file_store/to_s3_migration.rb @@ -139,9 +139,6 @@ module FileStore # we don't want have migrated state, ensure we run all jobs here Jobs.run_immediately! - log "Checking if #{@current_db} already migrated..." - return log "Already migrated #{@current_db}!" if migration_successful? - log "*" * 30 + " DRY RUN " + "*" * 30 if @dry_run log "Migrating uploads to S3 for '#{@current_db}'..." diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index b810a9dd78..eb7c2322fc 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -543,12 +543,11 @@ class Plugin::Instance Discourse::Utils.execute_command('mkdir', '-p', target) target << name.gsub(/\s/, "_") - # TODO a cleaner way of registering and unregistering - Discourse::Utils.execute_command('rm', '-f', target) - Discourse::Utils.execute_command('ln', '-s', public_data, target) + + Discourse::Utils.atomic_ln_s(public_data, target) end - ensure_directory(Plugin::Instance.js_path) + ensure_directory(js_file_path) contents = [] handlebars_includes.each { |hb| contents << "require_asset('#{hb}')" } @@ -558,12 +557,15 @@ class Plugin::Instance contents << (is_dir ? "depend_on('#{f}')" : "require_asset('#{f}')") end - File.delete(js_file_path) if js_asset_exists? - if contents.present? contents.insert(0, "<%") contents << "%>" - write_asset(js_file_path, contents.join("\n")) + Discourse::Utils.atomic_write_file(js_file_path, contents.join("\n")) + else + begin + File.delete(js_file_path) + rescue Errno::ENOENT + end end end diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb index 09826338c8..5ceabfde79 100644 --- a/lib/plugin/metadata.rb +++ b/lib/plugin/metadata.rb @@ -21,6 +21,7 @@ class Plugin::Metadata "discourse-cakeday", "discourse-canned-replies", "discourse-calendar", + "discourse-categories-suppressed", "discourse-characters-required", "discourse-chat-integration", "discourse-checklist", diff --git a/lib/post_action_creator.rb b/lib/post_action_creator.rb index 342ed5fd98..ab38576c8f 100644 --- a/lib/post_action_creator.rb +++ b/lib/post_action_creator.rb @@ -48,10 +48,8 @@ class PostActionCreator @meta_post = nil end - def perform - result = CreateResult.new - - unless guardian.post_can_act?( + def post_can_act? + guardian.post_can_act?( @post, @post_action_name, opts: { @@ -59,6 +57,12 @@ class PostActionCreator taken_actions: PostAction.counts_for([@post].compact, @created_by)[@post&.id] } ) + end + + def perform + result = CreateResult.new + + unless post_can_act? result.forbidden = true result.add_error(I18n.t("invalid_access")) return result diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index 0de6000fea..d207e01eed 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -68,7 +68,7 @@ module PrettyText sha1, url, extension, original_filename, secure = row if short_urls = reverse_map[sha1] - secure_media = FileHelper.is_supported_media?(original_filename) && SiteSetting.secure_media? && secure + secure_media = SiteSetting.secure_media? && secure short_urls.each do |short_url| result[short_url] = { diff --git a/lib/search.rb b/lib/search.rb index 1869c0ce45..e7e97d8ede 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -176,7 +176,7 @@ class Search @search_context = @guardian.user end - if @search_all_topics + if @search_all_topics && @guardian.user @opts[:type_filter] = "all_topics" end diff --git a/lib/site_settings/validations.rb b/lib/site_settings/validations.rb index 56987b0dfe..0bf13f7606 100644 --- a/lib/site_settings/validations.rb +++ b/lib/site_settings/validations.rb @@ -143,6 +143,8 @@ module SiteSettings::Validations def validate_s3_upload_bucket(new_val) validate_bucket_setting("s3_upload_bucket", new_val, SiteSetting.s3_backup_bucket) + + validate_error(:s3_upload_bucket_is_required, setting_name: 's3_upload_bucket') if new_val.blank? && SiteSetting.enable_s3_uploads? end def validate_s3_backup_bucket(new_val) diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index 626acd5760..df830da6ac 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -77,7 +77,7 @@ task 'db:migrate' => ['load_config', 'environment', 'set_locale'] do |_, args| SeedFu.seed(DiscoursePluginRegistry.seed_paths) - if !Discourse.skip_post_deployment_migrations? + if !Discourse.skip_post_deployment_migrations? && ENV['SKIP_OPTIMIZE_ICONS'] != '1' puts print "Optimizing site icons... " SiteIconManager.ensure_optimized! diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 7480cb73c4..33c69b7d6c 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -615,6 +615,21 @@ task "uploads:recover" => :environment do end end +task "uploads:sync_s3_acls" => :environment do + RailsMultisite::ConnectionManagement.each_connection do |db| + unless Discourse.store.external? + puts "This task only works for external storage." + exit 1 + end + + puts "CAUTION: This task may take a long time to complete!" + puts "-" * 30 + puts "Uploads marked as secure will get a private ACL, and uploads marked as not secure will get a public ACL." + adjust_acls(Upload.find_each(batch_size: 100)) + puts "", "Upload ACL sync complete!" + end +end + task "uploads:disable_secure_media" => :environment do RailsMultisite::ConnectionManagement.each_connection do |db| unless Discourse.store.external? @@ -628,43 +643,56 @@ task "uploads:disable_secure_media" => :environment do secure_uploads = Upload.includes(:posts).where(secure: true) secure_upload_count = secure_uploads.count + uploads_to_adjust_acl_for = [] + posts_to_rebake = {} i = 0 secure_uploads.find_each(batch_size: 20).each do |upload| - Upload.transaction do - upload.secure = false + uploads_to_adjust_acl_for << upload - RakeHelpers.print_status_with_label("Updating ACL for upload #{upload.id}.......", i, secure_upload_count) - Discourse.store.update_upload_ACL(upload) - - RakeHelpers.print_status_with_label("Rebaking posts for upload #{upload.id}.......", i, secure_upload_count) - upload.posts.each(&:rebake!) - upload.save - - i += 1 + upload.posts.each do |post| + # don't want unnecessary double-ups + next if posts_to_rebake.key?(post.id) + posts_to_rebake[post.id] = post end + + i += 1 end + puts "", "Marking #{secure_upload_count} uploads as not secure.", "" + secure_uploads.update_all(secure: false) + + adjust_acls(uploads_to_adjust_acl_for) + post_rebake_errors = rebake_upload_posts(posts_to_rebake) + log_rebake_errors(post_rebake_errors) + RakeHelpers.print_status_with_label("Rebaking and updating complete! ", i, secure_upload_count) - puts "" end - puts "Secure media is now disabled!", "" + puts "", "Secure media is now disabled!", "" +end + +# Renamed to uploads:secure_upload_analyse_and_update +task "uploads:ensure_correct_acl" => :environment do + puts "This task has been deprecated, run uploads:secure_upload_analyse_and_update task instead." + exit 1 end ## # Run this task whenever the secure_media or login_required # settings are changed for a Discourse instance to update -# the upload secure flag and S3 upload ACLs. -task "uploads:ensure_correct_acl" => :environment do +# the upload secure flag and S3 upload ACLs. Any uploads that +# have their secure status changed will have all associated posts +# rebaked. +task "uploads:secure_upload_analyse_and_update" => :environment do RailsMultisite::ConnectionManagement.each_connection do |db| unless Discourse.store.external? puts "This task only works for external storage." exit 1 end - puts "Ensuring correct ACL for uploads in #{db}...", "" - + puts "Analyzing security for uploads in #{db}...", "" + upload_ids_to_mark_as_secure, upload_ids_to_mark_as_not_secure, posts_to_rebake, uploads_to_adjust_acl_for = nil Upload.transaction do mark_secure_in_loop_because_no_login_required = false @@ -677,19 +705,23 @@ task "uploads:ensure_correct_acl" => :environment do update_uploads_access_control_post end - # First of all only get relevant uploads (supported media). - # - # Also only get uploads that are not for a theme or a site setting, so only - # get post related uploads. - uploads_with_supported_media = Upload.includes(:posts, :access_control_post, :optimized_images).where( - "LOWER(original_filename) SIMILAR TO '%\.(jpg|jpeg|png|gif|svg|ico|mp3|ogg|wav|m4a|mov|mp4|webm|ogv)'" - ).joins(:post_uploads) + # Get all uploads in the database, including optimized images. Both media (images, videos, + # etc) along with attachments (pdfs, txt, etc.) must be loaded because all can be marked as + # secure based on site settings. + uploads_to_update = Upload.includes(:posts, :optimized_images).joins(:post_uploads) - puts "There are #{uploads_with_supported_media.count} upload(s) with supported media that could be marked secure.", "" + # we do this to avoid a heavier post query, and to make sure we only + # get unique posts AND include deleted posts (unscoped) + unique_access_control_posts = Post.unscoped.select(:id, :topic_id).includes(topic: :category).where(id: uploads_to_update.pluck(:access_control_post_id).uniq) + uploads_to_update.each do |upload| + upload.access_control_post = unique_access_control_posts.find { |post| post.id == upload.access_control_post_id } + end + + puts "There are #{uploads_to_update.count} upload(s) that could be marked secure.", "" # Simply mark all these uploads as secure if login_required because no anons will be able to access them if SiteSetting.login_required? - mark_all_as_secure_login_required(uploads_with_supported_media) + mark_secure_in_loop_because_no_login_required = false else # If NOT login_required, then we have to go for the other slower flow, where in the loop @@ -698,24 +730,50 @@ task "uploads:ensure_correct_acl" => :environment do puts "Marking posts as secure in the next step because login_required is false." end - puts "", "Determining which of #{uploads_with_supported_media.count} upload posts need to be marked secure and be rebaked.", "" + puts "", "Analysing which of #{uploads_to_update.count} uploads need to be marked secure and be rebaked.", "" - upload_ids_to_mark_as_secure, posts_to_rebake = determine_upload_security_and_posts_to_rebake( - uploads_with_supported_media, mark_secure_in_loop_because_no_login_required + upload_ids_to_mark_as_secure, + upload_ids_to_mark_as_not_secure, + posts_to_rebake, + uploads_to_adjust_acl_for = determine_upload_security_and_posts_to_rebake( + uploads_to_update, mark_secure_in_loop_because_no_login_required ) - mark_specific_uploads_as_secure_no_login_required(upload_ids_to_mark_as_secure) - - post_rebake_errors = rebake_upload_posts(posts_to_rebake) - log_rebake_errors(post_rebake_errors) + if !SiteSetting.login_required? + update_specific_upload_security_no_login_required(upload_ids_to_mark_as_secure, upload_ids_to_mark_as_not_secure) + else + mark_all_as_secure_login_required(uploads_to_update) + end end + + # Enqueue rebakes AFTER upload transaction complete, so there is no race condition + # between updating the DB and the rebakes occurring. + post_rebake_errors = rebake_upload_posts(posts_to_rebake) + log_rebake_errors(post_rebake_errors) + + # Also do this AFTER upload transaction complete so we don't end up with any + # errors leaving ACLs in a bad state (the ACL sync task can be run to fix any + # outliers at any time). + adjust_acls(uploads_to_adjust_acl_for) end - puts "", "Done" + puts "", "", "Done!" end -def mark_all_as_secure_login_required(uploads_with_supported_media) - puts "Marking #{uploads_with_supported_media.count} upload(s) as secure because login_required is true.", "" - uploads_with_supported_media.update_all(secure: true) +def adjust_acls(uploads_to_adjust_acl_for) + total_count = uploads_to_adjust_acl_for.respond_to?(:length) ? uploads_to_adjust_acl_for.length : uploads_to_adjust_acl_for.count + puts "", "Updating ACL for #{total_count} uploads.", "" + i = 0 + uploads_to_adjust_acl_for.each do |upload| + RakeHelpers.print_status_with_label("Updating ACL for upload.......", i, total_count) + Discourse.store.update_upload_ACL(upload) + i += 1 + end + RakeHelpers.print_status_with_label("Updaing ACLs complete! ", i, total_count) +end + +def mark_all_as_secure_login_required(uploads_to_update) + puts "Marking #{uploads_to_update.count} upload(s) as secure because login_required is true.", "" + uploads_to_update.update_all(secure: true) puts "Finished marking upload(s) as secure." end @@ -727,11 +785,16 @@ def log_rebake_errors(rebake_errors) end end -def mark_specific_uploads_as_secure_no_login_required(upload_ids_to_mark_as_secure) - return if upload_ids_to_mark_as_secure.empty? - puts "Marking #{upload_ids_to_mark_as_secure.length} uploads as secure because UploadSecurity determined them to be secure." - Upload.where(id: upload_ids_to_mark_as_secure).update_all(secure: true) - puts "Finished marking uploads as secure." +def update_specific_upload_security_no_login_required(upload_ids_to_mark_as_secure, upload_ids_to_mark_as_not_secure) + if upload_ids_to_mark_as_secure.any? + puts "Marking #{upload_ids_to_mark_as_secure.length} uploads as secure because UploadSecurity determined them to be secure." + Upload.where(id: upload_ids_to_mark_as_secure).update_all(secure: true) + end + if upload_ids_to_mark_as_not_secure.any? + puts "Marking #{upload_ids_to_mark_as_not_secure.length} uploads as not secure because UploadSecurity determined them to be not secure." + Upload.where(id: upload_ids_to_mark_as_not_secure).update_all(secure: false) + end + puts "Finished updating upload security." end def update_uploads_access_control_post @@ -752,12 +815,13 @@ def update_uploads_access_control_post end def rebake_upload_posts(posts_to_rebake) + posts_to_rebake = posts_to_rebake.values post_rebake_errors = [] puts "", "Rebaking #{posts_to_rebake.length} posts with affected uploads.", "" begin i = 0 posts_to_rebake.each do |post| - RakeHelpers.print_status_with_label("Determining which uploads to mark secure and rebake.....", i, posts_to_rebake.length) + RakeHelpers.print_status_with_label("Rebaking posts.....", i, posts_to_rebake.length) post.rebake! i += 1 end @@ -770,32 +834,55 @@ def rebake_upload_posts(posts_to_rebake) post_rebake_errors end -def determine_upload_security_and_posts_to_rebake(uploads_with_supported_media, mark_secure_in_loop_because_no_login_required) +def determine_upload_security_and_posts_to_rebake(uploads_to_update, mark_secure_in_loop_because_no_login_required) upload_ids_to_mark_as_secure = [] - posts_to_rebake = [] + upload_ids_to_mark_as_not_secure = [] + uploads_to_adjust_acl_for = [] + posts_to_rebake = {} i = 0 - uploads_with_supported_media.find_each(batch_size: 50) do |upload_with_supported_media| - RakeHelpers.print_status_with_label("Updating ACL for upload.......", i, uploads_with_supported_media.count) + uploads_to_update.find_each(batch_size: 50) do |upload_to_update| # we just need to determine the post security here so the ACL is set to the correct thing, # because the update_upload_ACL method uses upload.secure? - upload_with_supported_media.secure = UploadSecurity.new(upload_with_supported_media).should_be_secure? - Discourse.store.update_upload_ACL(upload_with_supported_media) + original_update_secure_status = upload_to_update.secure + upload_to_update.secure = UploadSecurity.new(upload_to_update).should_be_secure? - RakeHelpers.print_status_with_label("Determining which uploads to mark secure and rebake.....", i, uploads_with_supported_media.count) - upload_with_supported_media.posts.each { |post| posts_to_rebake << post } + # no point changing ACLs or rebaking or doing any such shenanigans + # when the secure status hasn't even changed! + if original_update_secure_status == upload_to_update.secure + i += 1 + next + end - if mark_secure_in_loop_because_no_login_required && upload_with_supported_media.secure? - upload_ids_to_mark_as_secure << upload_with_supported_media.id + # we only want to update the acl later once the secure status + # has been saved in the DB; otherwise if there is a later failure + # we get stuck with an incorrect ACL in S3 + uploads_to_adjust_acl_for << upload_to_update + RakeHelpers.print_status_with_label("Analysing which upload posts to rebake.....", i, uploads_to_update.count) + upload_to_update.posts.each do |post| + # don't want unnecessary double-ups + next if posts_to_rebake.key?(post.id) + posts_to_rebake[post.id] = post + end + + # some uploads will be marked as not secure here. + # we need to address this with upload_ids_to_mark_as_not_secure + # e.g. turning off SiteSetting.login_required + if mark_secure_in_loop_because_no_login_required + if upload_to_update.secure? + upload_ids_to_mark_as_secure << upload_to_update.id + else + upload_ids_to_mark_as_not_secure << upload_to_update.id + end end i += 1 end - RakeHelpers.print_status_with_label("Determination complete! ", i, uploads_with_supported_media.count) + RakeHelpers.print_status_with_label("Analysis complete! ", i, uploads_to_update.count) puts "" - [upload_ids_to_mark_as_secure, posts_to_rebake] + [upload_ids_to_mark_as_secure, upload_ids_to_mark_as_not_secure, posts_to_rebake, uploads_to_adjust_acl_for] end def inline_uploads(post) diff --git a/lib/topic_publisher.rb b/lib/topic_publisher.rb index b78b58bd11..63afb8dede 100644 --- a/lib/topic_publisher.rb +++ b/lib/topic_publisher.rb @@ -36,7 +36,7 @@ class TopicPublisher op = @topic.first_post if op.present? - op.revisions.delete_all + op.revisions.destroy_all op.update_columns( version: 1, diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 86c126f2fe..f77d37e746 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -19,12 +19,6 @@ class TopicQuery int.call(x) && x.to_i.between?(0, PG_MAX_INT) end - array_int_or_int = lambda do |x| - int.call(x) || ( - Array === x && x.length > 0 && x.all?(&int) - ) - end - { max_posts: zero_up_to_max_int, min_posts: zero_up_to_max_int, @@ -675,20 +669,11 @@ class TopicQuery if options[:no_subcategories] result = result.where('categories.id = ?', category_id) else - sql = <<~SQL - categories.id IN ( - WITH RECURSIVE subcategories AS ( - SELECT :category_id id, 1 depth - UNION - SELECT categories.id, (subcategories.depth + 1) depth - FROM categories - JOIN subcategories ON subcategories.id = categories.parent_category_id - WHERE subcategories.depth < :max_category_nesting - ) - SELECT subcategories.id FROM subcategories - ) AND (categories.id = :category_id OR topics.id != categories.topic_id) + result = result.where(<<~SQL, subcategory_ids: subcategory_ids(category_id), category_id: category_id) + categories.id in (:subcategory_ids) AND ( + categories.topic_id <> topics.id OR categories.id = :category_id + ) SQL - result = result.where(sql, category_id: category_id, max_category_nesting: SiteSetting.max_category_nesting) end result = result.references(:categories) @@ -1067,6 +1052,29 @@ class TopicQuery private + def subcategory_ids(category_id) + @subcategory_ids ||= {} + @subcategory_ids[category_id] ||= + begin + sql = <<~SQL + WITH RECURSIVE subcategories AS ( + SELECT :category_id id, 1 depth + UNION + SELECT categories.id, (subcategories.depth + 1) depth + FROM categories + JOIN subcategories ON subcategories.id = categories.parent_category_id + WHERE subcategories.depth < :max_category_nesting + ) + SELECT id FROM subcategories + SQL + DB.query_single( + sql, + category_id: category_id, + max_category_nesting: SiteSetting.max_category_nesting + ) + end + end + def sanitize_sql_array(input) ActiveRecord::Base.public_send(:sanitize_sql_array, input.join(',')) end diff --git a/lib/upload_security.rb b/lib/upload_security.rb index d2e11d9eef..2c9d2f2386 100644 --- a/lib/upload_security.rb +++ b/lib/upload_security.rb @@ -57,9 +57,6 @@ class UploadSecurity # if there is no access control post id and the upload is currently secure, we # do not want to make it un-secure to avoid unintentionally exposing it def access_control_post_has_secure_media? - # if the post is deleted the access_control_post will be blank... - # TODO: deal with this in a better way - return false if @upload.access_control_post.blank? @upload.access_control_post.with_secure_media? end diff --git a/lib/version.rb b/lib/version.rb index 85a9a0503c..2d8503a967 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -9,7 +9,7 @@ module Discourse MAJOR = 2 MINOR = 5 TINY = 0 - PRE = 'beta1' + PRE = 'beta2' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/plugins/discourse-internet-explorer/public/js/ie.js b/plugins/discourse-internet-explorer/public/js/ie.js index 4c0fbf5fd8..6c998e402a 100644 --- a/plugins/discourse-internet-explorer/public/js/ie.js +++ b/plugins/discourse-internet-explorer/public/js/ie.js @@ -970,3 +970,12 @@ if (!String.prototype.startsWith) { return ES6; }); /* eslint-enable */ + +// Polyfill Promise - used by popper.js +window.addEventListener( + "load", + function() { + window.Promise = require("rsvp").Promise; + }, + false +); diff --git a/plugins/discourse-local-dates/config/locales/client.bs_BA.yml b/plugins/discourse-local-dates/config/locales/client.bs_BA.yml index de9010fbb9..3fcb6e6c27 100644 --- a/plugins/discourse-local-dates/config/locales/client.bs_BA.yml +++ b/plugins/discourse-local-dates/config/locales/client.bs_BA.yml @@ -8,6 +8,13 @@ bs_BA: js: discourse_local_dates: + relative_dates: + today: Danas%{time} + tomorrow: Sutra %{time} + yesterday: Jučer %{time} + countdown: + passed: datum je prošao + title: Unesi datum / vrijeme create: form: insert: Unesi @@ -17,7 +24,20 @@ bs_BA: timezones_title: Vremenske zone za prikazati timezones_description: Vremenske zone će biti korištene kako bi prikazale datume u pregledu i u nazad. recurring_title: Vraćanje + recurring_description: "Definiši ponavljanje eventa. Možete također menuelno izmijeniti opcije ponavljanja generisanih od strane foruma i koristiti jedan od sljedećih ključeva: godine, kvartali, mjeseci, sedmice, dani, sati, minute, sekunde, milisekunde." + recurring_none: Bez ponavljanja invalid_date: Neispravan datum, osigurajte da su datum i vrijeme tačni date_title: Datum time_title: Vrijeme format_title: Format datuma + timezone: Vremenska zona + until: Sve do... + recurring: + every_day: "Svaki dan" + every_week: "Svake sedmice" + every_two_weeks: "Svake dvije sedmice" + every_month: "Svaki mjesec" + every_two_months: "Svaka dva mjeseca" + every_three_months: "Svaka tri mjeseca" + every_six_months: "Svakih šest mjeseci" + every_year: "Svake godine" diff --git a/plugins/discourse-local-dates/config/locales/client.fa_IR.yml b/plugins/discourse-local-dates/config/locales/client.fa_IR.yml index b2214bb0f1..b67c306f03 100644 --- a/plugins/discourse-local-dates/config/locales/client.fa_IR.yml +++ b/plugins/discourse-local-dates/config/locales/client.fa_IR.yml @@ -22,3 +22,4 @@ fa_IR: date_title: تاریخ time_title: زمان format_title: فرمت تاریخ + timezone: منطقه ی زمانی diff --git a/plugins/discourse-narrative-bot/config/locales/server.bs_BA.yml b/plugins/discourse-narrative-bot/config/locales/server.bs_BA.yml index d912946b5e..57e1762b4f 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.bs_BA.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.bs_BA.yml @@ -26,6 +26,14 @@ bs_BA: Ovaj bedž je dodjeljen prilikom uspješnog završetka interaktivnog tutorijala za nove korisnike. Postali ste majstor naprednih alata u diskusiji - sada ste potpuno licencirani! discourse_narrative_bot: bio: "Pozdrav, ja nisam stvarna osoba. Ja sam automatski robot koji će vas naučiti kako da koristite ovaj web sajt. Kako bi komunicirali sa mnom, pošaljite mi poruku ili me spomenite pisajući **`@%{discobot_username}`** bilo gdje." + tl2_promotion_message: + subject_template: "Čestitamo na povišenju vašeg nivoa povjerenja!" + text_body_template: | + Sada kada vam je povišen nivo povjerenja, došlo je vrijeme da naučite neke napredne mogućnosti! + + Odgovori na ovu poruku sa `@%{discobot_username} %{reset_trigger}` kako bi saznali nešto više o tome šta je moguće sve raditi. + + Pozivamo vas da se i dalje ovdje uključite - uživamo što vas imamo ovdje na forumu. timeout: message: |- Hej @%{username}, samo vas provjeravam jer nisam vas čuo neko vrijeme. @@ -113,6 +121,20 @@ bs_BA: random_mention: reply: |- Pozdrav! Kako bi saznali šta sve mogu raditi, recite `@%{discobot_username} %{help_trigger}`. + tracks: |- + Trenutno znam kako uraditi sljedeće stvari: + + `@%{discobot_username} %{reset_trigger} {ime-tutorijala}` + > Starta interaktivni tutorijal. `{ime-tutorijala}` može biti jedan od `%{tracks}`. + bot_actions: |- + `@%{discobot_username} %{dice_trigger} 2d6` + > :game_die: 3, 6 + + `@%{discobot_username} %{quote_trigger}` + %{quote_sample} + + `@%{discobot_username} %{magic_8_ball_trigger}` + > :crystal_ball: Možeš se na to osloniti do_not_understand: first_response: |- Hej, hvala za odgovor! @@ -190,6 +212,12 @@ bs_BA: search: instructions: |- _psst_… Sakrio sam iznenađenje u ovoj temi. Ako ste spremni za izazov, ** odaberite ikonu za pretraživanje ** gore desno ↗ da ga potražite. Pokušajte tražiti pojam "capy​bara" u ovoj temi + hidden_message: |- + Kako ste uspjeli propustiti ovu kapibaru? :wink: + + + + Dali ste primjetili da ste sada na početku? Nahranite ovog jadnog gladnog kapibaru tako što će te **odgovoriti sa `%{search_answer}` emoji-em** i bit će te automatski vraćeni na kraj. reply: |- Yay ste ga pronašli: tada: - Za detaljnije pretrage, pređite na [full search page] (%{search_url}). - Da biste skočili bilo gdje u dugoj diskusiji, pokušajte kontrolirati vremensku liniju teme desno (i na dnu, na mobitelu). - Ako imate fizičku tipkovnicu: pritisnite ? da biste videli naše korisne prečice na tastaturi. not_found: |- diff --git a/plugins/discourse-narrative-bot/config/locales/server.nl.yml b/plugins/discourse-narrative-bot/config/locales/server.nl.yml index 55c0363946..da479e2934 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.nl.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.nl.yml @@ -95,6 +95,11 @@ nl: random_mention: reply: |- Hallo! Zeg `@%{discobot_username} %{help_trigger}` om te ontdekken wat ik kan. + tracks: |- + Momenteel kan ik de volgende dingen doen: + + `@%{discobot_username} %{reset_trigger} {name-of-tutorial}` + > Start een interactieve handleiding. `{name-of-tutorial}` kan één zijn van: `%{tracks}`. do_not_understand: first_response: |- Hallo, bedankt voor het antwoord! @@ -103,8 +108,26 @@ nl: track_response: U kunt het opnieuw proberen, of als u deze stap wilt overslaan, `%{skip_trigger}` zeggen. Zeg anders `%{reset_trigger}` om opnieuw te beginnen. new_user_narrative: reset_trigger: "handleiding" + title: "Certificaat voor het invullen van een handleiding voor nieuwe gebruikers" + cert_title: "Als erkenning voor de succesvolle afronding van de gebruikershandleiding voor nieuwe gebruikers" hello: title: "Gegroet!" + formatting: + reply: |- + Uitstekend werk! HTML en BBCode werken ook voor de opmaak – om meer te leren, [probeer deze handleiding](http://commonmark.org/help) :nerd: + not_found: |- + Oh, ik vond geen opmaak in je antwoord. :pencil2: + + Kan je opnieuw proberen? Gebruik de B vet of I cursief knoppen in de editor als je vast komt te zitten. + search: + reply: |- + Joepie! Je hebt het gevonden :tada: + + - Voor meer gedetailleerde zoekopdrachten gaat u naar de [geavanceerde zoekpagina](%{search_url}). + + - Om naar eender waar in een lange discussie te springen, probeer de tijdslijnbediening aan de rechterzijde (en onderaan, op mobiele aparaten). + + - Indien je een fysiek toetsenbord hebt :keyboard:, druk ? om onze handige snelkoppelingen te bekijken. advanced_user_narrative: reset_trigger: "geavanceerde handleiding" title: ":arrow_up: Geavanceerde gebruikersfuncties" diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb index 8da5cdc059..185d472ea6 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/advanced_user_narrative.rb @@ -82,7 +82,7 @@ module DiscourseNarrativeBot }, tutorial_poll: { - prerequisite: Proc.new { SiteSetting.poll_enabled }, + prerequisite: Proc.new { SiteSetting.poll_enabled && @user.has_trust_level?(SiteSetting.poll_minimum_trust_level_to_create) }, next_state: :tutorial_details, next_instructions: Proc.new { I18n.t("#{I18N_KEY}.details.instructions", i18n_post_args) }, reply: { diff --git a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb index d9ea4c9e2e..28fec30417 100644 --- a/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb +++ b/plugins/discourse-narrative-bot/spec/discourse_narrative_bot/advanced_user_narrative_spec.rb @@ -556,12 +556,29 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do end end - describe 'when poll is disabled' do - before do + describe 'when user cannot create polls' do + it 'should create the right reply (polls disabled)' do SiteSetting.poll_enabled = false + + TopicUser.change( + user.id, + topic.id, + notification_level: TopicUser.notification_levels[:tracking] + ) + + expected_raw = <<~RAW + #{I18n.t('discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.reply', base_uri: '')} + + #{I18n.t('discourse_narrative_bot.advanced_user_narrative.details.instructions', base_uri: '')} + RAW + + expect(Post.last.raw).to eq(expected_raw.chomp) + expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_details) end - it 'should create the right reply' do + it 'should create the right reply (insufficient trust level)' do + user.update(trust_level: 0) + TopicUser.change( user.id, topic.id, @@ -589,6 +606,19 @@ RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do ) end + it 'allows new users to create polls' do + user.update(trust_level: 0) + + post = PostCreator.create(user, topic_id: topic.id, raw: <<~RAW) + [poll type=regular] + * foo + * bar + [/poll] + RAW + + expect(post.errors[:base].size).to eq(0) + end + describe 'when post is not in the right topic' do it 'should not do anything' do other_post diff --git a/plugins/poll/assets/stylesheets/common/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss index 4ab9e427ab..8306f7bfcf 100644 --- a/plugins/poll/assets/stylesheets/common/poll.scss +++ b/plugins/poll/assets/stylesheets/common/poll.scss @@ -67,7 +67,9 @@ div.poll { } .poll-voters:not(:empty) { + min-height: 30px; margin-bottom: 0.25em; + li { display: inline; } @@ -76,6 +78,10 @@ div.poll { .poll-voters-toggle-expand { width: 100%; text-align: center; + + .spinner { + margin-top: 0.25em; + } } .results { diff --git a/plugins/poll/config/locales/client.bs_BA.yml b/plugins/poll/config/locales/client.bs_BA.yml index c343660c22..379da2075a 100644 --- a/plugins/poll/config/locales/client.bs_BA.yml +++ b/plugins/poll/config/locales/client.bs_BA.yml @@ -17,14 +17,25 @@ bs_BA: few: "ukupno glasova" other: "ukupno glasova" average_rating: "Prosječna ocjena: %{average}." + public: + title: "Glasanje je javno." + results: + groups: + title: "Morate biti pripadnik %{groups} kako bi ste mogli glasati u ovoj anketi." + vote: + title: "Rezultati će biti prikazani pritiskom naglasaj." + closed: + title: "Rezultati će biti prikazani onda kada glasanje bude završeno." + staff: + title: "Rezultati su prikazani samo uredništvu i admin korisnicima." multiple: help: at_least_min_options: one: "Odaberi barem %{count} opciju" - few: "Odaberi barem %{name} opcije" - other: "Odaberi barem %{name} opcija" + few: "Odaberi barem %{count} opcije" + other: "Odaberi barem %{count} opcija" up_to_max_options: - one: "Odaberi do %{count} opcije" + one: "Odaberi do %{count} opciju" few: "Odaberi do %{count} opcije" other: "Odaberi do %{count} opcija" x_options: @@ -40,7 +51,15 @@ bs_BA: label: "Prikaži rezultate" hide-results: title: "Nazad na glasove" + label: "Prikaži glasanje" + group-results: + title: "Grupiraj glasove po polju korisnika" + label: "Prikaži raspodjelu" + ungroup-results: + title: "Kombiniraj sve glasove" + label: "Sakrij raspodjelu" export-results: + title: "Izvezi rezultate ankete (Export)" label: "Izvoz" open: title: "Otvori anketu" @@ -50,13 +69,18 @@ bs_BA: title: "Zatvori anketu" label: "Zatvori" confirm: "Da li ste sigurni da želite da zatvorite ovu anketu?" + automatic_close: + closes_in: "Završava za %{timeLeft}." + age: "Završeno %{age}" error_while_toggling_status: "Izvinjavamo se, pojavio se problem u prebacivanju statutusa ove ankete" error_while_casting_votes: "Izvinjavamo se, pojavila se greška prikazivanja vaših glasova" error_while_fetching_voters: "Izvinjavamo se, pojavila se greška pri prikazivanju glasača" + error_while_exporting_results: "Izvinjavamo se, došlo je do greške izvoza rezultata vaše ankete." ui_builder: title: Izgradi anketu insert: Umetni anketu help: + options_count: Unesite barem jednu opciju invalid_values: Minimalna vrijednost mora biti manja od maksimalne. min_step_value: Minimalna vrijednost razmaka je 1 poll_type: @@ -66,6 +90,14 @@ bs_BA: number: Rejting broja poll_result: label: Rezultati + always: Uvjek vidljivo + vote: Prilikom glasanja + closed: Kada se završi + staff: Samo za urednike i admine + poll_groups: + label: Dozvoljene grupe + poll_chart_type: + label: Tip chart grafikona poll_config: max: Maksimalno min: Minimalno diff --git a/plugins/poll/config/locales/server.bs_BA.yml b/plugins/poll/config/locales/server.bs_BA.yml index a347f6a1dc..78bf651b41 100644 --- a/plugins/poll/config/locales/server.bs_BA.yml +++ b/plugins/poll/config/locales/server.bs_BA.yml @@ -11,11 +11,15 @@ bs_BA: poll_maximum_options: "Maksimalan broj dozvoljenih opcija u anketi." poll_edit_window_mins: "Broj minuta nakon kreiranja posta tokom kojih se mogu uređivati ankete." poll_minimum_trust_level_to_create: "Definišite minimalni nivo povjerenja potreban za kreiranje anketa." + poll_groupable_user_fields: "Set polja korisničkih imena koji može biti iskorišten za grupisanje i filtriranje rezultata ankete." + poll_export_data_explorer_query_id: "ID od Data Explorer Query koji se koristi za izvoz (export) rezultata ankete (0 za isključi)." poll: poll: "anketa" invalid_argument: "Nevažeća vrijednost '%{value}' za argument '%{argument}'." multiple_polls_without_name: "Postoji više anketa bez imena. Koristite atribut „ name “ da biste jedinstveno identifikovali vaše ankete." multiple_polls_with_same_name: "Postoji više anketa sa istim imenom: %{name} . Koristite atribut „ name “ da biste jedinstveno identifikovali vaše ankete." + default_poll_must_have_at_least_1_option: "Anketa mora imati barem 1 opciju." + named_poll_must_have_at_least_1_option: "Anketa imenovana %{name} mora imati barem 1 opciju." default_poll_must_have_less_options: one: "Anketa mora imati manje od %{count} opcije." few: "Anketa mora imati manje od %{count} opcija." @@ -26,6 +30,8 @@ bs_BA: other: "Anketa nazvana %{name} mora imati manje od %{count} opcija." default_poll_must_have_different_options: "Anketa mora imati različite opcije." named_poll_must_have_different_options: "Anketa nazvana %{name} mora imati različite opcije." + default_poll_must_not_have_any_empty_options: "Anketa ne smije imati praznu opciju." + named_poll_must_not_have_any_empty_options: "Anketa imenovana sa %{name} ne smije imati prazne opcije." default_poll_with_multiple_choices_has_invalid_parameters: "Anketa sa više izbora ima nevažeće parametre." named_poll_with_multiple_choices_has_invalid_parameters: "Anketa nazvana %{name} sa višestrukim izborom ima nevažeće parametre." requires_at_least_1_valid_option: "Morate odabrati najmanje 1 važeću opciju." @@ -42,3 +48,5 @@ bs_BA: insufficient_rights_to_create: "Nije vam dozvoljeno da kreirate ankete." email: link_to_poll: "Kliknite za pregled ankete." + user_field: + no_data: "Bez podataka" diff --git a/plugins/poll/lib/post_validator.rb b/plugins/poll/lib/post_validator.rb index 19f6648e4f..788fe1e24b 100644 --- a/plugins/poll/lib/post_validator.rb +++ b/plugins/poll/lib/post_validator.rb @@ -9,7 +9,7 @@ module DiscoursePoll def validate_post min_trust_level = SiteSetting.poll_minimum_trust_level_to_create - if @post&.user&.staff? || @post&.user&.trust_level >= TrustLevel[min_trust_level] + if @post&.user&.staff? || @post&.user&.trust_level >= TrustLevel[min_trust_level] || @post&.topic&.pm_with_non_human_user? true else @post.errors.add(:base, I18n.t("poll.insufficient_rights_to_create")) diff --git a/plugins/poll/spec/controllers/posts_controller_spec.rb b/plugins/poll/spec/controllers/posts_controller_spec.rb index 411247c8f7..8f9dae14a2 100644 --- a/plugins/poll/spec/controllers/posts_controller_spec.rb +++ b/plugins/poll/spec/controllers/posts_controller_spec.rb @@ -348,6 +348,23 @@ describe PostsController do json = ::JSON.parse(response.body) expect(json["errors"][0]).to eq(I18n.t("poll.insufficient_rights_to_create")) end + + it "skips the check in PMs with bots" do + user = Fabricate(:user, trust_level: 1) + topic = Fabricate(:private_message_topic, topic_allowed_users: [ + Fabricate.build(:topic_allowed_user, user: user), + Fabricate.build(:topic_allowed_user, user: Discourse.system_user) + ]) + Fabricate(:post, topic_id: topic.id, user_id: Discourse::SYSTEM_USER_ID) + + log_in_user(user) + + post :create, params: { + topic_id: topic.id, raw: "[poll]\n- A\n- B\n[/poll]" + }, format: :json + + expect(::JSON.parse(response.body)["errors"]).to eq(nil) + end end describe "regular user with equal trust level" do diff --git a/script/import_scripts/google_groups.rb b/script/import_scripts/google_groups.rb index 6e3f2f0969..d8f58cf323 100755 --- a/script/import_scripts/google_groups.rb +++ b/script/import_scripts/google_groups.rb @@ -158,12 +158,11 @@ def login get("https://google.com/404") add_cookies( - "accounts.google.com", "myaccount.google.com", "google.com" ) - get("https://accounts.google.com/servicelogin") + get("https://myaccount.google.com/?utm_source=sign_in_no_continue") begin wait_for_url { |url| url.start_with?("https://myaccount.google.com") } diff --git a/script/import_scripts/simplepress.rb b/script/import_scripts/simplepress.rb index 6932338009..3375258790 100644 --- a/script/import_scripts/simplepress.rb +++ b/script/import_scripts/simplepress.rb @@ -195,6 +195,11 @@ class ImportScripts::SimplePress < ImportScripts::Base def process_simplepress_post(raw, import_id) s = raw.dup + # fix invalid byte sequence in UTF-8 (ArgumentError) + unless s.valid_encoding? + s.force_encoding("UTF-8") + end + # convert the quote line s.gsub!(/\[quote='([^']+)'.*?pid='(\d+).*?\]/) { "[quote=\"#{convert_username($1, import_id)}, " + post_id_to_post_num_and_topic($2, import_id) + '"]' diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 779c829ed4..d5bbb9fef5 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -485,7 +485,8 @@ describe PostCreator do it 'whispers do not mess up the public view' do - freeze_time + # turns out this can fail on leap years if we don't do this + freeze_time DateTime.parse('2010-01-01 12:00') first = PostCreator.new( user, @@ -1414,7 +1415,7 @@ describe PostCreator do end it "links post uploads" do - public_post = PostCreator.create( + _public_post = PostCreator.create( user, topic_id: public_topic.id, raw: "A public post with an image.\n![](#{image_upload.short_path})" diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index 7e5590c989..c4578148d7 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -315,56 +315,53 @@ describe Search do TopicAllowedUser.create!(user_id: u2.id, topic_id: private_topic.id) # private only - results = Search.execute('cheese', - type_filter: 'all_topics', + results = Search.execute('in:all cheese', guardian: Guardian.new(u1)) expect(results.posts.length).to eq(1) # public only - results = Search.execute('eggs', - type_filter: 'all_topics', + results = Search.execute('in:all eggs', guardian: Guardian.new(u1)) expect(results.posts.length).to eq(1) # both - results = Search.execute('spam', - type_filter: 'all_topics', + results = Search.execute('in:all spam', guardian: Guardian.new(u1)) expect(results.posts.length).to eq(2) + # for anon + results = Search.execute('in:all spam', + guardian: Guardian.new) + expect(results.posts.length).to eq(1) + # nonparticipatory user - results = Search.execute('cheese', - type_filter: 'all_topics', + results = Search.execute('in:all cheese', guardian: Guardian.new(u3)) expect(results.posts.length).to eq(0) - results = Search.execute('eggs', - type_filter: 'all_topics', + results = Search.execute('in:all eggs', guardian: Guardian.new(u3)) expect(results.posts.length).to eq(1) - results = Search.execute('spam', - type_filter: 'all_topics', + results = Search.execute('in:all spam', guardian: Guardian.new(u3)) expect(results.posts.length).to eq(1) # Admin doesn't see private topic - results = Search.execute('spam', - type_filter: 'all_topics', + results = Search.execute('in:all spam', guardian: Guardian.new(u4)) expect(results.posts.length).to eq(1) # same keyword for different users - results = Search.execute('ham', - type_filter: 'all_topics', + results = Search.execute('in:all ham', guardian: Guardian.new(u1)) expect(results.posts.length).to eq(2) - results = Search.execute('ham', - type_filter: 'all_topics', + + results = Search.execute('in:all ham', guardian: Guardian.new(u2)) expect(results.posts.length).to eq(2) - results = Search.execute('ham', - type_filter: 'all_topics', + + results = Search.execute('in:all ham', guardian: Guardian.new(u3)) expect(results.posts.length).to eq(1) end diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index b4c5cd9345..3793da7579 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -145,7 +145,7 @@ describe TopicQuery do list = TopicQuery.new(moderator, category: diff_category.slug).list_latest expect(list.topics.size).to eq(1) - expect(list.preload_key).to eq("topic_list_c/different-category/l/latest") + expect(list.preload_key).to eq("topic_list_c/different-category/#{diff_category.id}/l/latest") # Defaults to no category filter when slug does not exist expect(TopicQuery.new(moderator, category: 'made up slug').list_latest.topics.size).to eq(2) diff --git a/spec/fixtures/csv/usernames_with_nil_values.csv b/spec/fixtures/csv/usernames_with_nil_values.csv new file mode 100644 index 0000000000..b35b163ca5 --- /dev/null +++ b/spec/fixtures/csv/usernames_with_nil_values.csv @@ -0,0 +1,4 @@ +username1, +username2, +username3, +,, diff --git a/spec/initializers/track_setting_changes_spec.rb b/spec/initializers/track_setting_changes_spec.rb new file mode 100644 index 0000000000..82b7963ec4 --- /dev/null +++ b/spec/initializers/track_setting_changes_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Setting changes' do + describe '#must_approve_users' do + before { SiteSetting.must_approve_users = false } + + it 'does not approve a user with associated reviewables' do + user_pending_approval = Fabricate(:reviewable_user).target + + SiteSetting.must_approve_users = true + + expect(user_pending_approval.reload.approved?).to eq(false) + end + + it 'approves a user with no associated reviewables' do + non_approved_user = Fabricate(:user, approved: false) + + SiteSetting.must_approve_users = true + + expect(non_approved_user.reload.approved?).to eq(true) + end + end +end diff --git a/spec/jobs/publish_topic_to_category_spec.rb b/spec/jobs/publish_topic_to_category_spec.rb index 4b9a526b12..3602eaa5b3 100644 --- a/spec/jobs/publish_topic_to_category_spec.rb +++ b/spec/jobs/publish_topic_to_category_spec.rb @@ -84,4 +84,22 @@ RSpec.describe Jobs::PublishTopicToCategory do expect(message.data[:refresh_stream]).to be_present end end + + describe 'when new category has a default auto-close' do + before do + another_category.update!(auto_close_hours: 5) + end + + it 'should apply the auto-close timer upon publishing' do + topic + + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id) + + topic.reload + topic_timer = topic.public_topic_timer + expect(topic.category).to eq(another_category) + expect(topic_timer.status_type).to eq(TopicTimer.types[:close]) + expect(topic_timer.execute_at).to be_within(1.second).of(5.hours.from_now) + end + end end diff --git a/spec/jobs/pull_hotlinked_images_spec.rb b/spec/jobs/pull_hotlinked_images_spec.rb index 4040674450..6e3a21ca9a 100644 --- a/spec/jobs/pull_hotlinked_images_spec.rb +++ b/spec/jobs/pull_hotlinked_images_spec.rb @@ -83,6 +83,17 @@ describe Jobs::PullHotlinkedImages do RAW end + it 'replaces correct image URL' do + url = image_url.sub("/2e/Longcat1.png", '') + post = Fabricate(:post, raw: "[Images](#{url})\n![](#{image_url})") + + expect do + Jobs::PullHotlinkedImages.new.execute(post_id: post.id) + end.to change { Upload.count }.by(1) + + expect(post.reload.raw).to eq("[Images](#{url})\n![](#{Upload.last.short_url})") + end + it 'replaces images without protocol' do url = image_url.sub(/^https?\:/, '') post = Fabricate(:post, raw: "test") diff --git a/spec/jobs/regular/update_private_uploads_acl_spec.rb b/spec/jobs/regular/update_private_uploads_acl_spec.rb index 25d6fe07d0..9c51ef734f 100644 --- a/spec/jobs/regular/update_private_uploads_acl_spec.rb +++ b/spec/jobs/regular/update_private_uploads_acl_spec.rb @@ -24,9 +24,9 @@ describe Jobs::UpdatePrivateUploadsAcl do before do SiteSetting.login_required = true SiteSetting.prevent_anons_from_downloading_files = true - SiteSetting::Upload.stubs(:enable_s3_uploads).returns(true) Discourse.stubs(:store).returns(stub(external?: false)) - SiteSetting.stubs(:secure_media?).returns(true) + enable_s3_uploads([upload]) + SiteSetting.secure_media = true end it "changes the upload to secure" do @@ -35,4 +35,20 @@ describe Jobs::UpdatePrivateUploadsAcl do end end end + + def enable_s3_uploads(uploads) + SiteSetting.enable_s3_uploads = true + SiteSetting.s3_upload_bucket = "s3-upload-bucket" + SiteSetting.s3_access_key_id = "some key" + SiteSetting.s3_secret_access_key = "some secrets3_region key" + + stub_request(:head, "https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/") + + uploads.each do |upload| + stub_request( + :put, + "https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/original/1X/#{upload.sha1}.#{upload.extension}?acl" + ) + end + end end diff --git a/spec/lib/backup_restore/database_restorer_spec.rb b/spec/lib/backup_restore/database_restorer_spec.rb index 6d79dff9ef..02666093b0 100644 --- a/spec/lib/backup_restore/database_restorer_spec.rb +++ b/spec/lib/backup_restore/database_restorer_spec.rb @@ -37,6 +37,7 @@ describe BackupRestore::DatabaseRestorer do def expect_db_migrate Discourse::Utils.expects(:execute_command).with do |env, command, options| env["SKIP_POST_DEPLOYMENT_MIGRATIONS"] == "0" && + env["SKIP_OPTIMIZE_ICONS"] == "1" && command == "rake db:migrate" && options[:chdir] == Rails.root end.once diff --git a/spec/lib/site_settings/validations_spec.rb b/spec/lib/site_settings/validations_spec.rb index 7f346d4d5c..69ab73920b 100644 --- a/spec/lib/site_settings/validations_spec.rb +++ b/spec/lib/site_settings/validations_spec.rb @@ -103,6 +103,15 @@ describe SiteSettings::Validations do SiteSetting.s3_backup_bucket = "my-awesome-bucket/foo" expect { validate("my-awesome-bucket/foo/uploads") }.to raise_error(Discourse::InvalidParameters, error_message) end + + it "cannot be made blank unless the setting is false" do + SiteSetting.s3_backup_bucket = "really-real-cool-bucket" + SiteSetting.enable_s3_uploads = true + + expect { validate("") }.to raise_error(Discourse::InvalidParameters) + SiteSetting.enable_s3_uploads = false + validate("") + end end end diff --git a/spec/lib/upload_security_spec.rb b/spec/lib/upload_security_spec.rb index 3dc812bbdc..eabf061678 100644 --- a/spec/lib/upload_security_spec.rb +++ b/spec/lib/upload_security_spec.rb @@ -97,11 +97,20 @@ RSpec.describe UploadSecurity do context "when the access control post has_secure_media?" do before do - upload.update(access_control_post: post_in_secure_context) + upload.update(access_control_post_id: post_in_secure_context.id) end it "returns true" do expect(subject.should_be_secure?).to eq(true) end + + context "when the post is deleted" do + before do + post_in_secure_context.trash! + end + it "still determines whether the post has secure media; returns true" do + expect(subject.should_be_secure?).to eq(true) + end + end end context "when uploading in the composer" do diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 73bfdc330b..e882c3fd05 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -618,6 +618,15 @@ describe Topic do expect(Post.last.action_code).to eq("removed_user") end + it 'should not create a small action if user is already invited through a group' do + group = Fabricate(:group, users: [user, another_user]) + expect(topic.invite_group(user, group)).to eq(true) + + expect { topic.invite(user, another_user.username) } + .to change { Notification.count }.by(1) + .and change { Post.where(post_type: Post.types[:small_action]).count }.by(0) + end + context "from a muted user" do before { MutedUser.create!(user: another_user, muted_user: user) } @@ -1114,6 +1123,14 @@ describe Topic do context 'closed' do let(:status) { 'closed' } it_should_behave_like 'a status that closes a topic' + + it 'should archive group message' do + group = Fabricate(:group) + group.add(@user) + topic = Fabricate(:private_message_topic, allowed_groups: [group]) + + expect { topic.update_status(status, true, @user) }.to change(topic.group_archived_messages, :count).by(1) + end end context 'autoclosed' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ee1bea598a..d23451fcfa 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2287,4 +2287,32 @@ describe User do expect(user.approved).to eq(true) end end + + describe "#recent_time_read" do + fab!(:user) { Fabricate(:user) } + fab!(:user2) { Fabricate(:user) } + + before_all do + UserVisit.create(user_id: user.id, visited_at: 1.minute.ago, posts_read: 1, mobile: false, time_read: 10) + UserVisit.create(user_id: user.id, visited_at: 2.days.ago, posts_read: 1, mobile: false, time_read: 20) + UserVisit.create(user_id: user.id, visited_at: 1.week.ago, posts_read: 1, mobile: false, time_read: 30) + UserVisit.create(user_id: user.id, visited_at: 1.year.ago, posts_read: 1, mobile: false, time_read: 40) # Old, should be ignored + UserVisit.create(user_id: user2.id, visited_at: 1.minute.ago, posts_read: 1, mobile: false, time_read: 50) + end + + it "calculates correctly" do + expect(user.recent_time_read).to eq(60) + expect(user2.recent_time_read).to eq(50) + end + + it "preloads correctly" do + User.preload_recent_time_read([user, user2]) + + expect(user.instance_variable_get(:@recent_time_read)).to eq(60) + expect(user2.instance_variable_get(:@recent_time_read)).to eq(50) + + expect(user.recent_time_read).to eq(60) + expect(user2.recent_time_read).to eq(50) + end + end end diff --git a/spec/requests/admin/badges_controller_spec.rb b/spec/requests/admin/badges_controller_spec.rb index 06d294bcc0..f54875a21a 100644 --- a/spec/requests/admin/badges_controller_spec.rb +++ b/spec/requests/admin/badges_controller_spec.rb @@ -179,6 +179,8 @@ describe Admin::BadgesController do end describe '#mass_award' do + before { @user = Fabricate(:user, email: 'user1@test.com', username: 'username1') } + it 'does nothing when there is no file' do post "/admin/badges/award/#{badge.id}.json", params: { file: '' } @@ -202,23 +204,31 @@ describe Admin::BadgesController do it 'awards the badge using a list of user emails' do Jobs.run_immediately! - user = Fabricate(:user, email: 'user1@test.com') file = file_from_fixtures('user_emails.csv', 'csv') post "/admin/badges/award/#{badge.id}.json", params: { file: fixture_file_upload(file) } - expect(UserBadge.exists?(user: user, badge: badge)).to eq(true) + expect(UserBadge.exists?(user: @user, badge: badge)).to eq(true) end it 'awards the badge using a list of usernames' do Jobs.run_immediately! - user = Fabricate(:user, username: 'username1') file = file_from_fixtures('usernames.csv', 'csv') post "/admin/badges/award/#{badge.id}.json", params: { file: fixture_file_upload(file) } - expect(UserBadge.exists?(user: user, badge: badge)).to eq(true) + expect(UserBadge.exists?(user: @user, badge: badge)).to eq(true) + end + + it 'works with a CSV containing nil values' do + Jobs.run_immediately! + + file = file_from_fixtures('usernames_with_nil_values.csv', 'csv') + + post "/admin/badges/award/#{badge.id}.json", params: { file: fixture_file_upload(file) } + + expect(UserBadge.exists?(user: @user, badge: badge)).to eq(true) end end end diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index 49a730ef64..92b79ce27f 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -757,50 +757,6 @@ RSpec.describe Admin::UsersController do end end - describe '#invite_admin' do - let(:api_key) { Fabricate(:api_key, user: admin) } - let(:api_params) do - { api_key: api_key.key, api_username: admin.username } - end - - it "doesn't work when not via API" do - post "/admin/users/invite_admin.json", params: { - name: 'Bill', username: 'bill22', email: 'bill@bill.com' - } - - expect(response.status).to eq(403) - end - - it 'should invite admin' do - expect do - post "/admin/users/invite_admin.json", params: api_params.merge( - name: 'Bill', username: 'bill22', email: 'bill@bill.com' - ) - end.to change { Jobs::CriticalUserEmail.jobs.size }.by(1) - - expect(response.status).to eq(200) - - u = User.find_by_email('bill@bill.com') - expect(u.name).to eq("Bill") - expect(u.username).to eq("bill22") - expect(u.admin).to eq(true) - expect(u.active).to eq(true) - expect(u.approved).to eq(true) - end - - it "doesn't send the email with send_email falsey" do - expect do - post "/admin/users/invite_admin.json", params: api_params.merge( - name: 'Bill', username: 'bill22', email: 'bill@bill.com', send_email: '0' - ) - end.to change { Jobs::CriticalUserEmail.jobs.size }.by(0) - - expect(response.status).to eq(200) - json = ::JSON.parse(response.body) - expect(json["password_url"]).to be_present - end - end - describe '#sync_sso' do let(:sso) { SingleSignOn.new } let(:sso_secret) { "sso secret" } diff --git a/spec/requests/safe_mode_controller_spec.rb b/spec/requests/safe_mode_controller_spec.rb index 542e3ed7d8..6bba0dad93 100644 --- a/spec/requests/safe_mode_controller_spec.rb +++ b/spec/requests/safe_mode_controller_spec.rb @@ -3,6 +3,18 @@ require 'rails_helper' RSpec.describe SafeModeController do + describe 'index' do + it 'never includes customizations' do + theme = Fabricate(:theme) + theme.set_field(target: :common, name: "header", value: "My Custom Header") + theme.save! + theme.set_default! + + get '/safe-mode' + expect(response.body).not_to include("My Custom Header") + end + end + describe 'enter' do context 'when no params are given' do it 'should redirect back to safe mode page' do diff --git a/spec/requests/uploads_controller_spec.rb b/spec/requests/uploads_controller_spec.rb index e27a4d4671..2f3333cacd 100644 --- a/spec/requests/uploads_controller_spec.rb +++ b/spec/requests/uploads_controller_spec.rb @@ -578,7 +578,7 @@ describe UploadsController do expect(result[0]["short_path"]).to eq(upload.short_path) end - it 'does not return secure urls for non-media uploads' do + it 'returns secure urls for non-media uploads' do upload.update!(original_filename: "not-an-image.pdf", extension: "pdf") sign_in(user) @@ -586,7 +586,7 @@ describe UploadsController do expect(response.status).to eq(200) result = JSON.parse(response.body) - expect(result[0]["url"]).not_to match("/secure-media-uploads") + expect(result[0]["url"]).to match("/secure-media-uploads") expect(result[0]["short_path"]).to eq(upload.short_path) end end diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index e2e278bb91..ad5fe6873e 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -130,6 +130,10 @@ module Helpers def capture_stdout old_stdout = $stdout + if ENV['RAILS_ENABLE_TEST_STDOUT'] + yield + return + end io = StringIO.new $stdout = io yield diff --git a/spec/tasks/bookmarks_spec.rb b/spec/tasks/bookmarks_spec.rb index 16fea02f98..68f8588e0b 100644 --- a/spec/tasks/bookmarks_spec.rb +++ b/spec/tasks/bookmarks_spec.rb @@ -17,8 +17,14 @@ RSpec.describe "bookmarks tasks" do create_post_actions_and_existing_bookmarks end + def invoke_task(args = nil) + capture_stdout do + Rake::Task['bookmarks:sync_to_table'].invoke(args) + end + end + it "migrates all PostActions" do - Rake::Task['bookmarks:sync_to_table'].invoke + invoke_task expect(Bookmark.all.count).to eq(3) end @@ -26,14 +32,14 @@ RSpec.describe "bookmarks tasks" do it "does not create bookmarks that already exist in the bookmarks table for a user" do Fabricate(:bookmark, user: user1, post: post1) - Rake::Task['bookmarks:sync_to_table'].invoke + invoke_task expect(Bookmark.all.count).to eq(3) expect(Bookmark.where(post: post1, user: user1).count).to eq(1) end it "respects the sync_limit if provided and stops creating bookmarks at the limit (so this can be run progrssively" do - Rake::Task['bookmarks:sync_to_table'].invoke(1) + invoke_task(1) expect(Bookmark.all.count).to eq(1) end diff --git a/spec/tasks/uploads_spec.rb b/spec/tasks/uploads_spec.rb index 944c78b5a0..21c769a092 100644 --- a/spec/tasks/uploads_spec.rb +++ b/spec/tasks/uploads_spec.rb @@ -6,9 +6,10 @@ RSpec.describe "tasks/uploads" do before do Rake::Task.clear Discourse::Application.load_tasks + SiteSetting.authorized_extensions += "|pdf" end - describe "uploads:ensure_correct_acl" do + describe "uploads:secure_upload_analyse_and_update" do let!(:uploads) do [ multi_post_upload1, @@ -19,6 +20,7 @@ RSpec.describe "tasks/uploads" do let(:multi_post_upload1) { Fabricate(:upload_s3) } let(:upload1) { Fabricate(:upload_s3) } let(:upload2) { Fabricate(:upload_s3) } + let(:upload3) { Fabricate(:upload_s3, original_filename: 'test.pdf') } let!(:post1) { Fabricate(:post) } let!(:post2) { Fabricate(:post) } @@ -29,11 +31,12 @@ RSpec.describe "tasks/uploads" do PostUpload.create(post: post2, upload: multi_post_upload1) PostUpload.create(post: post2, upload: upload1) PostUpload.create(post: post3, upload: upload2) + PostUpload.create(post: post3, upload: upload3) end def invoke_task capture_stdout do - Rake::Task['uploads:ensure_correct_acl'].invoke + Rake::Task['uploads:secure_upload_analyse_and_update'].invoke end end @@ -59,6 +62,7 @@ RSpec.describe "tasks/uploads" do expect(multi_post_upload1.reload.access_control_post).to eq(post1) expect(upload1.reload.access_control_post).to eq(post2) expect(upload2.reload.access_control_post).to eq(post3) + expect(upload3.reload.access_control_post).to eq(post3) end it "sets the upload in the read restricted topic category to secure" do @@ -66,6 +70,7 @@ RSpec.describe "tasks/uploads" do invoke_task expect(upload2.reload.secure).to eq(true) expect(upload1.reload.secure).to eq(false) + expect(upload3.reload.secure).to eq(false) end it "sets the upload in the PM topic to secure" do @@ -86,10 +91,95 @@ RSpec.describe "tasks/uploads" do expect(post2.reload.baked_at).not_to eq(post2_baked) expect(post3.reload.baked_at).not_to eq(post3_baked) end + + context "for an upload that is already secure and does not need to change" do + before do + post3.topic.update(archetype: 'private_message', category: nil) + upload2.update(access_control_post: post3) + upload2.update_secure_status + end + + it "does not rebake the associated post" do + post3_baked = post3.baked_at.to_s + invoke_task + expect(post3.reload.baked_at.to_s).to eq(post3_baked) + end + + it "does not attempt to update the acl" do + Discourse.store.expects(:update_upload_ACL).with(upload2).never + invoke_task + end + end + + context "for an upload that is already secure and is changing to not secure" do + it "changes the upload to not secure and updates the ACL" do + upload_to_mark_not_secure = Fabricate(:upload_s3, secure: true) + post_for_upload = Fabricate(:post) + PostUpload.create(post: post_for_upload, upload: upload_to_mark_not_secure) + enable_s3_uploads(uploads.concat([upload_to_mark_not_secure])) + invoke_task + expect(upload_to_mark_not_secure.reload.secure).to eq(false) + end + end end end end + describe "uploads:disable_secure_media" do + def invoke_task + capture_stdout do + Rake::Task['uploads:disable_secure_media'].invoke + end + end + + before do + enable_s3_uploads(uploads) + SiteSetting.secure_media = true + PostUpload.create(post: post1, upload: upload1) + PostUpload.create(post: post1, upload: upload2) + PostUpload.create(post: post2, upload: upload3) + PostUpload.create(post: post2, upload: upload4) + end + + let!(:uploads) do + [ + upload1, upload2, upload3, upload4, upload5 + ] + end + let(:post1) { Fabricate(:post) } + let(:post2) { Fabricate(:post) } + let(:upload1) { Fabricate(:upload_s3, secure: true, access_control_post: post1) } + let(:upload2) { Fabricate(:upload_s3, secure: true, access_control_post: post1) } + let(:upload3) { Fabricate(:upload_s3, secure: true, access_control_post: post2) } + let(:upload4) { Fabricate(:upload_s3, secure: true, access_control_post: post2) } + let(:upload5) { Fabricate(:upload_s3, secure: false) } + + it "disables the secure media setting" do + invoke_task + expect(SiteSetting.secure_media).to eq(false) + end + + it "updates all secure uploads to secure: false" do + invoke_task + [upload1, upload2, upload3, upload4].each do |upl| + expect(upl.reload.secure).to eq(false) + end + end + + it "rebakes the associated posts" do + baked_post1 = post1.baked_at + baked_post2 = post2.baked_at + invoke_task + expect(post1.reload.baked_at).not_to eq(baked_post1) + expect(post2.reload.baked_at).not_to eq(baked_post2) + end + + it "updates the affected ACLs" do + FileStore::S3Store.any_instance.expects(:update_upload_ACL).times(4) + invoke_task + end + end + def enable_s3_uploads(uploads) SiteSetting.enable_s3_uploads = true SiteSetting.s3_upload_bucket = "s3-upload-bucket" diff --git a/test/javascripts/acceptance/admin-emails-test.js.es6 b/test/javascripts/acceptance/admin-emails-test.js.es6 index f44cf638e9..652dbcf757 100644 --- a/test/javascripts/acceptance/admin-emails-test.js.es6 +++ b/test/javascripts/acceptance/admin-emails-test.js.es6 @@ -1,4 +1,5 @@ import { acceptance } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; acceptance("Admin - Emails", { loggedIn: true }); @@ -16,15 +17,14 @@ Hello, this is a test! This part should be elided.`.trim(); QUnit.test("shows selected and elided text", async assert => { - // prettier-ignore - server.post("/admin/email/advanced-test", () => { // eslint-disable-line no-undef + pretender.post("/admin/email/advanced-test", () => { return [ 200, { "Content-Type": "application/json" }, { format: 1, text: "Hello, this is a test!", - elided: "---\n\nThis part should be elided.", + elided: "---\n\nThis part should be elided." } ]; }); diff --git a/test/javascripts/acceptance/admin-user-emails-test.js.es6 b/test/javascripts/acceptance/admin-user-emails-test.js.es6 index 6fa2965bfb..a9c0c2dcb3 100644 --- a/test/javascripts/acceptance/admin-user-emails-test.js.es6 +++ b/test/javascripts/acceptance/admin-user-emails-test.js.es6 @@ -2,19 +2,6 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("Admin - User Emails", { loggedIn: true }); -const responseWithSecondary = secondaryEmails => { - return [ - 200, - { "Content-Type": "application/json" }, - { - id: 1, - username: "eviltrout", - email: "eviltrout@example.com", - secondary_emails: secondaryEmails - } - ]; -}; - const assertNoSecondary = assert => { assert.equal( find(".display-row.email .value a").text(), @@ -31,49 +18,40 @@ const assertNoSecondary = assert => { ); }; -const assertMultipleSecondary = assert => { +const assertMultipleSecondary = (assert, firstEmail, secondEmail) => { assert.equal( find(".display-row.secondary-emails .value li:first-of-type a").text(), - "eviltrout1@example.com", + firstEmail, "it should display the first secondary email" ); assert.equal( find(".display-row.secondary-emails .value li:last-of-type a").text(), - "eviltrout2@example.com", + secondEmail, "it should display the second secondary email" ); }; QUnit.test("viewing self without secondary emails", async assert => { - // prettier-ignore - server.get("/admin/users/1.json", () => { // eslint-disable-line no-undef - return responseWithSecondary([]); - }); - await visit("/admin/users/1/eviltrout"); assertNoSecondary(assert); }); QUnit.test("viewing self with multiple secondary emails", async assert => { - // prettier-ignore - server.get("/admin/users/1.json", () => { // eslint-disable-line no-undef - return responseWithSecondary([ - "eviltrout1@example.com", - "eviltrout2@example.com", - ]); - }); - - await visit("/admin/users/1/eviltrout"); + await visit("/admin/users/3/markvanlan"); assert.equal( find(".display-row.email .value a").text(), - "eviltrout@example.com", + "markvanlan@example.com", "it should display the user's primary email" ); - assertMultipleSecondary(assert); + assertMultipleSecondary( + assert, + "markvanlan1@example.com", + "markvanlan2@example.com" + ); }); QUnit.test("viewing another user with no secondary email", async assert => { @@ -84,20 +62,12 @@ QUnit.test("viewing another user with no secondary email", async assert => { }); QUnit.test("viewing another account with secondary emails", async assert => { - // prettier-ignore - server.get("/u/regular/emails.json", () => { // eslint-disable-line no-undef - return [ - 200, - { "Content-Type": "application/json" }, - { - email: "eviltrout@example.com", - secondary_emails: ["eviltrout1@example.com", "eviltrout2@example.com"] - } - ]; - }); - - await visit("/admin/users/1234/regular"); + await visit("/admin/users/1235/regular1"); await click(`.display-row.secondary-emails button`); - assertMultipleSecondary(assert); + assertMultipleSecondary( + assert, + "regular2alt1@example.com", + "regular2alt2@example.com" + ); }); diff --git a/test/javascripts/acceptance/admin-user-index-test.js.es6 b/test/javascripts/acceptance/admin-user-index-test.js.es6 index 6322a0da55..6ea2fd87f0 100644 --- a/test/javascripts/acceptance/admin-user-index-test.js.es6 +++ b/test/javascripts/acceptance/admin-user-index-test.js.es6 @@ -1,10 +1,11 @@ import selectKit from "helpers/select-kit-helper"; import { acceptance } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; acceptance("Admin - User Index", { loggedIn: true, - pretend(server, helper) { - server.get("/groups/search.json", () => { + pretend(pretenderServer, helper) { + pretenderServer.get("/groups/search.json", () => { return helper.response([ { id: 42, @@ -35,8 +36,7 @@ acceptance("Admin - User Index", { }); QUnit.test("can edit username", async assert => { - /* global server */ - server.put("/users/sam/preferences/username", () => [ + pretender.put("/users/sam/preferences/username", () => [ 200, { "Content-Type": "application/json" }, { id: 2, username: "new-sam" } diff --git a/test/javascripts/acceptance/click-track-test.js.es6 b/test/javascripts/acceptance/click-track-test.js.es6 index fe7251fec1..d26e96d5a5 100644 --- a/test/javascripts/acceptance/click-track-test.js.es6 +++ b/test/javascripts/acceptance/click-track-test.js.es6 @@ -1,10 +1,10 @@ +import pretender from "helpers/create-pretender"; import { acceptance } from "helpers/qunit-helpers"; acceptance("Click Track", {}); QUnit.test("Do not track mentions", async assert => { - /* global server */ - server.post("/clicks/track", () => assert.ok(false)); + pretender.post("/clicks/track", () => assert.ok(false)); await visit("/t/internationalization-localization/280"); assert.ok(invisible(".user-card"), "card should not appear"); diff --git a/test/javascripts/acceptance/composer-actions-test.js.es6 b/test/javascripts/acceptance/composer-actions-test.js.es6 index c95497eb08..bd7458ccb0 100644 --- a/test/javascripts/acceptance/composer-actions-test.js.es6 +++ b/test/javascripts/acceptance/composer-actions-test.js.es6 @@ -349,19 +349,21 @@ acceptance("Composer Actions With New Topic Draft", { }, beforeEach() { _clearSnapshots(); - }, - pretend(server, helper) { - server.get("draft.json", () => { - return helper.response({ - draft: - '{"reply":"dum de dum da ba.","action":"createTopic","title":"dum da ba dum dum","categoryId":null,"archetypeId":"regular","metaData":null,"composerTime":540879,"typingTime":3400}', - draft_sequence: 0 - }); - }); } }); +const stubDraftResponse = () => { + sandbox.stub(Draft, "get").returns( + Promise.resolve({ + draft: + '{"reply":"dum de dum da ba.","action":"createTopic","title":"dum da ba dum dum","categoryId":null,"archetypeId":"regular","metaData":null,"composerTime":540879,"typingTime":3400}', + draft_sequence: 0 + }) + ); +}; + QUnit.test("shared draft", async assert => { + stubDraftResponse(); try { toggleCheckDraftPopup(true); @@ -399,6 +401,7 @@ QUnit.test("shared draft", async assert => { } finally { toggleCheckDraftPopup(false); } + sandbox.restore(); }); QUnit.test("reply_as_new_topic with new_topic draft", async assert => { @@ -406,10 +409,12 @@ QUnit.test("reply_as_new_topic with new_topic draft", async assert => { await click(".create.reply"); const composerActions = selectKit(".composer-actions"); await composerActions.expand(); + stubDraftResponse(); await composerActions.selectRowByValue("reply_as_new_topic"); assert.equal( find(".bootbox .modal-body").text(), I18n.t("composer.composer_actions.reply_as_new_topic.confirm") ); await click(".modal-footer .btn.btn-default"); + sandbox.restore(); }); diff --git a/test/javascripts/acceptance/composer-attachment-test.js.es6 b/test/javascripts/acceptance/composer-attachment-test.js.es6 index edf6bc4374..0333936f4b 100644 --- a/test/javascripts/acceptance/composer-attachment-test.js.es6 +++ b/test/javascripts/acceptance/composer-attachment-test.js.es6 @@ -1,21 +1,18 @@ import { acceptance } from "helpers/qunit-helpers"; -acceptance("Composer Attachment", { - loggedIn: true, - pretend(server, helper) { - server.post("/uploads/lookup-urls", () => { - return helper.response([ - { - short_url: "upload://asdsad.png", - url: "/uploads/default/3X/1/asjdiasjdiasida.png", - short_path: "/uploads/short-url/asdsad.png" - } - ]); - }); - } -}); +function setupPretender(server, helper) { + server.post("/uploads/lookup-urls", () => { + return helper.response([ + { + short_url: "upload://asdsad.png", + url: "/secure-media-uploads/default/3X/1/asjdiasjdiasida.png", + short_path: "/uploads/short-url/asdsad.png" + } + ]); + }); +} -QUnit.test("attachments are cooked properly", async assert => { +async function writeInComposer(assert) { await visit("/t/internationalization-localization/280"); await click("#topic-footer-buttons .btn.create"); @@ -29,7 +26,17 @@ QUnit.test("attachments are cooked properly", async assert => { ); await fillIn(".d-editor-input", "[test|attachment](upload://asdsad.png)"); +} +acceptance("Composer Attachment", { + loggedIn: true, + pretend(server, helper) { + setupPretender(server, helper); + } +}); + +QUnit.test("attachments are cooked properly", async assert => { + await writeInComposer(assert); assert.equal( find(".d-editor-preview:visible") .html() @@ -37,3 +44,26 @@ QUnit.test("attachments are cooked properly", async assert => { '

test

' ); }); + +acceptance("Composer Attachment - Secure Media Enabled", { + loggedIn: true, + settings: { + secure_media: true + }, + pretend(server, helper) { + setupPretender(server, helper); + } +}); + +QUnit.test( + "attachments are cooked properly when secure media is enabled", + async assert => { + await writeInComposer(assert); + assert.equal( + find(".d-editor-preview:visible") + .html() + .trim(), + '

test

' + ); + } +); diff --git a/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 index ee4d22e4c4..890a7dff5c 100644 --- a/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 +++ b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 @@ -1,18 +1,15 @@ import { acceptance } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; acceptance("Composer - Edit conflict", { loggedIn: true }); QUnit.test("Edit a post that causes an edit conflict", async assert => { - // prettier-ignore - server.put("/posts/398", () => [ // eslint-disable-line no-undef - 409, { "Content-Type": "application/json" }, { errors: ["edit conflict"] } - ]); - await visit("/t/internationalization-localization/280"); await click(".topic-post:eq(0) button.show-more-actions"); await click(".topic-post:eq(0) button.edit"); + await fillIn(".d-editor-input", "this will 409"); await click("#reply-control button.create"); assert.equal( find("#reply-control button.create") @@ -28,14 +25,33 @@ QUnit.test("Edit a post that causes an edit conflict", async assert => { await click(".modal .btn-primary"); }); +function handleDraftPretender(assert) { + pretender.post("/draft.json", request => { + if ( + request.requestBody.indexOf("%22reply%22%3A%22%22") === -1 && + request.requestBody.indexOf("Any+plans+to+support+localization") !== -1 + ) { + assert.notEqual(request.requestBody.indexOf("originalText"), -1); + } + if ( + request.requestBody.indexOf( + "draft_key=topic_280&sequence=4&data=%7B%22reply%22%3A%22hello+world+hello+world+hello+world+hello+world+hello+world%22%2C%22action%22%3A%22reply%22%2C%22categoryId%22%3A2%2C%22archetypeId%22%3A%22regular%22%2C%22metaData" + ) !== -1 + ) { + assert.equal( + request.requestBody.indexOf("originalText"), + -1, + request.requestBody + ); + } + return [200, { "Content-Type": "application/json" }, { success: true }]; + }); +} + QUnit.test( "Should not send originalText when posting a new reply", async assert => { - // prettier-ignore - server.post("/draft.json", request => { // eslint-disable-line no-undef - assert.equal(request.requestBody.indexOf("originalText"), -1, request.requestBody); - return [ 200, { "Content-Type": "application/json" }, { success: true } ]; - }); + handleDraftPretender(assert); await visit("/t/internationalization-localization/280"); await click(".topic-post:eq(0) button.reply"); @@ -47,13 +63,7 @@ QUnit.test( ); QUnit.test("Should send originalText when editing a reply", async assert => { - // prettier-ignore - server.post("/draft.json", request => { // eslint-disable-line no-undef - if (request.requestBody.indexOf("%22reply%22%3A%22%22") === -1) { - assert.notEqual(request.requestBody.indexOf("originalText"), -1); - } - return [ 200, { "Content-Type": "application/json" }, { success: true } ]; - }); + handleDraftPretender(assert); await visit("/t/internationalization-localization/280"); await click(".topic-post:eq(0) button.show-more-actions"); diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6 index 47f2fc99a3..b9da96ee5e 100644 --- a/test/javascripts/acceptance/composer-test.js.es6 +++ b/test/javascripts/acceptance/composer-test.js.es6 @@ -2,17 +2,13 @@ import { run } from "@ember/runloop"; import selectKit from "helpers/select-kit-helper"; import { acceptance } from "helpers/qunit-helpers"; import { toggleCheckDraftPopup } from "discourse/controllers/composer"; +import Draft from "discourse/models/draft"; +import { Promise } from "rsvp"; acceptance("Composer", { loggedIn: true, - pretend(server, helper) { - server.get("/draft.json", () => { - return helper.response({ - draft: null, - draft_sequence: 42 - }); - }); - server.post("/uploads/lookup-urls", () => { + pretend(pretenderServer, helper) { + pretenderServer.post("/uploads/lookup-urls", () => { return helper.response([]); }); }, @@ -617,14 +613,6 @@ QUnit.test("Checks for existing draft", async assert => { try { toggleCheckDraftPopup(true); - // prettier-ignore - server.get("/draft.json", () => { // eslint-disable-line no-undef - return [ 200, { "Content-Type": "application/json" }, { - draft: "{\"reply\":\"This is a draft of the first post\",\"action\":\"reply\",\"categoryId\":1,\"archetypeId\":\"regular\",\"metaData\":null,\"composerTime\":2863,\"typingTime\":200}", - draft_sequence: 42 - } ]; - }); - await visit("/t/internationalization-localization/280"); await click(".topic-post:eq(0) button.show-more-actions"); @@ -646,18 +634,17 @@ QUnit.test("Can switch states without abandon popup", async assert => { const longText = "a".repeat(256); + sandbox.stub(Draft, "get").returns( + Promise.resolve({ + draft: null, + draft_sequence: 0 + }) + ); + await click(".btn-primary.create.btn"); await fillIn(".d-editor-input", longText); - // prettier-ignore - server.get("/draft.json", () => { // eslint-disable-line no-undef - return [ 200, { "Content-Type": "application/json" }, { - draft: "{\"reply\":\"This is a draft of the first post\",\"action\":\"reply\",\"categoryId\":1,\"archetypeId\":\"regular\",\"metaData\":null,\"composerTime\":2863,\"typingTime\":200}", - draft_sequence: 42 - } ]; - }); - await click("article#post_3 button.reply"); const composerActions = selectKit(".composer-actions"); @@ -686,19 +673,20 @@ QUnit.test("Can switch states without abandon popup", async assert => { } finally { toggleCheckDraftPopup(false); } + sandbox.restore(); }); QUnit.test("Loading draft also replaces the recipients", async assert => { try { toggleCheckDraftPopup(true); - // prettier-ignore - server.get("/draft.json", () => { // eslint-disable-line no-undef - return [ 200, { "Content-Type": "application/json" }, { - "draft":"{\"reply\":\"hello\",\"action\":\"privateMessage\",\"title\":\"hello\",\"categoryId\":null,\"archetypeId\":\"private_message\",\"metaData\":null,\"usernames\":\"codinghorror\",\"composerTime\":9159,\"typingTime\":2500}", - "draft_sequence":0 - } ]; - }); + sandbox.stub(Draft, "get").returns( + Promise.resolve({ + draft: + '{"reply":"hello","action":"privateMessage","title":"hello","categoryId":null,"archetypeId":"private_message","metaData":null,"usernames":"codinghorror","composerTime":9159,"typingTime":2500}', + draft_sequence: 0 + }) + ); await visit("/u/charlie"); await click("button.compose-pm"); diff --git a/test/javascripts/acceptance/composer-uncategorized-test.js.es6 b/test/javascripts/acceptance/composer-uncategorized-test.js.es6 index 7ca84df311..46dac33167 100644 --- a/test/javascripts/acceptance/composer-uncategorized-test.js.es6 +++ b/test/javascripts/acceptance/composer-uncategorized-test.js.es6 @@ -3,14 +3,6 @@ import { acceptance, updateCurrentUser } from "helpers/qunit-helpers"; acceptance("Composer and uncategorized is not allowed", { loggedIn: true, - pretend(server, helper) { - server.get("/draft.json", () => { - return helper.response({ - draft: null, - draft_sequence: 42 - }); - }); - }, settings: { enable_whispers: true, allow_uncategorized_topics: false diff --git a/test/javascripts/acceptance/group-test.js.es6 b/test/javascripts/acceptance/group-test.js.es6 index af5bc39935..9ba5a9f2f0 100644 --- a/test/javascripts/acceptance/group-test.js.es6 +++ b/test/javascripts/acceptance/group-test.js.es6 @@ -1,12 +1,13 @@ import selectKit from "helpers/select-kit-helper"; import { acceptance } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; let groupArgs = { settings: { enable_group_directory: true }, - pretend(server, helper) { - server.post("/groups/Macdonald/request_membership", () => { + pretend(pretenderServer, helper) { + pretenderServer.post("/groups/Macdonald/request_membership", () => { return helper.response({ relative_url: "/t/internationalization-localization/280" }); @@ -127,10 +128,12 @@ QUnit.test("User Viewing Group", async assert => { QUnit.test( "Admin viewing group messages when there are no messages", async assert => { - // prettier-ignore - server.get("/topics/private-messages-group/eviltrout/discourse.json", () => { // eslint-disable-line no-undef - return response({ topic_list: { topics: [] } }); - }); + pretender.get( + "/topics/private-messages-group/eviltrout/discourse.json", + () => { + return response({ topic_list: { topics: [] } }); + } + ); await visit("/g/discourse"); await click(".nav-pills li a[title='Messages']"); @@ -146,87 +149,89 @@ QUnit.test( ); QUnit.test("Admin viewing group messages", async assert => { - // prettier-ignore - server.get("/topics/private-messages-group/eviltrout/discourse.json", () => { // eslint-disable-line no-undef - return response({ - users: [ - { - id: 2, - username: "bruce1", - avatar_template: - "/user_avatar/meta.discourse.org/bruce1/{size}/5245.png" - }, - { - id: 3, - username: "CodingHorror", - avatar_template: - "/user_avatar/meta.discourse.org/codinghorror/{size}/5245.png" - } - ], - primary_groups: [], - topic_list: { - can_create_topic: true, - draft: null, - draft_key: "new_topic", - draft_sequence: 0, - per_page: 30, - topics: [ + pretender.get( + "/topics/private-messages-group/eviltrout/discourse.json", + () => { + return response({ + users: [ { - id: 12199, - title: "This is a private message 1", - fancy_title: "This is a private message 1", - slug: "this-is-a-private-message-1", - posts_count: 0, - reply_count: 0, - highest_post_number: 0, - image_url: null, - created_at: "2018-03-16T03:38:45.583Z", - last_posted_at: null, - bumped: true, - bumped_at: "2018-03-16T03:38:45.583Z", - unseen: false, - pinned: false, - unpinned: null, - visible: true, - closed: false, - archived: false, - bookmarked: null, - liked: null, - views: 0, - like_count: 0, - has_summary: false, - archetype: "private_message", - last_poster_username: "bruce1", - category_id: null, - pinned_globally: false, - featured_link: null, - posters: [ - { - extras: "latest single", - description: "Original Poster, Most Recent Poster", - user_id: 2, - primary_group_id: null - } - ], - participants: [ - { - extras: "latest", - description: null, - user_id: 2, - primary_group_id: null - }, - { - extras: null, - description: null, - user_id: 3, - primary_group_id: null - } - ] + id: 2, + username: "bruce1", + avatar_template: + "/user_avatar/meta.discourse.org/bruce1/{size}/5245.png" + }, + { + id: 3, + username: "CodingHorror", + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5245.png" } - ] - } - }); - }); + ], + primary_groups: [], + topic_list: { + can_create_topic: true, + draft: null, + draft_key: "new_topic", + draft_sequence: 0, + per_page: 30, + topics: [ + { + id: 12199, + title: "This is a private message 1", + fancy_title: "This is a private message 1", + slug: "this-is-a-private-message-1", + posts_count: 0, + reply_count: 0, + highest_post_number: 0, + image_url: null, + created_at: "2018-03-16T03:38:45.583Z", + last_posted_at: null, + bumped: true, + bumped_at: "2018-03-16T03:38:45.583Z", + unseen: false, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 0, + like_count: 0, + has_summary: false, + archetype: "private_message", + last_poster_username: "bruce1", + category_id: null, + pinned_globally: false, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: 2, + primary_group_id: null + } + ], + participants: [ + { + extras: "latest", + description: null, + user_id: 2, + primary_group_id: null + }, + { + extras: null, + description: null, + user_id: 3, + primary_group_id: null + } + ] + } + ] + } + }); + } + ); await visit("/g/discourse"); await click(".nav-pills li a[title='Messages']"); diff --git a/test/javascripts/acceptance/keyboard-shortcuts-test.js.es6 b/test/javascripts/acceptance/keyboard-shortcuts-test.js.es6 index 89f8fd5a4c..b238e36118 100644 --- a/test/javascripts/acceptance/keyboard-shortcuts-test.js.es6 +++ b/test/javascripts/acceptance/keyboard-shortcuts-test.js.es6 @@ -1,16 +1,16 @@ import { acceptance } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; acceptance("Keyboard Shortcuts", { loggedIn: true }); test("go to first suggested topic", async assert => { - /* global server */ - server.get("/t/27331/4.json", () => [ + pretender.get("/t/27331/4.json", () => [ 200, { "Content-Type": "application/json" }, {} ]); - server.get("/t/27331.json", () => [ + pretender.get("/t/27331.json", () => [ 200, { "Content-Type": "application/json" }, {} @@ -20,7 +20,7 @@ test("go to first suggested topic", async assert => { * No suggested topics exist. */ - server.get("/t/9/last.json", () => [ + pretender.get("/t/9/last.json", () => [ 200, { "Content-Type": "application/json" }, {} @@ -44,7 +44,7 @@ test("go to first suggested topic", async assert => { * Suggested topic is returned by server. */ - server.get("/t/28830/last.json", () => [ + pretender.get("/t/28830/last.json", () => [ 200, { "Content-Type": "application/json" }, { diff --git a/test/javascripts/acceptance/login-with-email-and-hide-email-address-taken-test.js.es6 b/test/javascripts/acceptance/login-with-email-and-hide-email-address-taken-test.js.es6 index a205a12d05..1c12c1f055 100644 --- a/test/javascripts/acceptance/login-with-email-and-hide-email-address-taken-test.js.es6 +++ b/test/javascripts/acceptance/login-with-email-and-hide-email-address-taken-test.js.es6 @@ -1,4 +1,5 @@ import { acceptance } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; acceptance("Login with email - hide email address taken", { settings: { @@ -10,8 +11,7 @@ acceptance("Login with email - hide email address taken", { return [200, { "Content-Type": "application/json" }, object]; }; - // prettier-ignore - server.post("/u/email-login", () => { // eslint-disable-line no-undef + pretender.post("/u/email-login", () => { return response({ success: "OK" }); }); } diff --git a/test/javascripts/acceptance/tags-test.js.es6 b/test/javascripts/acceptance/tags-test.js.es6 index 276c814a91..e3a6449aa4 100644 --- a/test/javascripts/acceptance/tags-test.js.es6 +++ b/test/javascripts/acceptance/tags-test.js.es6 @@ -1,4 +1,6 @@ import { updateCurrentUser, acceptance } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; + acceptance("Tags", { loggedIn: true }); QUnit.test("list the tags", async assert => { @@ -19,48 +21,6 @@ acceptance("Tags listed by group", { }); QUnit.test("list the tags in groups", async assert => { - // prettier-ignore - server.get("/tags", () => { // eslint-disable-line no-undef - return [ - 200, - { "Content-Type": "application/json" }, - { - tags: [ - { id: "planned", text: "planned", count: 7, pm_count: 0 }, - { id: "private", text: "private", count: 0, pm_count: 7 } - ], - extras: { - tag_groups: [ - { - id: 2, - name: "Ford Cars", - tags: [ - { id: "Escort", text: "Escort", count: 1, pm_count: 0 }, - { id: "focus", text: "focus", count: 3, pm_count: 0 } - ] - }, - { - id: 1, - name: "Honda Cars", - tags: [ - { id: "civic", text: "civic", count: 4, pm_count: 0 }, - { id: "accord", text: "accord", count: 2, pm_count: 0 } - ] - }, - { - id: 1, - name: "Makes", - tags: [ - { id: "ford", text: "ford", count: 5, pm_count: 0 }, - { id: "honda", text: "honda", count: 6, pm_count: 0 } - ] - } - ] - } - } - ]; - }); - await visit("/tags"); assert.equal( $(".tag-list").length, @@ -102,14 +62,13 @@ QUnit.test("list the tags in groups", async assert => { }); test("new topic button is not available for staff-only tags", async assert => { - /* global server */ - server.get("/tag/regular-tag/notifications", () => [ + pretender.get("/tag/regular-tag/notifications", () => [ 200, { "Content-Type": "application/json" }, { tag_notification: { id: "regular-tag", notification_level: 1 } } ]); - server.get("/tag/regular-tag/l/latest.json", () => [ + pretender.get("/tag/regular-tag/l/latest.json", () => [ 200, { "Content-Type": "application/json" }, { @@ -133,13 +92,13 @@ test("new topic button is not available for staff-only tags", async assert => { } ]); - server.get("/tag/staff-only-tag/notifications", () => [ + pretender.get("/tag/staff-only-tag/notifications", () => [ 200, { "Content-Type": "application/json" }, { tag_notification: { id: "staff-only-tag", notification_level: 1 } } ]); - server.get("/tag/staff-only-tag/l/latest.json", () => [ + pretender.get("/tag/staff-only-tag/l/latest.json", () => [ 200, { "Content-Type": "application/json" }, { @@ -286,7 +245,7 @@ test("tag info can show synonyms", async assert => { }); test("admin can manage tags", async assert => { - server.delete("/tag/planters/synonyms/containers", () => [ + pretender.delete("/tag/planters/synonyms/containers", () => [ 200, { "Content-Type": "application/json" }, { success: true } diff --git a/test/javascripts/acceptance/user-test.js.es6 b/test/javascripts/acceptance/user-test.js.es6 index 2f2fc61d08..24f630b942 100644 --- a/test/javascripts/acceptance/user-test.js.es6 +++ b/test/javascripts/acceptance/user-test.js.es6 @@ -1,10 +1,12 @@ import { acceptance } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; +import Draft from "discourse/models/draft"; +import { Promise } from "rsvp"; acceptance("User", { loggedIn: true }); QUnit.test("Invalid usernames", async assert => { - // prettier-ignore - server.get("/u/eviltrout%2F..%2F..%2F.json", () => { // eslint-disable-line no-undef + pretender.get("/u/eviltrout%2F..%2F..%2F.json", () => { return [400, { "Content-Type": "application/json" }, {}]; }); @@ -67,13 +69,12 @@ QUnit.test("Viewing Summary", async assert => { }); QUnit.test("Viewing Drafts", async assert => { - // prettier-ignore - server.get("/draft.json", () => { // eslint-disable-line no-undef - return [ 200, { "Content-Type": "application/json" }, { - draft: "{\"reply\":\"This is a draft of the first post\",\"action\":\"reply\",\"categoryId\":1,\"archetypeId\":\"regular\",\"metaData\":null,\"composerTime\":2863,\"typingTime\":200}", - draft_sequence: 42 - } ]; - }); + sandbox.stub(Draft, "get").returns( + Promise.resolve({ + draft: null, + draft_sequence: 0 + }) + ); await visit("/u/eviltrout/activity/drafts"); assert.ok(exists(".user-stream"), "has drafts stream"); @@ -87,4 +88,5 @@ QUnit.test("Viewing Drafts", async assert => { exists(".d-editor-input"), "composer is visible after resuming a draft" ); + sandbox.restore(); }); diff --git a/test/javascripts/components/admin-report-test.js.es6 b/test/javascripts/components/admin-report-test.js.es6 index e713ff693d..dfbab4c64f 100644 --- a/test/javascripts/components/admin-report-test.js.es6 +++ b/test/javascripts/components/admin-report-test.js.es6 @@ -1,4 +1,5 @@ import componentTest from "helpers/component-test"; +import pretender from "helpers/create-pretender"; moduleForComponent("admin-report", { integration: true @@ -133,13 +134,18 @@ componentTest("exception", { componentTest("rate limited", { beforeEach() { - const response = object => { - return [429, { "Content-Type": "application/json" }, object]; - }; - - // prettier-ignore - server.get("/admin/reports/bulk", () => { //eslint-disable-line - return response({"errors":["You’ve performed this action too many times. Please wait 10 seconds before trying again."],"error_type":"rate_limit","extras":{"wait_seconds":10}}); + pretender.get("/admin/reports/bulk", () => { + return [ + 429, + { "Content-Type": "application/json" }, + { + errors: [ + "You’ve performed this action too many times. Please wait 10 seconds before trying again." + ], + error_type: "rate_limit", + extras: { wait_seconds: 10 } + } + ]; }); }, diff --git a/test/javascripts/components/badge-title-test.js.es6 b/test/javascripts/components/badge-title-test.js.es6 index 8f1630f844..7be2678269 100644 --- a/test/javascripts/components/badge-title-test.js.es6 +++ b/test/javascripts/components/badge-title-test.js.es6 @@ -1,6 +1,7 @@ import selectKit from "helpers/select-kit-helper"; import componentTest from "helpers/component-test"; import EmberObject from "@ember/object"; +import pretender from "helpers/create-pretender"; moduleForComponent("badge-title", { integration: true }); @@ -23,8 +24,7 @@ componentTest("badge title", { }, async test(assert) { - /* global server */ - server.put("/u/eviltrout/preferences/badge_title", () => [ + pretender.put("/u/eviltrout/preferences/badge_title", () => [ 200, { "Content-Type": "application/json" }, {} diff --git a/test/javascripts/components/select-kit/category-drop-test.js.es6 b/test/javascripts/components/select-kit/category-drop-test.js.es6 index e44b8d24a7..2a3b7b6e56 100644 --- a/test/javascripts/components/select-kit/category-drop-test.js.es6 +++ b/test/javascripts/components/select-kit/category-drop-test.js.es6 @@ -1,3 +1,4 @@ +import DiscourseURL from "discourse/lib/url"; import Category from "discourse/models/category"; import componentTest from "helpers/component-test"; import { testSelectKitModule } from "./select-kit-test-helper"; @@ -335,3 +336,22 @@ componentTest( } } ); + +componentTest("category url", { + template: template(), + + beforeEach() { + initCategoriesWithParentCategory(this); + sandbox.stub(DiscourseURL, "routeTo"); + }, + + async test(assert) { + await this.subject.expand(); + await this.subject.selectRowByValue(26); + + assert.ok( + DiscourseURL.routeTo.calledWith("/c/feature/spec/26"), + "it builds a correct URL" + ); + } +}); diff --git a/test/javascripts/components/select-kit/single-select-test.js.es6 b/test/javascripts/components/select-kit/single-select-test.js.es6 index 42f058a586..88dbbacf9e 100644 --- a/test/javascripts/components/select-kit/single-select-test.js.es6 +++ b/test/javascripts/components/select-kit/single-select-test.js.es6 @@ -238,3 +238,22 @@ componentTest("selected value can be 0", { assert.equal(this.subject.header().value(), 0); } }); + +componentTest("prevents propagating click event on header", { + template: + "{{#d-button icon='times' action=onClick}}{{single-select value=value content=content}}{{/d-button}}", + + beforeEach() { + this.setProperties({ + onClick: () => this.set("value", "foo"), + content: DEFAULT_CONTENT, + value: DEFAULT_VALUE + }); + }, + + async test(assert) { + assert.equal(this.value, DEFAULT_VALUE); + await this.subject.expand(); + assert.equal(this.value, DEFAULT_VALUE); + } +}); diff --git a/test/javascripts/components/select-kit/tag-drop-test.js.es6 b/test/javascripts/components/select-kit/tag-drop-test.js.es6 index 5d758fa90b..2c6d156052 100644 --- a/test/javascripts/components/select-kit/tag-drop-test.js.es6 +++ b/test/javascripts/components/select-kit/tag-drop-test.js.es6 @@ -2,6 +2,7 @@ import componentTest from "helpers/component-test"; import { testSelectKitModule } from "./select-kit-test-helper"; import Site from "discourse/models/site"; import { set } from "@ember/object"; +import pretender from "helpers/create-pretender"; testSelectKitModule("tag-drop", { beforeEach() { @@ -12,19 +13,14 @@ testSelectKitModule("tag-drop", { return [200, { "Content-Type": "application/json" }, object]; }; - // prettier-ignore - server.get("/tags/filter/search", (params) => { //eslint-disable-line + pretender.get("/tags/filter/search", params => { if (params.queryParams.q === "rég") { return response({ - "results": [ - { "id": "régis", "text": "régis", "count": 2, "pm_count": 0 } - ] + results: [{ id: "régis", text: "régis", count: 2, pm_count: 0 }] }); - }else if (params.queryParams.q === "dav") { + } else if (params.queryParams.q === "dav") { return response({ - "results": [ - { "id": "David", "text": "David", "count": 2, "pm_count": 0 } - ] + results: [{ id: "David", text: "David", count: 2, pm_count: 0 }] }); } }); diff --git a/test/javascripts/components/select-kit/user-chooser-test.js.es6 b/test/javascripts/components/select-kit/user-chooser-test.js.es6 index 068d8c6a1f..c59d7cb571 100644 --- a/test/javascripts/components/select-kit/user-chooser-test.js.es6 +++ b/test/javascripts/components/select-kit/user-chooser-test.js.es6 @@ -1,5 +1,6 @@ import componentTest from "helpers/component-test"; import { testSelectKitModule } from "./select-kit-test-helper"; +import pretender from "helpers/create-pretender"; testSelectKitModule("user-chooser"); @@ -42,9 +43,8 @@ componentTest("can add a username", { return [200, { "Content-Type": "application/json" }, object]; }; - // prettier-ignore - server.get("/u/search/users", () => { //eslint-disable-line - return response({users:[{username: "maja", name: "Maja"}]}); + pretender.get("/u/search/users", () => { + return response({ users: [{ username: "maja", name: "Maja" }] }); }); }, diff --git a/test/javascripts/controllers/history-test.js.es6 b/test/javascripts/controllers/history-test.js.es6 index 0aeccda048..6d8ccae7cf 100644 --- a/test/javascripts/controllers/history-test.js.es6 +++ b/test/javascripts/controllers/history-test.js.es6 @@ -1,6 +1,6 @@ moduleFor("controller:history"); -QUnit.test("displayEdit", function(assert) { +QUnit.test("displayEdit", async function(assert) { const HistoryController = this.subject(); HistoryController.setProperties({ @@ -82,8 +82,8 @@ QUnit.test("displayEdit", function(assert) { } }); - HistoryController.bodyDiffChanged().then(() => { - const output = HistoryController.get("bodyDiff"); - assert.equal(output, expectedOutput, "it keeps safe HTML"); - }); + await HistoryController.bodyDiffChanged(); + + const output = HistoryController.get("bodyDiff"); + assert.equal(output, expectedOutput, "it keeps safe HTML"); }); diff --git a/test/javascripts/controllers/topic-test.js.es6 b/test/javascripts/controllers/topic-test.js.es6 index da92e3df31..4277564fad 100644 --- a/test/javascripts/controllers/topic-test.js.es6 +++ b/test/javascripts/controllers/topic-test.js.es6 @@ -5,6 +5,7 @@ import PostStream from "discourse/models/post-stream"; import { Placeholder } from "discourse/lib/posts-with-placeholders"; import User from "discourse/models/user"; import { Promise } from "rsvp"; +import pretender from "helpers/create-pretender"; moduleFor("controller:topic", "controller:topic", { needs: [ @@ -522,8 +523,7 @@ QUnit.test("topVisibleChanged", function(assert) { QUnit.test( "deletePost - no modal is shown if post does not have replies", function(assert) { - /* global server */ - server.get("/posts/2/reply-ids.json", () => { + pretender.get("/posts/2/reply-ids.json", () => { return [200, { "Content-Type": "application/json" }, []]; }); diff --git a/test/javascripts/fixtures/draft.js.es6 b/test/javascripts/fixtures/draft.js.es6 index bdbdfed17c..a1d5ce6b9c 100644 --- a/test/javascripts/fixtures/draft.js.es6 +++ b/test/javascripts/fixtures/draft.js.es6 @@ -2,5 +2,20 @@ export default { "/draft.json": { draft: null, draft_sequence: 0 + }, + "/draft.json?draft_key=topic_280": { + draft: + '{"reply":"This is a draft of the first post","action":"reply","categoryId":1,"archetypeId":"regular","metaData":null,"composerTime":2863,"typingTime":200}', + draft_sequence: 42 + }, + "/draft.json?draft_key=topic_281": { + draft: + '{"reply":"dum de dum da ba.","action":"createTopic","title":"dum da ba dum dum","categoryId":null,"archetypeId":"regular","metaData":null,"composerTime":540879,"typingTime":3400}', + draft_sequence: 0 + }, + "/draft.json?draft_key=topic_9": { + draft: + '{"reply":"This is a draft of the first post","action":"reply","categoryId":1,"archetypeId":"regular","metaData":null,"composerTime":2863,"typingTime":200}', + draft_sequence: 42 } }; diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index 5efd61e9a8..76cca6fa51 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -33,724 +33,794 @@ export function success() { const loggedIn = () => !!User.current(); const helpers = { response, success, parsePostData }; + export let fixturesByUrl; -export default function() { - const server = new Pretender(function() { - // Autoload any `*-pretender` files - Object.keys(requirejs.entries).forEach(e => { - let m = e.match(/^.*helpers\/([a-z-]+)\-pretender$/); - if (m && m[1] !== "create") { - let result = requirejs(e).default.call(this, helpers); - if (m[1] === "fixture") { - fixturesByUrl = result; - } +export default new Pretender(); + +export function applyDefaultHandlers(pretender) { + // Autoload any `*-pretender` files + Object.keys(requirejs.entries).forEach(e => { + let m = e.match(/^.*helpers\/([a-z-]+)\-pretender$/); + if (m && m[1] !== "create") { + let result = requirejs(e).default.call(pretender, helpers); + if (m[1] === "fixture") { + fixturesByUrl = result; } - }); + } + }); - this.get("/admin/plugins", () => response({ plugins: [] })); + pretender.get("/admin/plugins", () => response({ plugins: [] })); - this.get("/composer_messages", () => response({ composer_messages: [] })); + pretender.get("/composer_messages", () => + response({ composer_messages: [] }) + ); - this.get("/latest.json", () => { - const json = fixturesByUrl["/latest.json"]; + pretender.get("/latest.json", () => { + const json = fixturesByUrl["/latest.json"]; - if (loggedIn()) { - // Stuff to let us post - json.topic_list.can_create_topic = true; - json.topic_list.draft_key = "new_topic"; - json.topic_list.draft_sequence = 1; - } - return response(json); - }); + if (loggedIn()) { + // Stuff to let us post + json.topic_list.can_create_topic = true; + json.topic_list.draft_key = "new_topic"; + json.topic_list.draft_sequence = 1; + } + return response(json); + }); - this.get("/c/bug/1/l/latest.json", () => { - const json = fixturesByUrl["/c/bug/1/l/latest.json"]; + pretender.get("/c/bug/1/l/latest.json", () => { + const json = fixturesByUrl["/c/bug/1/l/latest.json"]; - if (loggedIn()) { - // Stuff to let us post - json.topic_list.can_create_topic = true; - json.topic_list.draft_key = "new_topic"; - json.topic_list.draft_sequence = 1; - } - return response(json); - }); + if (loggedIn()) { + // Stuff to let us post + json.topic_list.can_create_topic = true; + json.topic_list.draft_key = "new_topic"; + json.topic_list.draft_sequence = 1; + } + return response(json); + }); - this.get("/tags", () => { - return response({ + pretender.get("/tags", () => { + return [ + 200, + { "Content-Type": "application/json" }, + { tags: [ - { - id: "eviltrout", - count: 1 - } - ] - }); - }); - - this.get("/tags/filter/search", () => { - return response({ results: [{ text: "monkey", count: 1 }] }); - }); - - this.get(`/u/:username/emails.json`, () => { - return response({ email: "eviltrout@example.com" }); - }); - - this.get("/u/eviltrout.json", () => { - const json = fixturesByUrl["/u/eviltrout.json"]; - json.user.can_edit = loggedIn(); - return response(json); - }); - - this.get("/u/eviltrout/summary.json", () => { - return response({ - user_summary: { - topic_ids: [1234], - replies: [{ topic_id: 1234 }], - links: [{ topic_id: 1234, url: "https://eviltrout.com" }], - most_replied_to_users: [{ id: 333 }], - most_liked_by_users: [{ id: 333 }], - most_liked_users: [{ id: 333 }], - badges: [{ badge_id: 444 }], - top_categories: [ + { id: "eviltrout", count: 1 }, + { id: "planned", text: "planned", count: 7, pm_count: 0 }, + { id: "private", text: "private", count: 0, pm_count: 7 } + ], + extras: { + tag_groups: [ + { + id: 2, + name: "Ford Cars", + tags: [ + { id: "Escort", text: "Escort", count: 1, pm_count: 0 }, + { id: "focus", text: "focus", count: 3, pm_count: 0 } + ] + }, { id: 1, - name: "bug", - color: "e9dd00", - text_color: "000000", - slug: "bug", - read_restricted: false, - parent_category_id: null, - topic_count: 1, - post_count: 1 + name: "Honda Cars", + tags: [ + { id: "civic", text: "civic", count: 4, pm_count: 0 }, + { id: "accord", text: "accord", count: 2, pm_count: 0 } + ] + }, + { + id: 1, + name: "Makes", + tags: [ + { id: "ford", text: "ford", count: 5, pm_count: 0 }, + { id: "honda", text: "honda", count: 6, pm_count: 0 } + ] } ] - }, - badges: [{ id: 444, count: 1 }], - topics: [{ id: 1234, title: "cool title", url: "/t/1234/cool-title" }] - }); - }); - - this.get("/u/eviltrout/invited_count.json", () => { - return response({ - counts: { pending: 1, redeemed: 0, total: 0 } - }); - }); - - this.get("/u/eviltrout/invited.json", () => { - return response({ invites: [{ id: 1 }] }); - }); - - this.get("/topics/private-messages/eviltrout.json", () => { - return response(fixturesByUrl["/topics/private-messages/eviltrout.json"]); - }); - - this.get("/topics/feature_stats.json", () => { - return response({ - pinned_in_category_count: 0, - pinned_globally_count: 0, - banner_count: 0 - }); - }); - - this.put("/t/34/convert-topic/public", () => { - return response({}); - }); - - this.put("/t/280/make-banner", () => { - return response({}); - }); - - this.put("/t/internationalization-localization/280/status", () => { - return response({ - success: "OK", - topic_status_update: null - }); - }); - - this.post("/clicks/track", success); - - this.get("/search", request => { - if (request.queryParams.q === "posts") { - return response({ - posts: [ - { - id: 1234 - } - ] - }); - } else if (request.queryParams.q === "evil") { - return response({ - posts: [ - { - id: 1234 - } - ], - tags: [ - { - id: 6, - name: "eviltrout" - } - ] - }); + } } + ]; + }); - return response({}); - }); + pretender.get("/tags/filter/search", () => { + return response({ results: [{ text: "monkey", count: 1 }] }); + }); - this.put("/u/eviltrout.json", () => response({ user: {} })); - - this.get("/t/280.json", () => response(fixturesByUrl["/t/280/1.json"])); - this.get("/t/34.json", () => response(fixturesByUrl["/t/34/1.json"])); - this.get("/t/280/:post_number.json", () => - response(fixturesByUrl["/t/280/1.json"]) - ); - this.get("/t/28830.json", () => response(fixturesByUrl["/t/28830/1.json"])); - this.get("/t/9.json", () => response(fixturesByUrl["/t/9/1.json"])); - this.get("/t/12.json", () => response(fixturesByUrl["/t/12/1.json"])); - this.put("/t/1234/re-pin", success); - - this.get("/t/id_for/:slug", () => { + pretender.get(`/u/:username/emails.json`, request => { + if (request.params.username === "regular2") { return response({ - id: 280, - slug: "internationalization-localization", - url: "/t/internationalization-localization/280" + email: "regular2@example.com", + secondary_emails: [ + "regular2alt1@example.com", + "regular2alt2@example.com" + ] }); - }); + } + return response({ email: "eviltrout@example.com" }); + }); - this.delete("/t/:id", success); - this.put("/t/:id/recover", success); - this.put("/t/:id/publish", success); + pretender.get("/u/eviltrout.json", () => { + const json = fixturesByUrl["/u/eviltrout.json"]; + json.user.can_edit = loggedIn(); + return response(json); + }); - this.get("/404-body", () => { - return [ - 200, - { "Content-Type": "text/html" }, - "
not found
" - ]; - }); - - this.delete("/draft.json", success); - this.post("/draft.json", success); - - this.get("/u/:username/staff-info.json", () => response({})); - - this.get("/post_action_users", () => { - return response({ - post_action_users: [ + pretender.get("/u/eviltrout/summary.json", () => { + return response({ + user_summary: { + topic_ids: [1234], + replies: [{ topic_id: 1234 }], + links: [{ topic_id: 1234, url: "https://eviltrout.com" }], + most_replied_to_users: [{ id: 333 }], + most_liked_by_users: [{ id: 333 }], + most_liked_users: [{ id: 333 }], + badges: [{ badge_id: 444 }], + top_categories: [ { id: 1, - username: "eviltrout", - avatar_template: "/user_avatar/default/eviltrout/{size}/1.png", - username_lower: "eviltrout" + name: "bug", + color: "e9dd00", + text_color: "000000", + slug: "bug", + read_restricted: false, + parent_category_id: null, + topic_count: 1, + post_count: 1 } ] - }); - }); - - this.get("/post_replies", () => { - return response({ post_replies: [{ id: 1234, cooked: "wat" }] }); - }); - - this.get("/post_reply_histories", () => { - return response({ post_reply_histories: [{ id: 1234, cooked: "wat" }] }); - }); - - this.get("/category_hashtags/check", () => { - return response({ valid: [{ slug: "bug", url: "/c/bugs" }] }); - }); - - this.get("/categories_and_latest", () => - response(fixturesByUrl["/categories_and_latest.json"]) - ); - - this.put("/categories/:category_id", request => { - const category = parsePostData(request.requestBody); - category.id = parseInt(request.params.category_id, 10); - - if (category.email_in === "duplicate@example.com") { - return response(422, { errors: ["duplicate email"] }); - } - - return response({ category }); - }); - - this.get("/draft.json", request => { - if (request.queryParams.draft_key === "new_topic") { - return response(fixturesByUrl["/draft.json"]); - } - - return response({}); - }); - - this.get("/drafts.json", () => response(fixturesByUrl["/drafts.json"])); - - this.put("/queued_posts/:queued_post_id", function(request) { - return response({ queued_post: { id: request.params.queued_post_id } }); - }); - - this.get("/queued_posts", function() { - return response({ - queued_posts: [ - { id: 1, raw: "queued post text", can_delete_user: true } - ] - }); - }); - - this.post("/session", function(request) { - const data = parsePostData(request.requestBody); - - if (data.password === "correct") { - return response({ username: "eviltrout" }); - } - - if (data.password === "not-activated") { - return response({ - error: "not active", - reason: "not_activated", - sent_to_email: "eviltrout@example.com", - current_email: "current@example.com" - }); - } - - if (data.password === "not-activated-edit") { - return response({ - error: "not active", - reason: "not_activated", - sent_to_email: "eviltrout@example.com", - current_email: "current@example.com" - }); - } - - if (data.password === "need-second-factor") { - if (data.second_factor_token && data.second_factor_token === "123456") { - return response({ username: "eviltrout" }); - } - - return response({ - failed: "FAILED", - ok: false, - error: - "Invalid authentication code. Each code can only be used once.", - reason: "invalid_second_factor", - backup_enabled: true, - security_key_enabled: false, - totp_enabled: true, - multiple_second_factor_methods: false - }); - } - - if (data.password === "need-security-key") { - if (data.securityKeyCredential) { - return response({ username: "eviltrout" }); - } - - return response({ - failed: "FAILED", - ok: false, - error: - "The selected second factor method is not enabled for your account.", - reason: "not_enabled_second_factor_method", - backup_enabled: false, - security_key_enabled: true, - totp_enabled: false, - multiple_second_factor_methods: false, - allowed_credential_ids: ["allowed_credential_ids"], - challenge: "challenge" - }); - } - - return response(400, { error: "invalid login" }); - }); - - this.post("/u/action/send_activation_email", success); - this.put("/u/update-activation-email", success); - - this.get("/u/hp.json", function() { - return response({ - value: "32faff1b1ef1ac3", - challenge: "61a3de0ccf086fb9604b76e884d75801" - }); - }); - - this.get("/session/csrf", function() { - return response({ csrf: "mgk906YLagHo2gOgM1ddYjAN4hQolBdJCqlY6jYzAYs=" }); - }); - - this.get("/groups/check-name", () => { - return response({ available: true }); - }); - - this.get("/u/check_username", function(request) { - if (request.queryParams.username === "taken") { - return response({ available: false, suggestion: "nottaken" }); - } - return response({ available: true }); - }); - - this.post("/u", () => response({ success: true })); - - this.get("/login.html", () => [200, {}, "LOGIN PAGE"]); - - this.delete("/posts/:post_id", success); - this.put("/posts/:post_id/recover", success); - this.get("/posts/:post_id/expand-embed", success); - - this.put("/posts/:post_id", request => { - const data = parsePostData(request.requestBody); - data.post.id = request.params.post_id; - data.post.version = 2; - return response(200, data.post); - }); - - this.get("/t/403.json", () => response(403, {})); - this.get("/t/404.json", () => response(404, "not found")); - this.get("/t/500.json", () => response(502, {})); - - this.put("/t/:slug/:id", request => { - const isJSON = request.requestHeaders["Content-Type"].includes( - "application/json" - ); - - const data = isJSON - ? JSON.parse(request.requestBody) - : parsePostData(request.requestBody); - - return response(200, { - basic_topic: { - id: request.params.id, - title: data.title, - fancy_title: data.title, - slug: request.params.slug - } - }); - }); - - this.get("groups", () => { - return response(200, fixturesByUrl["/groups.json"]); - }); - - this.get("/groups.json", () => { - return response(200, fixturesByUrl["/groups.json?username=eviltrout"]); - }); - - this.get("groups/search.json", () => { - return response(200, []); - }); - - this.get("/topics/groups/discourse.json", () => { - return response(200, fixturesByUrl["/topics/groups/discourse.json"]); - }); - - this.get("/groups/discourse/mentions.json", () => { - return response(200, fixturesByUrl["/groups/discourse/posts.json"]); - }); - - this.get("/groups/discourse/messages.json", () => { - return response(200, fixturesByUrl["/groups/discourse/posts.json"]); - }); - - this.get("/groups/moderators/members.json", () => { - return response(200, fixturesByUrl["/groups/discourse/members.json"]); - }); - - this.get("/t/:topic_id/posts.json", request => { - const postIds = request.queryParams.post_ids; - const postNumber = parseInt(request.queryParams.post_number, 10); - let posts; - - if (postIds) { - posts = postIds.map(p => ({ - id: parseInt(p, 10), - post_number: parseInt(p, 10) - })); - } else if (postNumber && request.queryParams.asc === "true") { - posts = _.range(postNumber + 1, postNumber + 6).map(p => ({ - id: parseInt(p, 10), - post_number: parseInt(p, 10) - })); - } else if (postNumber && request.queryParams.asc === "false") { - posts = _.range(postNumber - 5, postNumber) - .reverse() - .map(p => ({ - id: parseInt(p, 10), - post_number: parseInt(p, 10) - })); - } - - return response(200, { post_stream: { posts } }); - }); - - this.get("/posts/:post_id/reply-history.json", () => { - return response(200, [{ id: 2222, post_number: 2222 }]); - }); - - this.get("/posts/:post_id/reply-ids.json", () => { - return response(200, { - direct_reply_ids: [45], - all_reply_ids: [45, 100] - }); - }); - - this.post("/user_badges", () => - response(200, fixturesByUrl["/user_badges"]) - ); - this.delete("/user_badges/:badge_id", success); - - this.post("/posts", function(request) { - const data = parsePostData(request.requestBody); - - if (data.title === "this title triggers an error") { - return response(422, { errors: ["That title has already been taken"] }); - } - - if (data.raw === "enqueue this content please") { - return response(200, { - success: true, - action: "enqueued", - pending_post: { - id: 1234, - raw: data.raw - } - }); - } - - if (data.raw === "custom message") { - return response(200, { - success: true, - action: "custom", - message: "This is a custom response", - route_to: "/faq" - }); - } - - return response(200, { - success: true, - action: "create_post", - post: { - id: 12345, - topic_id: 280, - topic_slug: "internationalization-localization" - } - }); - }); - - this.post("/topics/timings", () => response(200, {})); - - const siteText = { id: "site.test", value: "Test McTest" }; - const overridden = { - id: "site.overridden", - value: "Overridden", - overridden: true - }; - - this.get("/admin/users/list/active.json", request => { - let store = [ - { - id: 1, - username: "eviltrout", - email: "eviltrout@example.com" - }, - { - id: 3, - username: "discobot", - email: "discobot_email" - } - ]; - - const showEmails = request.queryParams.show_emails; - - if (showEmails === "false") { - store = store.map(item => { - delete item.email; - return item; - }); - } - - const ascending = request.queryParams.ascending; - const order = request.queryParams.order; - - if (order) { - store = store.sort(function(a, b) { - return a[order] - b[order]; - }); - } - - if (ascending) { - store = store.reverse(); - } - - return response(200, store); - }); - - this.get("/admin/users/list/suspect.json", () => { - return response(200, [ - { - id: 2, - username: "sam", - email: "sam@example.com" - } - ]); - }); - - this.get("/admin/customize/site_texts", request => { - if (request.queryParams.overridden) { - return response(200, { site_texts: [overridden] }); - } else { - return response(200, { site_texts: [siteText, overridden] }); - } - }); - - this.get("/admin/customize/site_texts/:key", () => - response(200, { site_text: siteText }) - ); - this.delete("/admin/customize/site_texts/:key", () => - response(200, { site_text: siteText }) - ); - - this.put("/admin/customize/site_texts/:key", request => { - const result = parsePostData(request.requestBody); - result.id = request.params.key; - result.can_revert = true; - return response(200, { site_text: result }); - }); - - this.get("/tag_groups", () => response(200, { tag_groups: [] })); - - this.get("/admin/users/1234.json", () => { - return response(200, { - id: 1234, - username: "regular" - }); - }); - - this.get("/admin/users/1.json", () => { - return response(200, { - id: 1, - username: "eviltrout", - admin: true - }); - }); - - this.get("/admin/users/2.json", () => { - return response(200, { - id: 2, - username: "sam", - admin: true - }); - }); - - this.delete("/admin/users/:user_id.json", () => - response(200, { deleted: true }) - ); - this.post("/admin/badges", success); - this.delete("/admin/badges/:id", success); - - this.get("/admin/logs/staff_action_logs.json", () => { - return response(200, { - staff_action_logs: [], - extras: { user_history_actions: [] } - }); - }); - - this.get("/admin/logs/watched_words", () => { - return response(200, fixturesByUrl["/admin/logs/watched_words.json"]); - }); - this.delete("/admin/logs/watched_words/:id.json", success); - - this.post("/admin/logs/watched_words.json", request => { - const result = parsePostData(request.requestBody); - result.id = new Date().getTime(); - return response(200, result); - }); - - this.get("/admin/logs/search_logs.json", () => { - return response(200, [ - { term: "foobar", searches: 35, click_through: 6, unique: 16 } - ]); - }); - - this.get("/admin/logs/search_logs/term.json", () => { - return response(200, { - term: { - type: "search_log_term", - title: "Search Count", - term: "ruby", - data: [{ x: "2017-07-20", y: 2 }] - } - }); - }); - - this.post("/uploads/lookup-metadata", () => { - return response(200, { - imageFilename: "somefile.png", - imageFilesize: "10 KB", - imageWidth: "1", - imageHeight: "1" - }); - }); - - this.get("/inline-onebox", request => { - if ( - request.queryParams.urls.includes( - "http://www.example.com/has-title.html" - ) - ) { - return [ - 200, - { "Content-Type": "application/html" }, - '{"inline-oneboxes":[{"url":"http://www.example.com/has-title.html","title":"This is a great title"}]}' - ]; - } - }); - - this.get("/onebox", request => { - if ( - request.queryParams.url === "http://www.example.com/has-title.html" || - request.queryParams.url === - "http://www.example.com/has-title-and-a-url-that-is-more-than-80-characters-because-thats-good-for-seo-i-guess.html" - ) { - return [ - 200, - { "Content-Type": "application/html" }, - '' - ]; - } - - if (request.queryParams.url === "http://www.example.com/no-title.html") { - return [ - 200, - { "Content-Type": "application/html" }, - '' - ]; - } - - if (request.queryParams.url.indexOf("/internal-page.html") > -1) { - return [ - 200, - { "Content-Type": "application/html" }, - '' - ]; - } - - return [404, { "Content-Type": "application/html" }, ""]; + }, + badges: [{ id: 444, count: 1 }], + topics: [{ id: 1234, title: "cool title", url: "/t/1234/cool-title" }] }); }); - server.prepareBody = function(body) { - if (body && typeof body === "object") { - return JSON.stringify(body); + pretender.get("/u/eviltrout/invited_count.json", () => { + return response({ + counts: { pending: 1, redeemed: 0, total: 0 } + }); + }); + + pretender.get("/u/eviltrout/invited.json", () => { + return response({ invites: [{ id: 1 }] }); + }); + + pretender.get("/topics/private-messages/eviltrout.json", () => { + return response(fixturesByUrl["/topics/private-messages/eviltrout.json"]); + }); + + pretender.get("/topics/feature_stats.json", () => { + return response({ + pinned_in_category_count: 0, + pinned_globally_count: 0, + banner_count: 0 + }); + }); + + pretender.put("/t/34/convert-topic/public", () => { + return response({}); + }); + + pretender.put("/t/280/make-banner", () => { + return response({}); + }); + + pretender.put("/t/internationalization-localization/280/status", () => { + return response({ + success: "OK", + topic_status_update: null + }); + }); + + pretender.post("/clicks/track", success); + + pretender.get("/search", request => { + if (request.queryParams.q === "posts") { + return response({ + posts: [ + { + id: 1234 + } + ] + }); + } else if (request.queryParams.q === "evil") { + return response({ + posts: [ + { + id: 1234 + } + ], + tags: [ + { + id: 6, + name: "eviltrout" + } + ] + }); } - return body; + + return response({}); + }); + + pretender.put("/u/eviltrout.json", () => response({ user: {} })); + + pretender.get("/t/280.json", () => response(fixturesByUrl["/t/280/1.json"])); + pretender.get("/t/34.json", () => response(fixturesByUrl["/t/34/1.json"])); + pretender.get("/t/280/:post_number.json", () => + response(fixturesByUrl["/t/280/1.json"]) + ); + pretender.get("/t/28830.json", () => + response(fixturesByUrl["/t/28830/1.json"]) + ); + pretender.get("/t/9.json", () => response(fixturesByUrl["/t/9/1.json"])); + pretender.get("/t/12.json", () => response(fixturesByUrl["/t/12/1.json"])); + pretender.put("/t/1234/re-pin", success); + + pretender.get("/t/id_for/:slug", () => { + return response({ + id: 280, + slug: "internationalization-localization", + url: "/t/internationalization-localization/280" + }); + }); + + pretender.delete("/t/:id", success); + pretender.put("/t/:id/recover", success); + pretender.put("/t/:id/publish", success); + + pretender.get("/404-body", () => { + return [ + 200, + { "Content-Type": "text/html" }, + "
not found
" + ]; + }); + + pretender.delete("/draft.json", success); + pretender.post("/draft.json", success); + + pretender.get("/u/:username/staff-info.json", () => response({})); + + pretender.get("/post_action_users", () => { + return response({ + post_action_users: [ + { + id: 1, + username: "eviltrout", + avatar_template: "/user_avatar/default/eviltrout/{size}/1.png", + username_lower: "eviltrout" + } + ] + }); + }); + + pretender.get("/post_replies", () => { + return response({ post_replies: [{ id: 1234, cooked: "wat" }] }); + }); + + pretender.get("/post_reply_histories", () => { + return response({ post_reply_histories: [{ id: 1234, cooked: "wat" }] }); + }); + + pretender.get("/category_hashtags/check", () => { + return response({ valid: [{ slug: "bug", url: "/c/bugs" }] }); + }); + + pretender.get("/categories_and_latest", () => + response(fixturesByUrl["/categories_and_latest.json"]) + ); + + pretender.put("/categories/:category_id", request => { + const category = parsePostData(request.requestBody); + category.id = parseInt(request.params.category_id, 10); + + if (category.email_in === "duplicate@example.com") { + return response(422, { errors: ["duplicate email"] }); + } + + return response({ category }); + }); + + pretender.get("/draft.json", request => { + if (request.queryParams.draft_key === "new_topic") { + return response(fixturesByUrl["/draft.json"]); + } else if (request.queryParams.draft_key.startsWith("topic_")) + return response( + fixturesByUrl[request.url] || { + draft: null, + draft_sequence: 0 + } + ); + return response({}); + }); + + pretender.get("/drafts.json", () => response(fixturesByUrl["/drafts.json"])); + + pretender.put("/queued_posts/:queued_post_id", function(request) { + return response({ queued_post: { id: request.params.queued_post_id } }); + }); + + pretender.get("/queued_posts", function() { + return response({ + queued_posts: [{ id: 1, raw: "queued post text", can_delete_user: true }] + }); + }); + + pretender.post("/session", function(request) { + const data = parsePostData(request.requestBody); + + if (data.password === "correct") { + return response({ username: "eviltrout" }); + } + + if (data.password === "not-activated") { + return response({ + error: "not active", + reason: "not_activated", + sent_to_email: "eviltrout@example.com", + current_email: "current@example.com" + }); + } + + if (data.password === "not-activated-edit") { + return response({ + error: "not active", + reason: "not_activated", + sent_to_email: "eviltrout@example.com", + current_email: "current@example.com" + }); + } + + if (data.password === "need-second-factor") { + if (data.second_factor_token && data.second_factor_token === "123456") { + return response({ username: "eviltrout" }); + } + + return response({ + failed: "FAILED", + ok: false, + error: "Invalid authentication code. Each code can only be used once.", + reason: "invalid_second_factor", + backup_enabled: true, + security_key_enabled: false, + totp_enabled: true, + multiple_second_factor_methods: false + }); + } + + if (data.password === "need-security-key") { + if (data.securityKeyCredential) { + return response({ username: "eviltrout" }); + } + + return response({ + failed: "FAILED", + ok: false, + error: + "The selected second factor method is not enabled for your account.", + reason: "not_enabled_second_factor_method", + backup_enabled: false, + security_key_enabled: true, + totp_enabled: false, + multiple_second_factor_methods: false, + allowed_credential_ids: ["allowed_credential_ids"], + challenge: "challenge" + }); + } + + return response(400, { error: "invalid login" }); + }); + + pretender.post("/u/action/send_activation_email", success); + pretender.put("/u/update-activation-email", success); + + pretender.get("/u/hp.json", function() { + return response({ + value: "32faff1b1ef1ac3", + challenge: "61a3de0ccf086fb9604b76e884d75801" + }); + }); + + pretender.get("/session/csrf", function() { + return response({ csrf: "mgk906YLagHo2gOgM1ddYjAN4hQolBdJCqlY6jYzAYs=" }); + }); + + pretender.get("/groups/check-name", () => { + return response({ available: true }); + }); + + pretender.get("/u/check_username", function(request) { + if (request.queryParams.username === "taken") { + return response({ available: false, suggestion: "nottaken" }); + } + return response({ available: true }); + }); + + pretender.post("/u", () => response({ success: true })); + + pretender.get("/login.html", () => [200, {}, "LOGIN PAGE"]); + + pretender.delete("/posts/:post_id", success); + pretender.put("/posts/:post_id/recover", success); + pretender.get("/posts/:post_id/expand-embed", success); + + pretender.put("/posts/:post_id", request => { + const data = parsePostData(request.requestBody); + if (data.post.raw === "this will 409") { + return [ + 409, + { "Content-Type": "application/json" }, + { errors: ["edit conflict"] } + ]; + } + data.post.id = request.params.post_id; + data.post.version = 2; + return response(200, data.post); + }); + + pretender.get("/t/403.json", () => response(403, {})); + pretender.get("/t/404.json", () => response(404, "not found")); + pretender.get("/t/500.json", () => response(502, {})); + + pretender.put("/t/:slug/:id", request => { + const isJSON = request.requestHeaders["Content-Type"].includes( + "application/json" + ); + + const data = isJSON + ? JSON.parse(request.requestBody) + : parsePostData(request.requestBody); + + return response(200, { + basic_topic: { + id: request.params.id, + title: data.title, + fancy_title: data.title, + slug: request.params.slug + } + }); + }); + + pretender.get("groups", () => { + return response(200, fixturesByUrl["/groups.json"]); + }); + + pretender.get("/groups.json", () => { + return response(200, fixturesByUrl["/groups.json?username=eviltrout"]); + }); + + pretender.get("groups/search.json", () => { + return response(200, []); + }); + + pretender.get("/topics/groups/discourse.json", () => { + return response(200, fixturesByUrl["/topics/groups/discourse.json"]); + }); + + pretender.get("/groups/discourse/mentions.json", () => { + return response(200, fixturesByUrl["/groups/discourse/posts.json"]); + }); + + pretender.get("/groups/discourse/messages.json", () => { + return response(200, fixturesByUrl["/groups/discourse/posts.json"]); + }); + + pretender.get("/groups/moderators/members.json", () => { + return response(200, fixturesByUrl["/groups/discourse/members.json"]); + }); + + pretender.get("/t/:topic_id/posts.json", request => { + const postIds = request.queryParams.post_ids; + const postNumber = parseInt(request.queryParams.post_number, 10); + let posts; + + if (postIds) { + posts = postIds.map(p => ({ + id: parseInt(p, 10), + post_number: parseInt(p, 10) + })); + } else if (postNumber && request.queryParams.asc === "true") { + posts = _.range(postNumber + 1, postNumber + 6).map(p => ({ + id: parseInt(p, 10), + post_number: parseInt(p, 10) + })); + } else if (postNumber && request.queryParams.asc === "false") { + posts = _.range(postNumber - 5, postNumber) + .reverse() + .map(p => ({ + id: parseInt(p, 10), + post_number: parseInt(p, 10) + })); + } + + return response(200, { post_stream: { posts } }); + }); + + pretender.get("/posts/:post_id/reply-history.json", () => { + return response(200, [{ id: 2222, post_number: 2222 }]); + }); + + pretender.get("/posts/:post_id/reply-ids.json", () => { + return response(200, { + direct_reply_ids: [45], + all_reply_ids: [45, 100] + }); + }); + + pretender.post("/user_badges", () => + response(200, fixturesByUrl["/user_badges"]) + ); + pretender.delete("/user_badges/:badge_id", success); + + pretender.post("/posts", function(request) { + const data = parsePostData(request.requestBody); + + if (data.title === "this title triggers an error") { + return response(422, { errors: ["That title has already been taken"] }); + } + + if (data.raw === "enqueue this content please") { + return response(200, { + success: true, + action: "enqueued", + pending_post: { + id: 1234, + raw: data.raw + } + }); + } + + if (data.raw === "custom message") { + return response(200, { + success: true, + action: "custom", + message: "This is a custom response", + route_to: "/faq" + }); + } + + return response(200, { + success: true, + action: "create_post", + post: { + id: 12345, + topic_id: 280, + topic_slug: "internationalization-localization" + } + }); + }); + + pretender.post("/topics/timings", () => response(200, {})); + + const siteText = { id: "site.test", value: "Test McTest" }; + const overridden = { + id: "site.overridden", + value: "Overridden", + overridden: true }; - server.unhandledRequest = function(verb, path) { - const error = - "Unhandled request in test environment: " + path + " (" + verb + ")"; - window.console.error(error); - throw error; - }; + pretender.get("/admin/users/list/active.json", request => { + let store = [ + { + id: 1, + username: "eviltrout", + email: "eviltrout@example.com" + }, + { + id: 3, + username: "discobot", + email: "discobot_email" + } + ]; - server.checkPassthrough = request => - request.requestHeaders["Discourse-Script"]; - return server; + const showEmails = request.queryParams.show_emails; + + if (showEmails === "false") { + store = store.map(item => { + delete item.email; + return item; + }); + } + + const ascending = request.queryParams.ascending; + const order = request.queryParams.order; + + if (order) { + store = store.sort(function(a, b) { + return a[order] - b[order]; + }); + } + + if (ascending) { + store = store.reverse(); + } + + return response(200, store); + }); + + pretender.get("/admin/users/list/suspect.json", () => { + return response(200, [ + { + id: 2, + username: "sam", + email: "sam@example.com" + } + ]); + }); + + pretender.get("/admin/customize/site_texts", request => { + if (request.queryParams.overridden) { + return response(200, { site_texts: [overridden] }); + } else { + return response(200, { site_texts: [siteText, overridden] }); + } + }); + + pretender.get("/admin/customize/site_texts/:key", () => + response(200, { site_text: siteText }) + ); + pretender.delete("/admin/customize/site_texts/:key", () => + response(200, { site_text: siteText }) + ); + + pretender.put("/admin/customize/site_texts/:key", request => { + const result = parsePostData(request.requestBody); + result.id = request.params.key; + result.can_revert = true; + return response(200, { site_text: result }); + }); + + pretender.get("/tag_groups", () => response(200, { tag_groups: [] })); + + pretender.get("/admin/users/1.json", () => { + return response(200, { + id: 1, + username: "eviltrout", + email: "eviltrout@example.com", + admin: true + }); + }); + + pretender.get("/admin/users/2.json", () => { + return response(200, { + id: 2, + username: "sam", + admin: true + }); + }); + + pretender.get("/admin/users/3.json", () => { + return response(200, { + id: 3, + username: "markvanlan", + email: "markvanlan@example.com", + secondary_emails: ["markvanlan1@example.com", "markvanlan2@example.com"] + }); + }); + + pretender.get("/admin/users/1234.json", () => { + return response(200, { + id: 1234, + username: "regular" + }); + }); + + pretender.get("/admin/users/1235.json", () => { + return response(200, { + id: 1235, + username: "regular2" + }); + }); + + pretender.delete("/admin/users/:user_id.json", () => + response(200, { deleted: true }) + ); + pretender.post("/admin/badges", success); + pretender.delete("/admin/badges/:id", success); + + pretender.get("/admin/logs/staff_action_logs.json", () => { + return response(200, { + staff_action_logs: [], + extras: { user_history_actions: [] } + }); + }); + + pretender.get("/admin/logs/watched_words", () => { + return response(200, fixturesByUrl["/admin/logs/watched_words.json"]); + }); + pretender.delete("/admin/logs/watched_words/:id.json", success); + + pretender.post("/admin/logs/watched_words.json", request => { + const result = parsePostData(request.requestBody); + result.id = new Date().getTime(); + return response(200, result); + }); + + pretender.get("/admin/logs/search_logs.json", () => { + return response(200, [ + { term: "foobar", searches: 35, click_through: 6, unique: 16 } + ]); + }); + + pretender.get("/admin/logs/search_logs/term.json", () => { + return response(200, { + term: { + type: "search_log_term", + title: "Search Count", + term: "ruby", + data: [{ x: "2017-07-20", y: 2 }] + } + }); + }); + + pretender.post("/uploads/lookup-metadata", () => { + return response(200, { + imageFilename: "somefile.png", + imageFilesize: "10 KB", + imageWidth: "1", + imageHeight: "1" + }); + }); + + pretender.get("/inline-onebox", request => { + if ( + request.queryParams.urls.includes("http://www.example.com/has-title.html") + ) { + return [ + 200, + { "Content-Type": "application/html" }, + '{"inline-oneboxes":[{"url":"http://www.example.com/has-title.html","title":"This is a great title"}]}' + ]; + } + }); + + pretender.get("/onebox", request => { + if ( + request.queryParams.url === "http://www.example.com/has-title.html" || + request.queryParams.url === + "http://www.example.com/has-title-and-a-url-that-is-more-than-80-characters-because-thats-good-for-seo-i-guess.html" + ) { + return [ + 200, + { "Content-Type": "application/html" }, + '' + ]; + } + + if (request.queryParams.url === "http://www.example.com/no-title.html") { + return [ + 200, + { "Content-Type": "application/html" }, + '' + ]; + } + + if (request.queryParams.url.indexOf("/internal-page.html") > -1) { + return [ + 200, + { "Content-Type": "application/html" }, + '' + ]; + } + if (request.queryParams.url === "http://somegoodurl.com/") { + return [ + 200, + { "Content-Type": "application/html" }, + ` + + ` + ]; + } + return [404, { "Content-Type": "application/html" }, ""]; + }); } diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index 34d92149b7..4b6ed9330f 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -1,5 +1,4 @@ import { isEmpty } from "@ember/utils"; -import { run } from "@ember/runloop"; import { later } from "@ember/runloop"; /* global QUnit, resetSite */ @@ -156,16 +155,6 @@ export function controllerFor(controller, model) { return controller; } -export function asyncTestDiscourse(text, func) { - QUnit.test(text, function(assert) { - const done = assert.async(); - run(() => { - func.call(this, assert); - done(); - }); - }); -} - export function fixture(selector) { if (selector) { return $("#qunit-fixture").find(selector); diff --git a/test/javascripts/helpers/site-settings.js b/test/javascripts/helpers/site-settings.js index 96807b7a7c..621aa3d6ac 100644 --- a/test/javascripts/helpers/site-settings.js +++ b/test/javascripts/helpers/site-settings.js @@ -99,7 +99,8 @@ Discourse.SiteSettingsOriginal = { desktop_category_page_style: "categories_and_latest_topics", enable_mentions: true, enable_personal_messages: true, - unicode_usernames: false + unicode_usernames: false, + secure_media: false }; Discourse.SiteSettings = jQuery.extend( true, diff --git a/test/javascripts/lib/click-track-edit-history-test.js.es6 b/test/javascripts/lib/click-track-edit-history-test.js.es6 index 745d7d195c..ae8f6d18c2 100644 --- a/test/javascripts/lib/click-track-edit-history-test.js.es6 +++ b/test/javascripts/lib/click-track-edit-history-test.js.es6 @@ -2,6 +2,7 @@ import DiscourseURL from "discourse/lib/url"; import ClickTrack from "discourse/lib/click-track"; import { fixture, logIn } from "helpers/qunit-helpers"; import User from "discourse/models/user"; +import pretender from "helpers/create-pretender"; QUnit.module("lib:click-track-edit-history", { beforeEach() { @@ -62,8 +63,7 @@ QUnit.skip("tracks internal URLs", async assert => { sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); const done = assert.async(); - /* global server */ - server.post("/clicks/track", request => { + pretender.post("/clicks/track", request => { assert.equal( request.requestBody, "url=http%3A%2F%2Fdiscuss.domain.com&post_id=42&topic_id=1337" @@ -78,8 +78,7 @@ QUnit.skip("tracks external URLs", async assert => { assert.expect(2); const done = assert.async(); - /* global server */ - server.post("/clicks/track", request => { + pretender.post("/clicks/track", request => { assert.equal( request.requestBody, "url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" @@ -97,8 +96,7 @@ QUnit.skip( User.currentProp("external_links_in_new_tab", true); const done = assert.async(); - /* global server */ - server.post("/clicks/track", request => { + pretender.post("/clicks/track", request => { assert.equal( request.requestBody, "url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" diff --git a/test/javascripts/lib/click-track-profile-page-test.js.es6 b/test/javascripts/lib/click-track-profile-page-test.js.es6 index a1cc164013..d46027a9d2 100644 --- a/test/javascripts/lib/click-track-profile-page-test.js.es6 +++ b/test/javascripts/lib/click-track-profile-page-test.js.es6 @@ -1,6 +1,7 @@ import DiscourseURL from "discourse/lib/url"; import ClickTrack from "discourse/lib/click-track"; import { fixture, logIn } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; QUnit.module("lib:click-track-profile-page", { beforeEach() { @@ -55,8 +56,7 @@ QUnit.skip("tracks internal URLs", async assert => { sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); const done = assert.async(); - /* global server */ - server.post("/clicks/track", request => { + pretender.post("/clicks/track", request => { assert.equal(request.requestBody, "url=http%3A%2F%2Fdiscuss.domain.com"); done(); }); @@ -68,8 +68,7 @@ QUnit.skip("tracks external URLs", async assert => { assert.expect(2); const done = assert.async(); - /* global server */ - server.post("/clicks/track", request => { + pretender.post("/clicks/track", request => { assert.equal( request.requestBody, "url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" @@ -84,8 +83,7 @@ QUnit.skip("tracks external URLs in other posts", async assert => { assert.expect(2); const done = assert.async(); - /* global server */ - server.post("/clicks/track", request => { + pretender.post("/clicks/track", request => { assert.equal( request.requestBody, "url=http%3A%2F%2Fwww.google.com&post_id=24&topic_id=7331" diff --git a/test/javascripts/lib/click-track-test.js.es6 b/test/javascripts/lib/click-track-test.js.es6 index 7dd0747e72..7d5167ce2f 100644 --- a/test/javascripts/lib/click-track-test.js.es6 +++ b/test/javascripts/lib/click-track-test.js.es6 @@ -3,6 +3,7 @@ import DiscourseURL from "discourse/lib/url"; import ClickTrack from "discourse/lib/click-track"; import { fixture, logIn } from "helpers/qunit-helpers"; import User from "discourse/models/user"; +import pretender from "helpers/create-pretender"; QUnit.module("lib:click-track", { beforeEach() { @@ -55,8 +56,7 @@ QUnit.skip("tracks internal URLs", async assert => { sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); const done = assert.async(); - /* global server */ - server.post("/clicks/track", request => { + pretender.post("/clicks/track", request => { assert.ok( request.requestBody, "url=http%3A%2F%2Fdiscuss.domain.com&post_id=42&topic_id=1337" @@ -74,8 +74,7 @@ QUnit.skip("does not track elements with no href", async assert => { QUnit.skip("does not track attachments", async assert => { sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); - /* global server */ - server.post("/clicks/track", () => assert.ok(false)); + pretender.post("/clicks/track", () => assert.ok(false)); assert.notOk(track(generateClickEventOn(".attachment"))); assert.ok( @@ -89,8 +88,7 @@ QUnit.skip("tracks external URLs", async assert => { assert.expect(2); const done = assert.async(); - /* global server */ - server.post("/clicks/track", request => { + pretender.post("/clicks/track", request => { assert.ok( request.requestBody, "url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" @@ -108,8 +106,7 @@ QUnit.skip( User.currentProp("external_links_in_new_tab", true); const done = assert.async(); - /* global server */ - server.post("/clicks/track", request => { + pretender.post("/clicks/track", request => { assert.ok( request.requestBody, "url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" diff --git a/test/javascripts/lib/link-mentions-test.js.es6 b/test/javascripts/lib/link-mentions-test.js.es6 index 46bdefa423..a7c309cbbb 100644 --- a/test/javascripts/lib/link-mentions-test.js.es6 +++ b/test/javascripts/lib/link-mentions-test.js.es6 @@ -3,12 +3,12 @@ import { linkSeenMentions } from "discourse/lib/link-mentions"; import { Promise } from "rsvp"; +import pretender from "helpers/create-pretender"; QUnit.module("lib:link-mentions"); QUnit.test("linkSeenMentions replaces users and groups", async assert => { - /* global server */ - server.get("/u/is_local_username", () => [ + pretender.get("/u/is_local_username", () => [ 200, { "Content-Type": "application/json" }, { diff --git a/test/javascripts/lib/load-script-test.js.es6 b/test/javascripts/lib/load-script-test.js.es6 index 1d3094bdf7..e73a1bafa9 100644 --- a/test/javascripts/lib/load-script-test.js.es6 +++ b/test/javascripts/lib/load-script-test.js.es6 @@ -12,11 +12,10 @@ QUnit.skip( const src = "/javascripts/ace/ace.js"; - await loadScript(src).then(() => { - assert.ok( - typeof window.ace !== "undefined", - "callbacks should only be executed after the script has fully loaded" - ); - }); + await loadScript(src); + assert.ok( + typeof window.ace !== "undefined", + "callbacks should only be executed after the script has fully loaded" + ); } ); diff --git a/test/javascripts/lib/oneboxer-test.js.es6 b/test/javascripts/lib/oneboxer-test.js.es6 index 77ca103827..22e86e97df 100644 --- a/test/javascripts/lib/oneboxer-test.js.es6 +++ b/test/javascripts/lib/oneboxer-test.js.es6 @@ -20,11 +20,6 @@ QUnit.test("load - failed onebox", async assert => { let element = document.createElement("A"); element.setAttribute("href", "http://somebadurl.com"); - // prettier-ignore - server.get("/onebox", () => { //eslint-disable-line - return [404, {}, {}]; - }); - await loadOnebox(element); assert.equal( @@ -55,11 +50,6 @@ QUnit.test("load - successful onebox", async assert => { `; - // prettier-ignore - server.get("/onebox", () => { //eslint-disable-line - return [200, {}, html]; - }); - let element = document.createElement("A"); element.setAttribute("href", "http://somegoodurl.com"); @@ -72,7 +62,7 @@ QUnit.test("load - successful onebox", async assert => { ); assert.equal( loadOnebox(element), - stringToHTML(html).outerHTML, + html.trim(), "it returns the html from the cache" ); }); diff --git a/test/javascripts/lib/preload-store-test.js.es6 b/test/javascripts/lib/preload-store-test.js.es6 index b0560b169c..e25e89ee82 100644 --- a/test/javascripts/lib/preload-store-test.js.es6 +++ b/test/javascripts/lib/preload-store-test.js.es6 @@ -1,5 +1,4 @@ import PreloadStore from "preload-store"; -import { asyncTestDiscourse } from "helpers/qunit-helpers"; import { Promise } from "rsvp"; QUnit.module("preload-store", { @@ -22,81 +21,45 @@ QUnit.test("remove", assert => { assert.blank(PreloadStore.get("bane"), "removes the value if the key exists"); }); -asyncTestDiscourse( +QUnit.test( "getAndRemove returns a promise that resolves to null", - function(assert) { - assert.expect(1); - - const done = assert.async(); - PreloadStore.getAndRemove("joker").then(function(result) { - assert.blank(result); - done(); - }); + async assert => { + assert.blank(await PreloadStore.getAndRemove("joker")); } ); -asyncTestDiscourse( +QUnit.test( "getAndRemove returns a promise that resolves to the result of the finder", - function(assert) { - assert.expect(1); + async assert => { + const finder = () => "batdance"; + const result = await PreloadStore.getAndRemove("joker", finder); - const done = assert.async(); - const finder = function() { - return "batdance"; - }; - PreloadStore.getAndRemove("joker", finder).then(function(result) { - assert.equal(result, "batdance"); - done(); - }); + assert.equal(result, "batdance"); } ); -asyncTestDiscourse( +QUnit.test( "getAndRemove returns a promise that resolves to the result of the finder's promise", - function(assert) { - assert.expect(1); + async assert => { + const finder = () => Promise.resolve("hahahah"); + const result = await PreloadStore.getAndRemove("joker", finder); - const finder = function() { - return new Promise(function(resolve) { - resolve("hahahah"); - }); - }; - - const done = assert.async(); - PreloadStore.getAndRemove("joker", finder).then(function(result) { - assert.equal(result, "hahahah"); - done(); - }); + assert.equal(result, "hahahah"); } ); -asyncTestDiscourse( +QUnit.test( "returns a promise that rejects with the result of the finder's rejected promise", - function(assert) { - assert.expect(1); + async assert => { + const finder = () => Promise.reject("error"); - const finder = function() { - return new Promise(function(resolve, reject) { - reject("error"); - }); - }; - - const done = assert.async(); - PreloadStore.getAndRemove("joker", finder).then(null, function(result) { + await PreloadStore.getAndRemove("joker", finder).catch(result => { assert.equal(result, "error"); - done(); }); } ); -asyncTestDiscourse("returns a promise that resolves to 'evil'", function( - assert -) { - assert.expect(1); - - const done = assert.async(); - PreloadStore.getAndRemove("bane").then(function(result) { - assert.equal(result, "evil"); - done(); - }); +QUnit.test("returns a promise that resolves to 'evil'", async assert => { + const result = await PreloadStore.getAndRemove("bane"); + assert.equal(result, "evil"); }); diff --git a/test/javascripts/lib/pretty-text-test.js.es6 b/test/javascripts/lib/pretty-text-test.js.es6 index bab2d71d3a..7c75034529 100644 --- a/test/javascripts/lib/pretty-text-test.js.es6 +++ b/test/javascripts/lib/pretty-text-test.js.es6 @@ -973,6 +973,58 @@ QUnit.test("images", assert => { ); }); +QUnit.test("attachment", assert => { + assert.cooked( + "[test.pdf|attachment](upload://o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf)", + `

test.pdf

`, + "It returns the correct attachment link HTML" + ); +}); + +QUnit.test("attachment - mapped url - secure media disabled", assert => { + function lookupUploadUrls() { + let cache = {}; + cache["upload://o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf"] = { + short_url: "upload://o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf", + url: + "/secure-media-uploads/original/3X/c/b/o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf", + short_path: "/uploads/short-url/blah" + }; + return cache; + } + assert.cookedOptions( + "[test.pdf|attachment](upload://o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf)", + { + siteSettings: { secure_media: false }, + lookupUploadUrls: lookupUploadUrls + }, + `

test.pdf

`, + "It returns the correct attachment link HTML when the URL is mapped without secure media" + ); +}); + +QUnit.test("attachment - mapped url - secure media enabled", assert => { + function lookupUploadUrls() { + let cache = {}; + cache["upload://o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf"] = { + short_url: "upload://o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf", + url: + "/secure-media-uploads/original/3X/c/b/o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf", + short_path: "/uploads/short-url/blah" + }; + return cache; + } + assert.cookedOptions( + "[test.pdf|attachment](upload://o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf)", + { + siteSettings: { secure_media: true }, + lookupUploadUrls: lookupUploadUrls + }, + `

test.pdf

`, + "It returns the correct attachment link HTML when the URL is mapped with secure media" + ); +}); + QUnit.test("video - secure media enabled", assert => { assert.cookedOptions( "![baby shark|video](upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp4)", @@ -1012,6 +1064,32 @@ QUnit.test("video", assert => { ); }); +QUnit.test("video - mapped url - secure media enabled", assert => { + function lookupUploadUrls() { + let cache = {}; + cache["upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp4"] = { + short_url: "upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp4", + url: "/secure-media-uploads/original/3X/c/b/test.mp4", + short_path: "/uploads/short-url/blah" + }; + return cache; + } + assert.cookedOptions( + "![baby shark|video](upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp4)", + { + siteSettings: { secure_media: true }, + lookupUploadUrls: lookupUploadUrls + }, + `

`, + "It returns the correct video HTML when the URL is mapped with secure media, removing data-orig-src" + ); +}); + QUnit.test("audio", assert => { assert.cooked( "![young americans|audio](upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp3)", @@ -1023,6 +1101,30 @@ QUnit.test("audio", assert => { ); }); +QUnit.test("audio - mapped url - secure media enabled", assert => { + function lookupUploadUrls() { + let cache = {}; + cache["upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp3"] = { + short_url: "upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp3", + url: "/secure-media-uploads/original/3X/c/b/test.mp3", + short_path: "/uploads/short-url/blah" + }; + return cache; + } + assert.cookedOptions( + "![baby shark|audio](upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp3)", + { + siteSettings: { secure_media: true }, + lookupUploadUrls: lookupUploadUrls + }, + `

`, + "It returns the correct audio HTML when the URL is mapped with secure media, removing data-orig-src" + ); +}); + QUnit.test("censoring", assert => { assert.cookedOptions( "Pleased to meet you, but pleeeease call me later, xyz123", diff --git a/test/javascripts/lib/screen-track-test.js.es6 b/test/javascripts/lib/screen-track-test.js.es6 index 2fb7df9497..9574e972a7 100644 --- a/test/javascripts/lib/screen-track-test.js.es6 +++ b/test/javascripts/lib/screen-track-test.js.es6 @@ -1,6 +1,7 @@ import TopicTrackingState from "discourse/models/topic-tracking-state"; import Session from "discourse/models/session"; import ScreenTrack from "discourse/lib/screen-track"; +import pretender from "helpers/create-pretender"; let clock; @@ -18,8 +19,7 @@ QUnit.module("lib:screen-track", { QUnit.skip("Correctly flushes posts as needed", assert => { const timings = []; - // prettier-ignore - server.post("/topics/timings", t => { //eslint-disable-line + pretender.post("/topics/timings", t => { timings.push(t); return [200, {}, ""]; }); diff --git a/test/javascripts/lib/upload-short-url-test.js.es6 b/test/javascripts/lib/upload-short-url-test.js.es6 index f969e64878..849b2d7602 100644 --- a/test/javascripts/lib/upload-short-url-test.js.es6 +++ b/test/javascripts/lib/upload-short-url-test.js.es6 @@ -5,14 +5,14 @@ import { } from "pretty-text/upload-short-url"; import { ajax } from "discourse/lib/ajax"; import { fixture } from "helpers/qunit-helpers"; +import pretender from "helpers/create-pretender"; -QUnit.module("lib:pretty-text/upload-short-url", { - beforeEach() { - const response = object => { - return [200, { "Content-Type": "application/json" }, object]; - }; - - const imageSrcs = [ +function stubUrls(imageSrcs, attachmentSrcs, otherMediaSrcs) { + const response = object => { + return [200, { "Content-Type": "application/json" }, object]; + }; + if (!imageSrcs) { + imageSrcs = [ { short_url: "upload://a.jpeg", url: "/uploads/default/original/3X/c/b/1.jpeg", @@ -22,18 +22,27 @@ QUnit.module("lib:pretty-text/upload-short-url", { short_url: "upload://b.jpeg", url: "/uploads/default/original/3X/c/b/2.jpeg", short_path: "/uploads/short-url/b.jpeg" + }, + { + short_url: "upload://z.jpeg", + url: "/uploads/default/original/3X/c/b/9.jpeg", + short_path: "/uploads/short-url/z.jpeg" } ]; + } - const attachmentSrcs = [ + if (!attachmentSrcs) { + attachmentSrcs = [ { short_url: "upload://c.pdf", url: "/uploads/default/original/3X/c/b/3.pdf", short_path: "/uploads/short-url/c.pdf" } ]; + } - const otherMediaSrcs = [ + if (!otherMediaSrcs) { + otherMediaSrcs = [ { short_url: "upload://d.mp4", url: "/uploads/default/original/3X/c/b/4.mp4", @@ -45,30 +54,37 @@ QUnit.module("lib:pretty-text/upload-short-url", { short_path: "/uploads/short-url/e.mp3" } ]; + } + // prettier-ignore + pretender.post("/uploads/lookup-urls", () => { //eslint-disable-line + return response(imageSrcs.concat(attachmentSrcs.concat(otherMediaSrcs))); + }); - // prettier-ignore - server.post("/uploads/lookup-urls", () => { //eslint-disable-line - return response(imageSrcs.concat(attachmentSrcs.concat(otherMediaSrcs))); - }); - - fixture().html( - imageSrcs.map(src => ``).join("") + - attachmentSrcs.map(src => ``).join("") - ); - }, - + fixture().html( + imageSrcs.map(src => ``).join("") + + attachmentSrcs + .map( + src => + `big enterprise contract.pdf` + ) + .join("") + + `
` + ); +} +QUnit.module("lib:pretty-text/upload-short-url", { afterEach() { resetCache(); } }); QUnit.test("resolveAllShortUrls", async assert => { + stubUrls(); let lookup; lookup = lookupCachedUploadUrl("upload://a.jpeg"); assert.deepEqual(lookup, {}); - await resolveAllShortUrls(ajax); + await resolveAllShortUrls(ajax, { secure_media: false }); lookup = lookupCachedUploadUrl("upload://a.jpeg"); @@ -105,3 +121,68 @@ QUnit.test("resolveAllShortUrls", async assert => { short_path: "/uploads/short-url/e.mp3" }); }); + +QUnit.test( + "resolveAllShortUrls - href + src replaced correctly", + async assert => { + stubUrls(); + await resolveAllShortUrls(ajax, { secure_media: false }); + + let image1 = fixture() + .find("img") + .eq(0); + let image2 = fixture() + .find("img") + .eq(1); + let link = fixture().find("a"); + + assert.equal(image1.attr("src"), "/uploads/default/original/3X/c/b/1.jpeg"); + assert.equal(image2.attr("src"), "/uploads/default/original/3X/c/b/2.jpeg"); + assert.equal(link.attr("href"), "/uploads/short-url/c.pdf"); + } +); + +QUnit.test( + "resolveAllShortUrls - when secure media is enabled use the attachment full URL", + async assert => { + stubUrls( + null, + [ + { + short_url: "upload://c.pdf", + url: "/secure-media-uploads/default/original/3X/c/b/3.pdf", + short_path: "/uploads/short-url/c.pdf" + } + ], + null + ); + await resolveAllShortUrls(ajax, { secure_media: true }); + + let link = fixture().find("a"); + assert.equal( + link.attr("href"), + "/secure-media-uploads/default/original/3X/c/b/3.pdf" + ); + } +); + +QUnit.test("resolveAllShortUrls - scoped", async assert => { + stubUrls(); + let lookup; + await resolveAllShortUrls(ajax, ".scoped-area"); + + lookup = lookupCachedUploadUrl("upload://z.jpeg"); + + assert.deepEqual(lookup, { + url: "/uploads/default/original/3X/c/b/9.jpeg", + short_path: "/uploads/short-url/z.jpeg" + }); + + // do this because the pretender caches ALL the urls, not + // just the ones being looked up (like the normal behaviour) + resetCache(); + await resolveAllShortUrls(ajax, ".scoped-area"); + + lookup = lookupCachedUploadUrl("upload://a.jpeg"); + assert.deepEqual(lookup, {}); +}); diff --git a/test/javascripts/lib/user-search-test.js.es6 b/test/javascripts/lib/user-search-test.js.es6 index c93a87564d..79e11587f0 100644 --- a/test/javascripts/lib/user-search-test.js.es6 +++ b/test/javascripts/lib/user-search-test.js.es6 @@ -1,5 +1,6 @@ import userSearch from "discourse/lib/user-search"; import { CANCELLED_STATUS } from "discourse/lib/autocomplete"; +import pretender from "helpers/create-pretender"; QUnit.module("lib:user-search", { beforeEach() { @@ -7,13 +8,11 @@ QUnit.module("lib:user-search", { return [200, { "Content-Type": "application/json" }, object]; }; - // prettier-ignore - server.get("/u/search/users", request => { //eslint-disable-line - + pretender.get("/u/search/users", request => { // special responder for per category search const categoryMatch = request.url.match(/category_id=([0-9]+)/); if (categoryMatch) { - if(categoryMatch[1] === "3"){ + if (categoryMatch[1] === "3") { return response({}); } return response({ @@ -24,11 +23,12 @@ QUnit.module("lib:user-search", { avatar_template: "https://avatars.discourse.org/v3/letter/t/41988e/{size}.png" } - ]}); + ] + }); } - if(request.url.match(/no-results/)){ - return response({users: []}); + if (request.url.match(/no-results/)) { + return response({ users: [] }); } return response({ diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index dd34d8a204..46609895a2 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -89,7 +89,7 @@ QUnit.test("avatarImg", assert => { size: "tiny", title: "evilest trout" }), - "", + "", "it adds a title if supplied" ); diff --git a/test/javascripts/models/post-stream-test.js.es6 b/test/javascripts/models/post-stream-test.js.es6 index 2276014241..f7a587333f 100644 --- a/test/javascripts/models/post-stream-test.js.es6 +++ b/test/javascripts/models/post-stream-test.js.es6 @@ -2,6 +2,7 @@ import Post from "discourse/models/post"; import createStore from "helpers/create-store"; import User from "discourse/models/user"; import { Promise } from "rsvp"; +import pretender from "helpers/create-pretender"; QUnit.module("model:post-stream"); @@ -445,7 +446,7 @@ QUnit.test("storePost", assert => { assert.equal(stored, postWithoutId, "it returns the same post back"); }); -QUnit.test("identity map", assert => { +QUnit.test("identity map", async assert => { const postStream = buildStream(1234); const store = postStream.store; @@ -464,34 +465,29 @@ QUnit.test("identity map", assert => { assert.blank(postStream.findLoadedPost(4), "it can't find uncached posts"); // Find posts by ids uses the identity map - return postStream.findPostsByIds([1, 2, 3]).then(result => { - assert.equal(result.length, 3); - assert.equal(result.objectAt(0), p1); - assert.equal(result.objectAt(1).get("post_number"), 2); - assert.equal(result.objectAt(2), p3); - }); + const result = await postStream.findPostsByIds([1, 2, 3]); + assert.equal(result.length, 3); + assert.equal(result.objectAt(0), p1); + assert.equal(result.objectAt(1).get("post_number"), 2); + assert.equal(result.objectAt(2), p3); }); -QUnit.test("loadIntoIdentityMap with no data", assert => { - return buildStream(1234) - .loadIntoIdentityMap([]) - .then(result => { - assert.equal(result.length, 0, "requesting no posts produces no posts"); - }); +QUnit.test("loadIntoIdentityMap with no data", async assert => { + const result = await buildStream(1234).loadIntoIdentityMap([]); + assert.equal(result.length, 0, "requesting no posts produces no posts"); }); -QUnit.test("loadIntoIdentityMap with post ids", assert => { +QUnit.test("loadIntoIdentityMap with post ids", async assert => { const postStream = buildStream(1234); + await postStream.loadIntoIdentityMap([10]); - return postStream.loadIntoIdentityMap([10]).then(function() { - assert.present( - postStream.findLoadedPost(10), - "it adds the returned post to the store" - ); - }); + assert.present( + postStream.findLoadedPost(10), + "it adds the returned post to the store" + ); }); -QUnit.test("appendMore for megatopic", assert => { +QUnit.test("appendMore for megatopic", async assert => { const postStream = buildStream(1234); const store = createStore(); const post = store.createRecord("post", { id: 1, post_number: 1 }); @@ -501,21 +497,20 @@ QUnit.test("appendMore for megatopic", assert => { posts: [post] }); - return postStream.appendMore().then(() => { - assert.present( - postStream.findLoadedPost(2), - "it adds the returned post to the store" - ); + await postStream.appendMore(); + assert.present( + postStream.findLoadedPost(2), + "it adds the returned post to the store" + ); - assert.equal( - postStream.get("posts").length, - 6, - "it adds the right posts into the stream" - ); - }); + assert.equal( + postStream.get("posts").length, + 6, + "it adds the right posts into the stream" + ); }); -QUnit.test("prependMore for megatopic", assert => { +QUnit.test("prependMore for megatopic", async assert => { const postStream = buildStream(1234); const store = createStore(); const post = store.createRecord("post", { id: 6, post_number: 6 }); @@ -525,18 +520,17 @@ QUnit.test("prependMore for megatopic", assert => { posts: [post] }); - return postStream.prependMore().then(() => { - assert.present( - postStream.findLoadedPost(5), - "it adds the returned post to the store" - ); + await postStream.prependMore(); + assert.present( + postStream.findLoadedPost(5), + "it adds the returned post to the store" + ); - assert.equal( - postStream.get("posts").length, - 6, - "it adds the right posts into the stream" - ); - }); + assert.equal( + postStream.get("posts").length, + 6, + "it adds the right posts into the stream" + ); }); QUnit.test("staging and undoing a new post", assert => { @@ -753,8 +747,7 @@ QUnit.test("triggerRecoveredPost", async assert => { return [200, { "Content-Type": "application/json" }, object]; }; - // prettier-ignore - server.get("/posts/4", () => { // eslint-disable-line no-undef + pretender.get("/posts/4", () => { return response({ id: 4, post_number: 4 }); }); @@ -865,7 +858,7 @@ QUnit.test("triggerNewPostInStream for ignored posts", async assert => { ); }); -QUnit.test("postsWithPlaceholders", assert => { +QUnit.test("postsWithPlaceholders", async assert => { const postStream = buildStream(4964, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); const postsWithPlaceholders = postStream.get("postsWithPlaceholders"); const store = postStream.store; @@ -903,16 +896,15 @@ QUnit.test("postsWithPlaceholders", assert => { assert.ok(postsWithPlaceholders.objectAt(3) !== p4); assert.ok(testProxy.objectAt(3) !== p4); - return promise.then(() => { - assert.equal(postsWithPlaceholders.objectAt(3), p4); - assert.equal( - postsWithPlaceholders.get("length"), - 8, - "have a larger placeholder window when loaded" - ); - assert.equal(testProxy.get("length"), 8); - assert.equal(testProxy.objectAt(3), p4); - }); + await promise; + assert.equal(postsWithPlaceholders.objectAt(3), p4); + assert.equal( + postsWithPlaceholders.get("length"), + 8, + "have a larger placeholder window when loaded" + ); + assert.equal(testProxy.get("length"), 8); + assert.equal(testProxy.objectAt(3), p4); }); QUnit.test("filteredPostsCount", assert => { diff --git a/test/javascripts/models/post-test.js.es6 b/test/javascripts/models/post-test.js.es6 index a96c32bb91..ad7fc393ec 100644 --- a/test/javascripts/models/post-test.js.es6 +++ b/test/javascripts/models/post-test.js.es6 @@ -80,20 +80,20 @@ QUnit.test("destroy by staff", assert => { ); }); -QUnit.test("destroy by non-staff", assert => { - var originalCooked = "this is the original cooked value", - user = User.create({ username: "evil trout" }), - post = buildPost({ user: user, cooked: originalCooked }); +QUnit.test("destroy by non-staff", async assert => { + const originalCooked = "this is the original cooked value"; + const user = User.create({ username: "evil trout" }); + const post = buildPost({ user: user, cooked: originalCooked }); - return post.destroy(user).then(() => { - assert.ok( - !post.get("can_delete"), - "the post can't be deleted again in this session" - ); - assert.ok( - post.get("cooked") !== originalCooked, - "the cooked content changed" - ); - assert.equal(post.get("version"), 2, "the version number increased"); - }); + await post.destroy(user); + + assert.ok( + !post.get("can_delete"), + "the post can't be deleted again in this session" + ); + assert.ok( + post.get("cooked") !== originalCooked, + "the cooked content changed" + ); + assert.equal(post.get("version"), 2, "the version number increased"); }); diff --git a/test/javascripts/models/report-test.js.es6 b/test/javascripts/models/report-test.js.es6 index 1fa622a1d3..821a88eee8 100644 --- a/test/javascripts/models/report-test.js.es6 +++ b/test/javascripts/models/report-test.js.es6 @@ -459,7 +459,7 @@ QUnit.test("computed labels", assert => { const computedUsernameLabel = usernameLabel.compute(row); assert.equal( computedUsernameLabel.formatedValue, - "joffrey" + "joffrey" ); assert.equal(computedUsernameLabel.value, "joffrey"); @@ -542,6 +542,6 @@ QUnit.test("computed labels", assert => { const userLink = computedLabels[0].compute(row).formatedValue; assert.equal( userLink, - "joffrey" + "joffrey" ); }); diff --git a/test/javascripts/models/rest-model-test.js.es6 b/test/javascripts/models/rest-model-test.js.es6 index c5e66c4867..c7c44bba26 100644 --- a/test/javascripts/models/rest-model-test.js.es6 +++ b/test/javascripts/models/rest-model-test.js.es6 @@ -18,43 +18,42 @@ QUnit.test("munging", assert => { assert.equal(g.get("inverse"), 0.6, "it runs `munge` on `create`"); }); -QUnit.test("update", assert => { +QUnit.test("update", async assert => { const store = createStore(); - return store.find("widget", 123).then(function(widget) { - assert.equal(widget.get("name"), "Trout Lure"); - assert.ok(!widget.get("isSaving"), "it is not saving"); + const widget = await store.find("widget", 123); + assert.equal(widget.get("name"), "Trout Lure"); + assert.ok(!widget.get("isSaving"), "it is not saving"); - const promise = widget.update({ name: "new name" }); - assert.ok(widget.get("isSaving"), "it is saving"); + const promise = widget.update({ name: "new name" }); + assert.ok(widget.get("isSaving"), "it is saving"); - promise.then(function(result) { - assert.ok(!widget.get("isSaving"), "it is no longer saving"); - assert.equal(widget.get("name"), "new name"); + const result = await promise; + assert.ok(!widget.get("isSaving"), "it is no longer saving"); + assert.equal(widget.get("name"), "new name"); - assert.ok(result.target, "it has a reference to the record"); - assert.equal(result.target.name, widget.get("name")); - }); - }); + assert.ok(result.target, "it has a reference to the record"); + assert.equal(result.target.name, widget.get("name")); }); -QUnit.test("updating simultaneously", assert => { +QUnit.test("updating simultaneously", async assert => { assert.expect(2); const store = createStore(); - return store.find("widget", 123).then(function(widget) { - const firstPromise = widget.update({ name: "new name" }); - const secondPromise = widget.update({ name: "new name" }); - firstPromise.then(function() { - assert.ok(true, "the first promise succeeeds"); - }); + const widget = await store.find("widget", 123); - secondPromise.catch(function() { - assert.ok(true, "the second promise fails"); - }); + const firstPromise = widget.update({ name: "new name" }); + const secondPromise = widget.update({ name: "new name" }); + + firstPromise.then(function() { + assert.ok(true, "the first promise succeeeds"); + }); + + secondPromise.catch(function() { + assert.ok(true, "the second promise fails"); }); }); -QUnit.test("save new", assert => { +QUnit.test("save new", async assert => { const store = createStore(); const widget = store.createRecord("widget"); @@ -65,16 +64,15 @@ QUnit.test("save new", assert => { const promise = widget.save({ name: "Evil Widget" }); assert.ok(widget.get("isSaving"), "it is not saving"); - return promise.then(function(result) { - assert.ok(!widget.get("isSaving"), "it is no longer saving"); - assert.ok(widget.get("id"), "it has an id"); - assert.ok(widget.get("name"), "Evil Widget"); - assert.ok(widget.get("isCreated"), "it is created"); - assert.ok(!widget.get("isNew"), "it is no longer new"); + const result = await promise; + assert.ok(!widget.get("isSaving"), "it is no longer saving"); + assert.ok(widget.get("id"), "it has an id"); + assert.ok(widget.get("name"), "Evil Widget"); + assert.ok(widget.get("isCreated"), "it is created"); + assert.ok(!widget.get("isNew"), "it is no longer new"); - assert.ok(result.target, "it has a reference to the record"); - assert.equal(result.target.name, widget.get("name")); - }); + assert.ok(result.target, "it has a reference to the record"); + assert.equal(result.target.name, widget.get("name")); }); QUnit.test("creating simultaneously", assert => { diff --git a/test/javascripts/models/result-set-test.js.es6 b/test/javascripts/models/result-set-test.js.es6 index 1f819b317e..f80ae1901c 100644 --- a/test/javascripts/models/result-set-test.js.es6 +++ b/test/javascripts/models/result-set-test.js.es6 @@ -4,50 +4,46 @@ import ResultSet from "discourse/models/result-set"; import createStore from "helpers/create-store"; QUnit.test("defaults", assert => { - const rs = ResultSet.create({ content: [] }); - assert.equal(rs.get("length"), 0); - assert.equal(rs.get("totalRows"), 0); - assert.ok(!rs.get("loadMoreUrl")); - assert.ok(!rs.get("loading")); - assert.ok(!rs.get("loadingMore")); - assert.ok(!rs.get("refreshing")); + const resultSet = ResultSet.create({ content: [] }); + assert.equal(resultSet.get("length"), 0); + assert.equal(resultSet.get("totalRows"), 0); + assert.ok(!resultSet.get("loadMoreUrl")); + assert.ok(!resultSet.get("loading")); + assert.ok(!resultSet.get("loadingMore")); + assert.ok(!resultSet.get("refreshing")); }); -QUnit.test("pagination support", assert => { +QUnit.test("pagination support", async assert => { const store = createStore(); - return store.findAll("widget").then(function(rs) { - assert.equal(rs.get("length"), 2); - assert.equal(rs.get("totalRows"), 4); - assert.ok(rs.get("loadMoreUrl"), "has a url to load more"); - assert.ok(!rs.get("loadingMore"), "it is not loading more"); - assert.ok(rs.get("canLoadMore")); + const resultSet = await store.findAll("widget"); + assert.equal(resultSet.get("length"), 2); + assert.equal(resultSet.get("totalRows"), 4); + assert.ok(resultSet.get("loadMoreUrl"), "has a url to load more"); + assert.ok(!resultSet.get("loadingMore"), "it is not loading more"); + assert.ok(resultSet.get("canLoadMore")); - const promise = rs.loadMore(); + const promise = resultSet.loadMore(); + assert.ok(resultSet.get("loadingMore"), "it is loading more"); - assert.ok(rs.get("loadingMore"), "it is loading more"); - promise.then(function() { - assert.ok(!rs.get("loadingMore"), "it finished loading more"); - assert.equal(rs.get("length"), 4); - assert.ok(!rs.get("loadMoreUrl")); - assert.ok(!rs.get("canLoadMore")); - }); - }); + await promise; + assert.ok(!resultSet.get("loadingMore"), "it finished loading more"); + assert.equal(resultSet.get("length"), 4); + assert.ok(!resultSet.get("loadMoreUrl")); + assert.ok(!resultSet.get("canLoadMore")); }); -QUnit.test("refresh support", assert => { +QUnit.test("refresh support", async assert => { const store = createStore(); - return store.findAll("widget").then(function(rs) { - assert.equal( - rs.get("refreshUrl"), - "/widgets?refresh=true", - "it has the refresh url" - ); + const resultSet = await store.findAll("widget"); + assert.equal( + resultSet.get("refreshUrl"), + "/widgets?refresh=true", + "it has the refresh url" + ); - const promise = rs.refresh(); + const promise = resultSet.refresh(); + assert.ok(resultSet.get("refreshing"), "it is refreshing"); - assert.ok(rs.get("refreshing"), "it is refreshing"); - promise.then(function() { - assert.ok(!rs.get("refreshing"), "it is finished refreshing"); - }); - }); + await promise; + assert.ok(!resultSet.get("refreshing"), "it is finished refreshing"); }); diff --git a/test/javascripts/models/store-test.js.es6 b/test/javascripts/models/store-test.js.es6 index f8d9ba1c03..2e220b152c 100644 --- a/test/javascripts/models/store-test.js.es6 +++ b/test/javascripts/models/store-test.js.es6 @@ -50,77 +50,68 @@ QUnit.test( } ); -QUnit.test("find", assert => { +QUnit.test("find", async assert => { const store = createStore(); - return store.find("widget", 123).then(function(w) { - assert.equal(w.get("name"), "Trout Lure"); - assert.equal(w.get("id"), 123); - assert.ok(!w.get("isNew"), "found records are not new"); - assert.equal(w.get("extras.hello"), "world", "extra attributes are set"); + const widget = await store.find("widget", 123); + assert.equal(widget.get("name"), "Trout Lure"); + assert.equal(widget.get("id"), 123); + assert.ok(!widget.get("isNew"), "found records are not new"); + assert.equal(widget.get("extras.hello"), "world", "extra attributes are set"); - // A second find by id returns the same object - store.find("widget", 123).then(function(w2) { - assert.equal(w, w2); - assert.equal(w.get("extras.hello"), "world", "extra attributes are set"); - }); - }); + // A second find by id returns the same object + const widget2 = await store.find("widget", 123); + assert.equal(widget, widget2); + assert.equal(widget.get("extras.hello"), "world", "extra attributes are set"); }); -QUnit.test("find with object id", assert => { +QUnit.test("find with object id", async assert => { const store = createStore(); - return store.find("widget", { id: 123 }).then(function(w) { - assert.equal(w.get("firstObject.name"), "Trout Lure"); - }); + const widget = await store.find("widget", { id: 123 }); + assert.equal(widget.get("firstObject.name"), "Trout Lure"); }); -QUnit.test("find with query param", assert => { +QUnit.test("find with query param", async assert => { const store = createStore(); - return store.find("widget", { name: "Trout Lure" }).then(function(w) { - assert.equal(w.get("firstObject.id"), 123); - }); + const widget = await store.find("widget", { name: "Trout Lure" }); + assert.equal(widget.get("firstObject.id"), 123); }); -QUnit.test("findStale with no stale results", assert => { +QUnit.test("findStale with no stale results", async assert => { const store = createStore(); const stale = store.findStale("widget", { name: "Trout Lure" }); assert.ok(!stale.hasResults, "there are no stale results"); assert.ok(!stale.results, "results are present"); - return stale.refresh().then(function(w) { - assert.equal( - w.get("firstObject.id"), - 123, - "a `refresh()` method provides results for stale" - ); - }); + const widget = await stale.refresh(); + assert.equal( + widget.get("firstObject.id"), + 123, + "a `refresh()` method provides results for stale" + ); }); -QUnit.test("update", assert => { +QUnit.test("update", async assert => { const store = createStore(); - return store.update("widget", 123, { name: "hello" }).then(function(result) { - assert.ok(result); - }); + const result = await store.update("widget", 123, { name: "hello" }); + assert.ok(result); }); -QUnit.test("update with a multi world name", function(assert) { +QUnit.test("update with a multi world name", async assert => { const store = createStore(); - return store - .update("cool-thing", 123, { name: "hello" }) - .then(function(result) { - assert.ok(result); - assert.equal(result.payload.name, "hello"); - }); + const result = await store.update("cool-thing", 123, { name: "hello" }); + assert.ok(result); + assert.equal(result.payload.name, "hello"); }); -QUnit.test("findAll", assert => { +QUnit.test("findAll", async assert => { const store = createStore(); - return store.findAll("widget").then(function(result) { - assert.equal(result.get("length"), 2); - const w = result.findBy("id", 124); - assert.ok(!w.get("isNew"), "found records are not new"); - assert.equal(w.get("name"), "Evil Repellant"); - }); + const result = await store.findAll("widget"); + assert.equal(result.get("length"), 2); + + const widget = result.findBy("id", 124); + assert.ok(!widget.get("isNew"), "found records are not new"); + assert.equal(widget.get("name"), "Evil Repellant"); }); QUnit.test("destroyRecord", function(assert) { @@ -140,59 +131,57 @@ QUnit.test("destroyRecord when new", function(assert) { }); }); -QUnit.test("find embedded", function(assert) { +QUnit.test("find embedded", async assert => { const store = createStore(); - return store.find("fruit", 1).then(function(f) { - assert.ok(f.get("farmer"), "it has the embedded object"); + const fruit = await store.find("fruit", 1); + assert.ok(fruit.get("farmer"), "it has the embedded object"); - const fruitCols = f.get("colors"); - assert.equal(fruitCols.length, 2); - assert.equal(fruitCols[0].get("id"), 1); - assert.equal(fruitCols[1].get("id"), 2); + const fruitCols = fruit.get("colors"); + assert.equal(fruitCols.length, 2); + assert.equal(fruitCols[0].get("id"), 1); + assert.equal(fruitCols[1].get("id"), 2); - assert.ok(f.get("category"), "categories are found automatically"); - }); + assert.ok(fruit.get("category"), "categories are found automatically"); }); QUnit.test("embedded records can be cleared", async assert => { const store = createStore(); - let f = await store.find("fruit", 4); - f.set("farmer", { dummy: "object" }); - f = await store.find("fruit", 4); - assert.ok(!f.get("farmer")); + let fruit = await store.find("fruit", 4); + fruit.set("farmer", { dummy: "object" }); + + fruit = await store.find("fruit", 4); + assert.ok(!fruit.get("farmer")); }); -QUnit.test("meta types", function(assert) { +QUnit.test("meta types", async assert => { const store = createStore(); - return store.find("barn", 1).then(function(f) { - assert.equal( - f.get("owner.name"), - "Old MacDonald", - "it has the embedded farmer" - ); - }); + const barn = await store.find("barn", 1); + assert.equal( + barn.get("owner.name"), + "Old MacDonald", + "it has the embedded farmer" + ); }); -QUnit.test("findAll embedded", function(assert) { +QUnit.test("findAll embedded", async assert => { const store = createStore(); - return store.findAll("fruit").then(function(fruits) { - assert.equal(fruits.objectAt(0).get("farmer.name"), "Old MacDonald"); - assert.equal( - fruits.objectAt(0).get("farmer"), - fruits.objectAt(1).get("farmer"), - "points at the same object" - ); - assert.equal( - fruits.get("extras.hello"), - "world", - "it can supply extra information" - ); + const fruits = await store.findAll("fruit"); + assert.equal(fruits.objectAt(0).get("farmer.name"), "Old MacDonald"); + assert.equal( + fruits.objectAt(0).get("farmer"), + fruits.objectAt(1).get("farmer"), + "points at the same object" + ); + assert.equal( + fruits.get("extras.hello"), + "world", + "it can supply extra information" + ); - const fruitCols = fruits.objectAt(0).get("colors"); - assert.equal(fruitCols.length, 2); - assert.equal(fruitCols[0].get("id"), 1); - assert.equal(fruitCols[1].get("id"), 2); + const fruitCols = fruits.objectAt(0).get("colors"); + assert.equal(fruitCols.length, 2); + assert.equal(fruitCols[0].get("id"), 1); + assert.equal(fruitCols[1].get("id"), 2); - assert.equal(fruits.objectAt(2).get("farmer.name"), "Luke Skywalker"); - }); + assert.equal(fruits.objectAt(2).get("farmer.name"), "Luke Skywalker"); }); diff --git a/test/javascripts/models/user-badge-test.js.es6 b/test/javascripts/models/user-badge-test.js.es6 index 2a0fdd9d6f..80d2432453 100644 --- a/test/javascripts/models/user-badge-test.js.es6 +++ b/test/javascripts/models/user-badge-test.js.es6 @@ -35,26 +35,23 @@ QUnit.test("createFromJson array", assert => { ); }); -QUnit.test("findByUsername", assert => { - return UserBadge.findByUsername("anne3").then(function(badges) { - assert.ok(Array.isArray(badges), "returns an array"); - }); +QUnit.test("findByUsername", async assert => { + const badges = await UserBadge.findByUsername("anne3"); + assert.ok(Array.isArray(badges), "returns an array"); }); -QUnit.test("findByBadgeId", assert => { - return UserBadge.findByBadgeId(880).then(function(badges) { - assert.ok(Array.isArray(badges), "returns an array"); - }); +QUnit.test("findByBadgeId", async assert => { + const badges = await UserBadge.findByBadgeId(880); + assert.ok(Array.isArray(badges), "returns an array"); }); -QUnit.test("grant", assert => { - return UserBadge.grant(1, "username").then(function(userBadge) { - assert.ok(!Array.isArray(userBadge), "does not return an array"); - }); +QUnit.test("grant", async assert => { + const userBadge = await UserBadge.grant(1, "username"); + assert.ok(!Array.isArray(userBadge), "does not return an array"); }); -QUnit.test("revoke", assert => { +QUnit.test("revoke", async assert => { assert.expect(0); const userBadge = UserBadge.create({ id: 1 }); - return userBadge.revoke(); + await userBadge.revoke(); }); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index f005b678ab..c2c3520a3b 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -1,5 +1,4 @@ /*global document, sinon, QUnit, Logster */ - //= require env //= require jquery.debug //= require jquery.ui.widget @@ -77,7 +76,7 @@ if (window.Logster) { window.Logster = { enabled: false }; } -var pretender = require("helpers/create-pretender", null, null, false), +var createPretender = require("helpers/create-pretender", null, null, false), fixtures = require("fixtures/site-fixtures", null, null, false).default, flushMap = require("discourse/models/store", null, null, false).flushMap, ScrollingDOMMethods = require("discourse/mixins/scrolling", null, null, false) @@ -102,13 +101,32 @@ function resetSite(siteSettings, extras) { } QUnit.testStart(function(ctx) { - server = pretender.default(); + server = createPretender.default; + createPretender.applyDefaultHandlers(server); + server.handlers = [] + + server.prepareBody = function(body) { + if (body && typeof body === "object") { + return JSON.stringify(body); + } + return body; + }; + + server.unhandledRequest = function(verb, path) { + const error = + "Unhandled request in test environment: " + path + " (" + verb + ")"; + window.console.error(error); + throw error; + }; + + server.checkPassthrough = request => + request.requestHeaders["Discourse-Script"]; if (ctx.module.startsWith(acceptanceModulePrefix)) { var helper = { - parsePostData: pretender.parsePostData, - response: pretender.response, - success: pretender.success + parsePostData: createPretender.parsePostData, + response: createPretender.response, + success: createPretender.success }; applyPretender( @@ -153,10 +171,6 @@ QUnit.testDone(function() { $(".modal-backdrop").remove(); flushMap(); - server.shutdown(); - - window.server = null; - // ensures any event not removed is not leaking between tests // most likely in intialisers, other places (controller, component...) // should be fixed in code