From ca71815bb289da346090fea43425300b2250f23b Mon Sep 17 00:00:00 2001 From: Joshua Rosenfeld Date: Fri, 12 Oct 2018 12:12:21 -0400 Subject: [PATCH 001/209] don't run specs on calendar for now --- lib/tasks/plugin.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/plugin.rake b/lib/tasks/plugin.rake index b726e45df4..ec1e275f65 100644 --- a/lib/tasks/plugin.rake +++ b/lib/tasks/plugin.rake @@ -7,6 +7,7 @@ task 'plugin:install_all_official' do 'discourse-nginx-performance-report', 'lazyYT', 'poll', + 'discourse-calendar' ]) map = { From 43a7b08a48b7e783c050f82a7c15970010a784b1 Mon Sep 17 00:00:00 2001 From: Joshua Rosenfeld Date: Fri, 12 Oct 2018 12:20:49 -0400 Subject: [PATCH 002/209] don't run specs on prometheus-alert-receiver for now either --- lib/tasks/plugin.rake | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/tasks/plugin.rake b/lib/tasks/plugin.rake index ec1e275f65..c15f29bc4e 100644 --- a/lib/tasks/plugin.rake +++ b/lib/tasks/plugin.rake @@ -7,7 +7,8 @@ task 'plugin:install_all_official' do 'discourse-nginx-performance-report', 'lazyYT', 'poll', - 'discourse-calendar' + 'discourse-calendar', + 'discourse-prometheus-alert-receiver' ]) map = { From 2178f7768fe7cfebd9ee1ad8aa024e11f1e11bc6 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 12 Oct 2018 12:33:08 -0400 Subject: [PATCH 003/209] FIX: Don't show empty user stats in the card when profile is hidden --- .../components/user-card-contents.hbs | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs index 59c1891fb1..39aed70a28 100644 --- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -116,30 +116,32 @@ {{/if}} {{#if user}} - + {{#if showCheckEmail}} + + {{/if}} + {{plugin-outlet name="user-card-metadata" args=(hash user=user)}} + + {{/unless}} {{/if}} {{#if publicUserFields}} From fd58ca19039d25019c3f628c3857ccd0f34d7b57 Mon Sep 17 00:00:00 2001 From: Joshua Rosenfeld Date: Fri, 12 Oct 2018 15:56:23 -0400 Subject: [PATCH 004/209] remove manually mapping of discourse-logster-rate-limit-checker, repo renamed to match --- lib/tasks/plugin.rake | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/tasks/plugin.rake b/lib/tasks/plugin.rake index c15f29bc4e..1f158cd692 100644 --- a/lib/tasks/plugin.rake +++ b/lib/tasks/plugin.rake @@ -13,8 +13,7 @@ task 'plugin:install_all_official' do map = { 'Canned Replies' => 'https://github.com/discourse/discourse-canned-replies', - 'discourse-logster-rate-limit-checker' => 'https://github.com/discourse/logster-rate-limit-checker', - 'discourse-perspective' => 'https://github.com/discourse/discourse-perspective-api', + 'discourse-perspective' => 'https://github.com/discourse/discourse-perspective-api' } #require 'plugin/metadata' From 6a59187ae8a7f239efc23b450796e5e445eeb6fb Mon Sep 17 00:00:00 2001 From: Joe <33972521+hnb-ku@users.noreply.github.com> Date: Sun, 14 Oct 2018 23:38:07 +0800 Subject: [PATCH 005/209] UX: images should be responsive in embedded comments --- app/assets/stylesheets/embed.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/embed.scss b/app/assets/stylesheets/embed.scss index 75e6ef54a3..92dcbfdbd4 100644 --- a/app/assets/stylesheets/embed.scss +++ b/app/assets/stylesheets/embed.scss @@ -70,6 +70,7 @@ article.post { } img { max-width: 100%; + height: auto; } p { margin: 0 0 1em 0; From 84d4c81a26bb585f67809c6f8af25c2a385de754 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 15 Oct 2018 09:43:31 +0800 Subject: [PATCH 006/209] FEATURE: Support backup uploads/downloads directly to/from S3. This reverts commit 3c59106bac4d79f39981bda3ff9db7786c1a78a0. --- .../controllers/admin-backups-index.js.es6 | 6 + .../javascripts/admin/models/backup.js.es6 | 7 +- .../admin/routes/admin-backups.js.es6 | 9 + .../admin/templates/backups-index.hbs | 9 +- .../components/watched-word-uploader.hbs | 2 +- .../components/backup-uploader.js.es6 | 51 +++++ .../discourse/lib/utilities.js.es6 | 1 + .../discourse/mixins/upload.js.es6 | 53 ++++-- .../templates/components/avatar-uploader.hbs | 2 +- .../templates/components/backup-uploader.hbs | 4 + .../templates/components/csv-uploader.hbs | 2 +- .../templates/components/emoji-uploader.hbs | 2 +- .../templates/components/image-uploader.hbs | 2 +- .../templates/components/images-uploader.hbs | 2 +- .../components/wizard-field-image.hbs | 2 +- .../stylesheets/common/base/upload.scss | 5 + app/assets/stylesheets/wizard.scss | 5 + app/controllers/admin/backups_controller.rb | 100 +++++++--- .../admin/dashboard_next_controller.rb | 8 +- app/jobs/regular/backup_chunks_merger.rb | 19 +- app/jobs/scheduled/schedule_backup.rb | 5 +- app/models/admin_dashboard_data.rb | 2 +- app/models/backup.rb | 92 --------- app/models/backup_file.rb | 25 +++ app/models/backup_location_site_setting.rb | 23 +++ app/serializers/backup_file_serializer.rb | 5 + app/serializers/backup_serializer.rb | 3 - app/services/handle_chunk_upload.rb | 3 +- config/locales/client.en.yml | 4 + config/locales/server.en.yml | 7 +- config/routes.rb | 1 + config/site_settings.yml | 9 +- ...6195601_migrate_s3_backup_site_settings.rb | 33 ++++ lib/backup_restore/backup_restore.rb | 4 +- lib/backup_restore/backup_store.rb | 74 ++++++++ lib/backup_restore/backuper.rb | 48 +++-- lib/backup_restore/local_backup_store.rb | 65 +++++++ lib/backup_restore/restorer.rb | 13 +- lib/backup_restore/s3_backup_store.rb | 95 ++++++++++ lib/disk_space.rb | 6 +- lib/json_error.rb | 6 + lib/s3_helper.rb | 26 +-- lib/site_settings/validations.rb | 13 +- lib/tasks/s3.rake | 2 + script/discourse | 38 ++-- .../backup_restore/local_backup_store_spec.rb | 56 ++++++ .../backup_restore/s3_backup_store_spec.rb | 109 +++++++++++ .../shared_examples_for_backup_store.rb | 176 ++++++++++++++++++ spec/models/admin_dashboard_data_spec.rb | 24 ++- spec/models/backup_spec.rb | 130 ------------- .../requests/admin/backups_controller_spec.rb | 103 +++++----- test/javascripts/lib/utilities-test.js.es6 | 8 + 52 files changed, 1079 insertions(+), 420 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/backup-uploader.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/backup-uploader.hbs delete mode 100644 app/models/backup.rb create mode 100644 app/models/backup_file.rb create mode 100644 app/models/backup_location_site_setting.rb create mode 100644 app/serializers/backup_file_serializer.rb delete mode 100644 app/serializers/backup_serializer.rb create mode 100644 db/migrate/20180916195601_migrate_s3_backup_site_settings.rb create mode 100644 lib/backup_restore/backup_store.rb create mode 100644 lib/backup_restore/local_backup_store.rb create mode 100644 lib/backup_restore/s3_backup_store.rb create mode 100644 spec/lib/backup_restore/local_backup_store_spec.rb create mode 100644 spec/lib/backup_restore/s3_backup_store_spec.rb create mode 100644 spec/lib/backup_restore/shared_examples_for_backup_store.rb delete mode 100644 spec/models/backup_spec.rb diff --git a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 index 49e8d9d441..6448c2565a 100644 --- a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 @@ -1,9 +1,15 @@ import { ajax } from "discourse/lib/ajax"; +import computed from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ adminBackups: Ember.inject.controller(), status: Ember.computed.alias("adminBackups.model"), + @computed + localBackupStorage() { + return this.siteSettings.backup_location === "local"; + }, + uploadLabel: function() { return I18n.t("admin.backups.upload.label"); }.property(), diff --git a/app/assets/javascripts/admin/models/backup.js.es6 b/app/assets/javascripts/admin/models/backup.js.es6 index dd6e8046e1..0f6663065d 100644 --- a/app/assets/javascripts/admin/models/backup.js.es6 +++ b/app/assets/javascripts/admin/models/backup.js.es6 @@ -1,5 +1,4 @@ import { ajax } from "discourse/lib/ajax"; -import PreloadStore from "preload-store"; const Backup = Discourse.Model.extend({ destroy() { @@ -16,9 +15,9 @@ const Backup = Discourse.Model.extend({ Backup.reopenClass({ find() { - return PreloadStore.getAndRemove("backups", () => - ajax("/admin/backups.json") - ).then(backups => backups.map(backup => Backup.create(backup))); + return ajax("/admin/backups.json").then(backups => + backups.map(backup => Backup.create(backup)) + ); }, start(withUploads) { diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6 index a0b9342c61..4514eec205 100644 --- a/app/assets/javascripts/admin/routes/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6 @@ -151,6 +151,15 @@ export default Discourse.Route.extend({ message: message }) ); + }, + + remoteUploadSuccess() { + Backup.find().then(backups => { + this.controllerFor("adminBackupsIndex").set( + "model", + backups.map(backup => Backup.create(backup)) + ); + }); } } }); diff --git a/app/assets/javascripts/admin/templates/backups-index.hbs b/app/assets/javascripts/admin/templates/backups-index.hbs index 3b20c7935e..bcf8dccaa4 100644 --- a/app/assets/javascripts/admin/templates/backups-index.hbs +++ b/app/assets/javascripts/admin/templates/backups-index.hbs @@ -1,5 +1,10 @@
- {{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadLabel title="admin.backups.upload.title"}} + {{#if localBackupStorage}} + {{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadLabel title="admin.backups.upload.title"}} + {{else}} + {{backup-uploader done="remoteUploadSuccess"}} + {{/if}} + {{#if site.isReadOnly}} {{d-button icon="eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}} {{else}} @@ -10,7 +15,7 @@ {{i18n 'admin.backups.columns.filename'}} {{i18n 'admin.backups.columns.size'}} - + {{#each model as |backup|}} diff --git a/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs b/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs index caa74a8615..e69083494f 100644 --- a/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs +++ b/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs @@ -1,7 +1,7 @@
One word per line diff --git a/app/assets/javascripts/discourse/components/backup-uploader.js.es6 b/app/assets/javascripts/discourse/components/backup-uploader.js.es6 new file mode 100644 index 0000000000..3b54695e3d --- /dev/null +++ b/app/assets/javascripts/discourse/components/backup-uploader.js.es6 @@ -0,0 +1,51 @@ +import { ajax } from "discourse/lib/ajax"; +import computed from "ember-addons/ember-computed-decorators"; +import UploadMixin from "discourse/mixins/upload"; + +export default Em.Component.extend(UploadMixin, { + tagName: "span", + + @computed("uploading", "uploadProgress") + uploadButtonText(uploading, progress) { + return uploading + ? I18n.t("admin.backups.upload.uploading_progress", { progress }) + : I18n.t("admin.backups.upload.label"); + }, + + validateUploadedFilesOptions() { + return { skipValidation: true }; + }, + + uploadDone() { + this.sendAction("done"); + }, + + calculateUploadUrl() { + return ""; + }, + + uploadOptions() { + return { + type: "PUT", + dataType: "xml", + autoUpload: false + }; + }, + + _init: function() { + const $upload = this.$(); + + $upload.on("fileuploadadd", (e, data) => { + ajax("/admin/backups/upload_url", { + data: { filename: data.files[0].name } + }).then(result => { + if (!result.success) { + bootbox.alert(result.message); + } else { + data.url = result.url; + data.submit(); + } + }); + }); + }.on("didInsertElement") +}); diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index 9ab42d5780..0ab5c4c933 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -225,6 +225,7 @@ export function validateUploadedFiles(files, opts) { } export function validateUploadedFile(file, opts) { + if (opts.skipValidation) return true; if (!authorizesOneOrMoreExtensions()) return false; opts = opts || {}; diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6 index 6992500306..6ff81b8ec0 100644 --- a/app/assets/javascripts/discourse/mixins/upload.js.es6 +++ b/app/assets/javascripts/discourse/mixins/upload.js.es6 @@ -2,6 +2,7 @@ import { displayErrorForUpload, validateUploadedFiles } from "discourse/lib/utilities"; +import getUrl from "discourse-common/lib/get-url"; export default Em.Mixin.create({ uploading: false, @@ -15,13 +16,25 @@ export default Em.Mixin.create({ return {}; }, + calculateUploadUrl() { + return ( + getUrl(this.getWithDefault("uploadUrl", "/uploads")) + + ".json?client_id=" + + this.messageBus.clientId + + "&authenticity_token=" + + encodeURIComponent(Discourse.Session.currentProp("csrfToken")) + ); + }, + + uploadOptions() { + return {}; + }, + _initialize: function() { - const $upload = this.$(), - csrf = Discourse.Session.currentProp("csrfToken"), - uploadUrl = Discourse.getURL( - this.getWithDefault("uploadUrl", "/uploads") - ), - reset = () => this.setProperties({ uploading: false, uploadProgress: 0 }); + const $upload = this.$(); + const reset = () => + this.setProperties({ uploading: false, uploadProgress: 0 }); + const maxFiles = this.getWithDefault("maxFiles", 10); $upload.on("fileuploaddone", (e, data) => { let upload = data.result; @@ -29,20 +42,21 @@ export default Em.Mixin.create({ reset(); }); - $upload.fileupload({ - url: - uploadUrl + - ".json?client_id=" + - this.messageBus.clientId + - "&authenticity_token=" + - encodeURIComponent(csrf), - dataType: "json", - dropZone: $upload, - pasteZone: $upload - }); + $upload.fileupload( + _.merge( + { + url: this.calculateUploadUrl(), + dataType: "json", + replaceFileInput: false, + dropZone: $upload, + pasteZone: $upload + }, + this.uploadOptions() + ) + ); $upload.on("fileuploaddrop", (e, data) => { - if (data.files.length > 10) { + if (data.files.length > maxFiles) { bootbox.alert(I18n.t("post.errors.too_many_dragged_and_dropped_files")); return false; } else { @@ -56,7 +70,8 @@ export default Em.Mixin.create({ this.validateUploadedFilesOptions() ); const isValid = validateUploadedFiles(data.files, opts); - let form = { type: this.get("type") }; + const type = this.get("type"); + let form = type ? { type } : {}; if (this.get("data")) { form = $.extend(form, this.get("data")); } diff --git a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs index b43cbb0d56..f6b2102c92 100644 --- a/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/avatar-uploader.hbs @@ -1,6 +1,6 @@ {{#if uploading}} {{i18n 'upload_selector.uploading'}} {{uploadProgress}}% diff --git a/app/assets/javascripts/discourse/templates/components/backup-uploader.hbs b/app/assets/javascripts/discourse/templates/components/backup-uploader.hbs new file mode 100644 index 0000000000..affa124962 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/backup-uploader.hbs @@ -0,0 +1,4 @@ + diff --git a/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs b/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs index 859433546c..ad3120827f 100644 --- a/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs @@ -1,6 +1,6 @@ {{#if uploading}} {{i18n 'upload_selector.uploading'}} {{uploadProgress}}% diff --git a/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs b/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs index 85ad4157bb..18ddcaef5a 100644 --- a/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs @@ -2,5 +2,5 @@ diff --git a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs index 5a12cff39b..8eb03a0145 100644 --- a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs @@ -2,7 +2,7 @@
{{#if backgroundStyle}} diff --git a/app/assets/javascripts/discourse/templates/components/images-uploader.hbs b/app/assets/javascripts/discourse/templates/components/images-uploader.hbs index a8a3f7a381..6c53367c14 100644 --- a/app/assets/javascripts/discourse/templates/components/images-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/images-uploader.hbs @@ -1,6 +1,6 @@ {{#if uploading}} {{i18n 'upload_selector.uploading'}} {{uploadProgress}}% diff --git a/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs b/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs index 05f1cb2951..1e8fbc61f7 100644 --- a/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs +++ b/app/assets/javascripts/wizard/templates/components/wizard-field-image.hbs @@ -10,5 +10,5 @@ {{d-icon "picture-o"}} {{/if}} - + diff --git a/app/assets/stylesheets/common/base/upload.scss b/app/assets/stylesheets/common/base/upload.scss index dc2a87aa10..ddaf0d1d4e 100644 --- a/app/assets/stylesheets/common/base/upload.scss +++ b/app/assets/stylesheets/common/base/upload.scss @@ -14,3 +14,8 @@ background-size: contain; } } + +.hidden-upload-field { + visibility: hidden; + position: absolute; +} diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss index 7b4526453a..a35b7830b1 100644 --- a/app/assets/stylesheets/wizard.scss +++ b/app/assets/stylesheets/wizard.scss @@ -328,6 +328,11 @@ body.wizard { } } + .wizard-hidden-upload-field { + visibility: hidden; + position: absolute; + } + .wizard-step-footer { display: flex; flex-direction: row; diff --git a/app/controllers/admin/backups_controller.rb b/app/controllers/admin/backups_controller.rb index ef8950fd5b..2b4ffd3512 100644 --- a/app/controllers/admin/backups_controller.rb +++ b/app/controllers/admin/backups_controller.rb @@ -1,20 +1,26 @@ require "backup_restore/backup_restore" +require "backup_restore/backup_store" class Admin::BackupsController < Admin::AdminController - before_action :ensure_backups_enabled skip_before_action :check_xhr, only: [:index, :show, :logs, :check_backup_chunk, :upload_backup_chunk] def index respond_to do |format| format.html do - store_preloaded("backups", MultiJson.dump(serialize_data(Backup.all, BackupSerializer))) store_preloaded("operations_status", MultiJson.dump(BackupRestore.operations_status)) store_preloaded("logs", MultiJson.dump(BackupRestore.logs)) render "default/empty" end + format.json do - render_serialized(Backup.all, BackupSerializer) + store = BackupRestore::BackupStore.create + + begin + render_serialized(store.files, BackupFileSerializer) + rescue BackupRestore::BackupStore::StorageError => e + render_json_error(e) + end end end end @@ -46,8 +52,11 @@ class Admin::BackupsController < Admin::AdminController end def email - if backup = Backup[params.fetch(:id)] - Jobs.enqueue(:download_backup_email, + store = BackupRestore::BackupStore.create + + if store.file(params.fetch(:id)).present? + Jobs.enqueue( + :download_backup_email, user_id: current_user.id, backup_file_path: url_for(controller: 'backups', action: 'show') ) @@ -59,28 +68,34 @@ class Admin::BackupsController < Admin::AdminController end def show - if !EmailBackupToken.compare(current_user.id, params.fetch(:token)) @error = I18n.t('download_backup_mailer.no_token') + return render template: 'admin/backups/show.html.erb', layout: 'no_ember', status: 422 end - if !@error && backup = Backup[params.fetch(:id)] + + store = BackupRestore::BackupStore.create + + if backup = store.file(params.fetch(:id), include_download_source: true) EmailBackupToken.del(current_user.id) StaffActionLogger.new(current_user).log_backup_download(backup) - headers['Content-Length'] = File.size(backup.path).to_s - send_file backup.path - else - if @error - render template: 'admin/backups/show.html.erb', layout: 'no_ember', status: 422 + + if store.remote? + redirect_to backup.source else - render body: nil, status: 404 + headers['Content-Length'] = File.size(backup.source).to_s + send_file backup.source end + else + render body: nil, status: 404 end end def destroy - if backup = Backup[params.fetch(:id)] + store = BackupRestore::BackupStore.create + + if backup = store.file(params.fetch(:id)) StaffActionLogger.new(current_user).log_backup_destroy(backup) - backup.remove + store.delete_file(backup.filename) render body: nil else render body: nil, status: 404 @@ -131,13 +146,13 @@ class Admin::BackupsController < Admin::AdminController end def check_backup_chunk - identifier = params.fetch(:resumableIdentifier) - filename = params.fetch(:resumableFilename) - chunk_number = params.fetch(:resumableChunkNumber) + identifier = params.fetch(:resumableIdentifier) + filename = params.fetch(:resumableFilename) + chunk_number = params.fetch(:resumableChunkNumber) current_chunk_size = params.fetch(:resumableCurrentChunkSize).to_i # path to chunk file - chunk = Backup.chunk_path(identifier, filename, chunk_number) + chunk = BackupRestore::LocalBackupStore.chunk_path(identifier, filename, chunk_number) # check chunk upload status status = HandleChunkUpload.check_chunk(chunk, current_chunk_size: current_chunk_size) @@ -145,21 +160,21 @@ class Admin::BackupsController < Admin::AdminController end def upload_backup_chunk - filename = params.fetch(:resumableFilename) + filename = params.fetch(:resumableFilename) total_size = params.fetch(:resumableTotalSize).to_i - return render status: 415, plain: I18n.t("backup.backup_file_should_be_tar_gz") unless /\.(tar\.gz|t?gz)$/i =~ filename - return render status: 415, plain: I18n.t("backup.not_enough_space_on_disk") unless has_enough_space_on_disk?(total_size) - return render status: 415, plain: I18n.t("backup.invalid_filename") unless !!(/^[a-zA-Z0-9\._\-]+$/ =~ filename) + return render status: 415, plain: I18n.t("backup.backup_file_should_be_tar_gz") unless valid_extension?(filename) + return render status: 415, plain: I18n.t("backup.not_enough_space_on_disk") unless has_enough_space_on_disk?(total_size) + return render status: 415, plain: I18n.t("backup.invalid_filename") unless valid_filename?(filename) - file = params.fetch(:file) - identifier = params.fetch(:resumableIdentifier) - chunk_number = params.fetch(:resumableChunkNumber).to_i - chunk_size = params.fetch(:resumableChunkSize).to_i + file = params.fetch(:file) + identifier = params.fetch(:resumableIdentifier) + chunk_number = params.fetch(:resumableChunkNumber).to_i + chunk_size = params.fetch(:resumableChunkSize).to_i current_chunk_size = params.fetch(:resumableCurrentChunkSize).to_i # path to chunk file - chunk = Backup.chunk_path(identifier, filename, chunk_number) + chunk = BackupRestore::LocalBackupStore.chunk_path(identifier, filename, chunk_number) # upload chunk HandleChunkUpload.upload_chunk(chunk, file: file) @@ -173,6 +188,24 @@ class Admin::BackupsController < Admin::AdminController render body: nil end + def create_upload_url + params.require(:filename) + filename = params.fetch(:filename) + + return render_error("backup.backup_file_should_be_tar_gz") unless valid_extension?(filename) + return render_error("backup.invalid_filename") unless valid_filename?(filename) + + store = BackupRestore::BackupStore.create + + begin + upload_url = store.generate_upload_url(filename) + rescue BackupRestore::BackupStore::BackupFileExists + return render_error("backup.file_exists") + end + + render json: success_json.merge(url: upload_url) + end + private def has_enough_space_on_disk?(size) @@ -183,4 +216,15 @@ class Admin::BackupsController < Admin::AdminController raise Discourse::InvalidAccess.new unless SiteSetting.enable_backups? end + def valid_extension?(filename) + /\.(tar\.gz|t?gz)$/i =~ filename + end + + def valid_filename?(filename) + !!(/^[a-zA-Z0-9\._\-]+$/ =~ filename) + end + + def render_error(message_key) + render json: failed_json.merge(message: I18n.t(message_key)) + end end diff --git a/app/controllers/admin/dashboard_next_controller.rb b/app/controllers/admin/dashboard_next_controller.rb index aaeb001a15..8da41c075d 100644 --- a/app/controllers/admin/dashboard_next_controller.rb +++ b/app/controllers/admin/dashboard_next_controller.rb @@ -27,8 +27,12 @@ class Admin::DashboardNextController < Admin::AdminController private def last_backup_taken_at - if last_backup = Backup.all.first - File.ctime(last_backup.path).utc + store = BackupRestore::BackupStore.create + + begin + store.latest_file&.last_modified + rescue BackupRestore::BackupStore::StorageError + nil end end end diff --git a/app/jobs/regular/backup_chunks_merger.rb b/app/jobs/regular/backup_chunks_merger.rb index 23cda4d264..0fe070b801 100644 --- a/app/jobs/regular/backup_chunks_merger.rb +++ b/app/jobs/regular/backup_chunks_merger.rb @@ -1,3 +1,6 @@ +require_dependency "backup_restore/local_backup_store" +require_dependency "backup_restore/backup_store" + module Jobs class BackupChunksMerger < Jobs::Base @@ -12,16 +15,24 @@ module Jobs raise Discourse::InvalidParameters.new(:identifier) if identifier.blank? raise Discourse::InvalidParameters.new(:chunks) if chunks <= 0 - backup_path = "#{Backup.base_directory}/#{filename}" + backup_path = "#{BackupRestore::LocalBackupStore.base_directory}/#{filename}" tmp_backup_path = "#{backup_path}.tmp" # path to tmp directory - tmp_directory = File.dirname(Backup.chunk_path(identifier, filename, 0)) + tmp_directory = File.dirname(BackupRestore::LocalBackupStore.chunk_path(identifier, filename, 0)) # merge all chunks - HandleChunkUpload.merge_chunks(chunks, upload_path: backup_path, tmp_upload_path: tmp_backup_path, model: Backup, identifier: identifier, filename: filename, tmp_directory: tmp_directory) + HandleChunkUpload.merge_chunks( + chunks, + upload_path: backup_path, + tmp_upload_path: tmp_backup_path, + identifier: identifier, + filename: filename, + tmp_directory: tmp_directory + ) # push an updated list to the clients - data = ActiveModel::ArraySerializer.new(Backup.all, each_serializer: BackupSerializer).as_json + store = BackupRestore::BackupStore.create + data = ActiveModel::ArraySerializer.new(store.files, each_serializer: BackupFileSerializer).as_json MessageBus.publish("/admin/backups", data, user_ids: User.staff.pluck(:id)) end diff --git a/app/jobs/scheduled/schedule_backup.rb b/app/jobs/scheduled/schedule_backup.rb index feca26cb65..b2e9129811 100644 --- a/app/jobs/scheduled/schedule_backup.rb +++ b/app/jobs/scheduled/schedule_backup.rb @@ -7,8 +7,9 @@ module Jobs def execute(args) return unless SiteSetting.enable_backups? && SiteSetting.automatic_backups_enabled? - if latest_backup = Backup.all[0] - date = File.ctime(latest_backup.path).getutc.to_date + store = BackupRestore::BackupStore.create + if latest_backup = store.latest_file + date = latest_backup.last_modified.to_date return if (date + SiteSetting.backup_frequency.days) > Time.now.utc.to_date end diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 8295934a72..33d2c87858 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -209,7 +209,7 @@ class AdminDashboardData bad_keys = (SiteSetting.s3_access_key_id.blank? || SiteSetting.s3_secret_access_key.blank?) && !SiteSetting.s3_use_iam_profile return I18n.t('dashboard.s3_config_warning') if SiteSetting.enable_s3_uploads && (bad_keys || SiteSetting.s3_upload_bucket.blank?) - return I18n.t('dashboard.s3_backup_config_warning') if SiteSetting.enable_s3_backups && (bad_keys || SiteSetting.s3_backup_bucket.blank?) + return I18n.t('dashboard.s3_backup_config_warning') if SiteSetting.backup_location == BackupLocationSiteSetting::S3 && (bad_keys || SiteSetting.s3_backup_bucket.blank?) end nil end diff --git a/app/models/backup.rb b/app/models/backup.rb deleted file mode 100644 index 8dc6ae2e73..0000000000 --- a/app/models/backup.rb +++ /dev/null @@ -1,92 +0,0 @@ -require 'disk_space' - -class Backup - include ActiveModel::SerializerSupport - - attr_reader :filename - attr_accessor :size, :path, :link - - def initialize(filename) - @filename = filename - end - - def self.all - Dir.glob(File.join(Backup.base_directory, "*.{gz,tgz}")) - .sort_by { |file| File.mtime(file) } - .reverse - .map { |backup| Backup.create_from_filename(File.basename(backup)) } - end - - def self.[](filename) - path = File.join(Backup.base_directory, filename) - if File.exists?(path) - Backup.create_from_filename(filename) - else - nil - end - end - - def remove - File.delete(@path) if File.exists?(path) - after_remove_hook - end - - def after_create_hook - upload_to_s3 if SiteSetting.enable_s3_backups? - DiscourseEvent.trigger(:backup_created) - end - - def after_remove_hook - remove_from_s3 if SiteSetting.enable_s3_backups? && !SiteSetting.s3_disable_cleanup? - DiskSpace.reset_cached_stats unless SiteSetting.enable_s3_backups? - end - - def s3_bucket - return @s3_bucket if @s3_bucket - raise Discourse::SiteSettingMissing.new("s3_backup_bucket") if SiteSetting.s3_backup_bucket.blank? - @s3_bucket = SiteSetting.s3_backup_bucket.downcase - end - - def s3 - require "s3_helper" unless defined? S3Helper - @s3_helper ||= S3Helper.new(s3_bucket, '', S3Helper.s3_options(SiteSetting)) - end - - def upload_to_s3 - return unless s3 - File.open(@path) do |file| - s3.upload(file, @filename) - end - end - - def remove_from_s3 - return unless s3 - s3.remove(@filename) - end - - def self.base_directory - base_directory = File.join(Rails.root, "public", "backups", RailsMultisite::ConnectionManagement.current_db) - FileUtils.mkdir_p(base_directory) unless Dir.exists?(base_directory) - base_directory - end - - def self.chunk_path(identifier, filename, chunk_number) - File.join(Backup.base_directory, "tmp", identifier, "#{filename}.part#{chunk_number}") - end - - def self.create_from_filename(filename) - Backup.new(filename).tap do |b| - b.path = File.join(Backup.base_directory, b.filename) - b.link = UrlHelper.schemaless "#{Discourse.base_url}/admin/backups/#{b.filename}" - b.size = File.size(b.path) - end - end - - def self.remove_old - return if Rails.env.development? - all_backups = Backup.all - return if all_backups.size <= SiteSetting.maximum_backups - all_backups[SiteSetting.maximum_backups..-1].each(&:remove) - end - -end diff --git a/app/models/backup_file.rb b/app/models/backup_file.rb new file mode 100644 index 0000000000..86482f687d --- /dev/null +++ b/app/models/backup_file.rb @@ -0,0 +1,25 @@ +class BackupFile + include ActiveModel::SerializerSupport + + attr_reader :filename, + :size, + :last_modified, + :source + + def initialize(filename:, size:, last_modified:, source: nil) + @filename = filename + @size = size + @last_modified = last_modified + @source = source + end + + def ==(other) + attributes == other.attributes + end + + protected + + def attributes + [@filename, @size, @last_modified, @source] + end +end diff --git a/app/models/backup_location_site_setting.rb b/app/models/backup_location_site_setting.rb new file mode 100644 index 0000000000..aef3c89fec --- /dev/null +++ b/app/models/backup_location_site_setting.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_dependency 'enum_site_setting'; + +class BackupLocationSiteSetting < EnumSiteSetting + LOCAL ||= "local" + S3 ||= "s3" + + def self.valid_value?(val) + values.any? { |v| v[:value] == val } + end + + def self.values + @values ||= [ + { name: "admin.backups.location.local", value: LOCAL }, + { name: "admin.backups.location.s3", value: S3 } + ] + end + + def self.translate_names? + true + end +end diff --git a/app/serializers/backup_file_serializer.rb b/app/serializers/backup_file_serializer.rb new file mode 100644 index 0000000000..f4cada5708 --- /dev/null +++ b/app/serializers/backup_file_serializer.rb @@ -0,0 +1,5 @@ +class BackupFileSerializer < ApplicationSerializer + attributes :filename, + :size, + :last_modified +end diff --git a/app/serializers/backup_serializer.rb b/app/serializers/backup_serializer.rb deleted file mode 100644 index 3d4356b09f..0000000000 --- a/app/serializers/backup_serializer.rb +++ /dev/null @@ -1,3 +0,0 @@ -class BackupSerializer < ApplicationSerializer - attributes :filename, :size, :link -end diff --git a/app/services/handle_chunk_upload.rb b/app/services/handle_chunk_upload.rb index 75362e54e5..93aee6122f 100644 --- a/app/services/handle_chunk_upload.rb +++ b/app/services/handle_chunk_upload.rb @@ -36,7 +36,6 @@ class HandleChunkUpload def merge_chunks upload_path = @params[:upload_path] tmp_upload_path = @params[:tmp_upload_path] - model = @params[:model] identifier = @params[:identifier] filename = @params[:filename] tmp_directory = @params[:tmp_directory] @@ -52,7 +51,7 @@ class HandleChunkUpload File.open(tmp_upload_path, "a") do |file| (1..@chunk).each do |chunk_number| # path to chunk - chunk_path = model.chunk_path(identifier, filename, chunk_number) + chunk_path = BackupRestore::LocalBackupStore.chunk_path(identifier, filename, chunk_number) # add chunk to file file << File.open(chunk_path).read end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e15b2eb44a..157b35bb0b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3134,6 +3134,7 @@ en: label: "Upload" title: "Upload a backup to this instance" uploading: "Uploading..." + uploading_progress: "Uploading... {{progress}}%" success: "'{{filename}}' has successfully been uploaded. The file is now being processed and will take up to a minute to show up in the list." error: "There has been an error while uploading '{{filename}}': {{message}}" operations: @@ -3164,6 +3165,9 @@ en: label: "Rollback" title: "Rollback the database to previous working state" confirm: "Are you sure you want to rollback the database to the previous working state?" + location: + local: "Local" + s3: "Amazon S3" export_csv: success: "Export initiated, you will be notified via message when the process is complete." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index f450f85e75..6a5e45265a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -166,6 +166,7 @@ en: other: 'You specified the invalid choices %{name}' default_categories_already_selected: "You cannot select a category used in another list." s3_upload_bucket_is_required: "You cannot enable uploads to S3 unless you've provided the 's3_upload_bucket'." + s3_backup_requires_s3_settings: "You cannot use S3 as backup location unless you've provided the '%{setting_name}'." conflicting_google_user_id: 'The Google Account ID for this account has changed; staff intervention is required for security reasons. Please contact staff and point them to
https://meta.discourse.org/t/76575' activemodel: @@ -194,6 +195,10 @@ en: backup_file_should_be_tar_gz: "The backup file should be a .tar.gz archive." not_enough_space_on_disk: "There is not enough space on disk to upload this backup." invalid_filename: "The backup filename contains invalid characters. Valid characters are a-z 0-9 . - _." + file_exists: "The file you are trying to upload already exists." + location: + local: "Local" + s3: "Amazon S3" invalid_params: "You supplied invalid parameters to the request: %{message}" not_logged_in: "You need to be logged in to do that." @@ -1359,7 +1364,6 @@ en: maximum_backups: "The maximum amount of backups to keep on disk. Older backups are automatically deleted" automatic_backups_enabled: "Run automatic backups as defined in backup frequency" backup_frequency: "The number of days between backups." - enable_s3_backups: "Upload backups to S3 when complete. IMPORTANT: requires valid S3 credentials entered in Files settings." s3_backup_bucket: "The remote bucket to hold backups. WARNING: Make sure it is a private bucket." s3_endpoint: "The endpoint can be modified to backup to an S3 compatible service like DigitalOcean Spaces or Minio. WARNING: Use default if using AWS S3" s3_force_path_style: "Enforce path-style addressing for your custom endpoint. IMPORTANT: Required for using Minio uploads and backups." @@ -1367,6 +1371,7 @@ en: s3_disable_cleanup: "Disable the removal of backups from S3 when removed locally." backup_time_of_day: "Time of day UTC when the backup should occur." backup_with_uploads: "Include uploads in scheduled backups. Disabling this will only backup the database." + backup_location: "Location where backups are stored. IMPORTANT: S3 requires valid S3 credentials entered in Files settings." active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds" verbose_localization: "Show extended localization tips in the UI" diff --git a/config/routes.rb b/config/routes.rb index 11a6106c46..1d6caab2b0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -280,6 +280,7 @@ Discourse::Application.routes.draw do put "readonly" => "backups#readonly" get "upload" => "backups#check_backup_chunk" post "upload" => "backups#upload_backup_chunk" + get "upload_url" => "backups#create_upload_url" end end diff --git a/config/site_settings.yml b/config/site_settings.yml index 725f736d5c..1a882a9c1e 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1405,6 +1405,12 @@ backups: allow_restore: default: false shadowed_by_global: true + backup_location: + default: 'local' + type: enum + enum: 'BackupLocationSiteSetting' + shadowed_by_global: true + client: true maximum_backups: client: true default: 5 @@ -1417,9 +1423,6 @@ backups: max: 30 default: 7 shadowed_by_global: true - enable_s3_backups: - default: false - shadowed_by_global: true s3_backup_bucket: default: '' regex: '^[a-z0-9\-\/]+$' # can't use '.' when using HTTPS diff --git a/db/migrate/20180916195601_migrate_s3_backup_site_settings.rb b/db/migrate/20180916195601_migrate_s3_backup_site_settings.rb new file mode 100644 index 0000000000..064a404627 --- /dev/null +++ b/db/migrate/20180916195601_migrate_s3_backup_site_settings.rb @@ -0,0 +1,33 @@ +class MigrateS3BackupSiteSettings < ActiveRecord::Migration[5.2] + def up + execute <<~SQL + UPDATE site_settings + SET name = 'backup_location', + data_type = 7, + value = 's3' + WHERE name = 'enable_s3_backups' AND value = 't'; + SQL + + execute <<~SQL + DELETE + FROM site_settings + WHERE name = 'enable_s3_backups'; + SQL + end + + def down + execute <<~SQL + UPDATE site_settings + SET name = 'enable_s3_backups', + data_type = 5, + value = 't' + WHERE name = 'backup_location' AND value = 's3'; + SQL + + execute <<~SQL + DELETE + FROM site_settings + WHERE name = 'backup_location'; + SQL + end +end diff --git a/lib/backup_restore/backup_restore.rb b/lib/backup_restore/backup_restore.rb index 0591b31fe1..8e69ed997a 100644 --- a/lib/backup_restore/backup_restore.rb +++ b/lib/backup_restore/backup_restore.rb @@ -1,5 +1,5 @@ -require "backup_restore/backuper" -require "backup_restore/restorer" +require_dependency "backup_restore/backuper" +require_dependency "backup_restore/restorer" module BackupRestore diff --git a/lib/backup_restore/backup_store.rb b/lib/backup_restore/backup_store.rb new file mode 100644 index 0000000000..3ca9d70529 --- /dev/null +++ b/lib/backup_restore/backup_store.rb @@ -0,0 +1,74 @@ +module BackupRestore + # @abstract + class BackupStore + class BackupFileExists < RuntimeError; end + class StorageError < RuntimeError; end + + # @return [BackupStore] + def self.create(opts = {}) + case SiteSetting.backup_location + when BackupLocationSiteSetting::LOCAL + require_dependency "backup_restore/local_backup_store" + BackupRestore::LocalBackupStore.new(opts) + when BackupLocationSiteSetting::S3 + require_dependency "backup_restore/s3_backup_store" + BackupRestore::S3BackupStore.new(opts) + end + end + + # @return [Array] + def files + unsorted_files.sort_by { |file| -file.last_modified.to_i } + end + + # @return [BackupFile] + def latest_file + files.first + end + + def delete_old + return unless cleanup_allowed? + return if (backup_files = files).size <= SiteSetting.maximum_backups + + backup_files[SiteSetting.maximum_backups..-1].each do |file| + delete_file(file.filename) + end + end + + def remote? + fail NotImplementedError + end + + # @return [BackupFile] + def file(filename, include_download_source: false) + fail NotImplementedError + end + + def delete_file(filename) + fail NotImplementedError + end + + def download_file(filename, destination, failure_message = nil) + fail NotImplementedError + end + + def upload_file(filename, source_path, content_type) + fail NotImplementedError + end + + def generate_upload_url(filename) + fail NotImplementedError + end + + private + + # @return [Array] + def unsorted_files + fail NotImplementedError + end + + def cleanup_allowed? + true + end + end +end diff --git a/lib/backup_restore/backuper.rb b/lib/backup_restore/backuper.rb index e7fdc04ed6..f03d3dd50f 100644 --- a/lib/backup_restore/backuper.rb +++ b/lib/backup_restore/backuper.rb @@ -1,4 +1,5 @@ -require 'disk_space' +require "disk_space" +require "mini_mime" module BackupRestore @@ -10,6 +11,7 @@ module BackupRestore @client_id = opts[:client_id] @publish_to_message_bus = opts[:publish_to_message_bus] || false @with_uploads = opts[:with_uploads].nil? ? true : opts[:with_uploads] + @filename_override = opts[:filename] ensure_no_operation_is_running ensure_we_have_a_user @@ -42,6 +44,7 @@ module BackupRestore log "Finalizing backup..." @with_uploads ? create_archive : move_dump_backup + upload_archive after_create_hook rescue SystemExit @@ -52,9 +55,9 @@ module BackupRestore @success = false else @success = true - File.join(@archive_directory, @backup_filename) + @backup_filename ensure - remove_old + delete_old clean_up notify_user log "Finished!" @@ -75,12 +78,14 @@ module BackupRestore def initialize_state @success = false + @store = BackupRestore::BackupStore.create @current_db = RailsMultisite::ConnectionManagement.current_db @timestamp = Time.now.strftime("%Y-%m-%d-%H%M%S") @tmp_directory = File.join(Rails.root, "tmp", "backups", @current_db, @timestamp) @dump_filename = File.join(@tmp_directory, BackupRestore::DUMP_FILE) - @archive_directory = File.join(Rails.root, "public", "backups", @current_db) - @archive_basename = File.join(@archive_directory, "#{SiteSetting.title.parameterize}-#{@timestamp}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}") + @archive_directory = BackupRestore::LocalBackupStore.base_directory(@current_db) + filename = @filename_override || "#{SiteSetting.title.parameterize}-#{@timestamp}" + @archive_basename = File.join(@archive_directory, "#{filename}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}") @backup_filename = if @with_uploads @@ -195,8 +200,10 @@ module BackupRestore def move_dump_backup log "Finalizing database dump file: #{@backup_filename}" + archive_filename = File.join(@archive_directory, @backup_filename) + Discourse::Utils.execute_command( - 'mv', @dump_filename, File.join(@archive_directory, @backup_filename), + 'mv', @dump_filename, archive_filename, failure_message: "Failed to move database dump file." ) @@ -243,17 +250,30 @@ module BackupRestore Discourse::Utils.execute_command('gzip', '-5', tar_filename, failure_message: "Failed to gzip archive.") end - def after_create_hook - log "Executing the after_create_hook for the backup..." - backup = Backup.create_from_filename(@backup_filename) - backup.after_create_hook + def upload_archive + return unless @store.remote? + + log "Uploading archive..." + content_type = MiniMime.lookup_by_filename(@backup_filename).content_type + archive_path = File.join(@archive_directory, @backup_filename) + @store.upload_file(@backup_filename, archive_path, content_type) + ensure + log "Removing archive from local storage..." + FileUtils.remove_file(archive_path, force: true) end - def remove_old - log "Removing old backups..." - Backup.remove_old + def after_create_hook + log "Executing the after_create_hook for the backup..." + DiscourseEvent.trigger(:backup_created) + end + + def delete_old + return if Rails.env.development? + + log "Deleting old backups..." + @store.delete_old rescue => ex - log "Something went wrong while removing old backups.", ex + log "Something went wrong while deleting old backups.", ex end def notify_user diff --git a/lib/backup_restore/local_backup_store.rb b/lib/backup_restore/local_backup_store.rb new file mode 100644 index 0000000000..8b0148ee3c --- /dev/null +++ b/lib/backup_restore/local_backup_store.rb @@ -0,0 +1,65 @@ +require_dependency "backup_restore/backup_store" +require_dependency "disk_space" + +module BackupRestore + class LocalBackupStore < BackupStore + def self.base_directory(current_db = nil) + current_db ||= RailsMultisite::ConnectionManagement.current_db + base_directory = File.join(Rails.root, "public", "backups", current_db) + FileUtils.mkdir_p(base_directory) unless Dir.exists?(base_directory) + base_directory + end + + def self.chunk_path(identifier, filename, chunk_number) + File.join(LocalBackupStore.base_directory, "tmp", identifier, "#{filename}.part#{chunk_number}") + end + + def initialize(opts = {}) + @base_directory = opts[:base_directory] || LocalBackupStore.base_directory + end + + def remote? + false + end + + def file(filename, include_download_source: false) + path = path_from_filename(filename) + create_file_from_path(path, include_download_source) if File.exists?(path) + end + + def delete_file(filename) + path = path_from_filename(filename) + + if File.exists?(path) + FileUtils.remove_file(path, force: true) + DiskSpace.reset_cached_stats + end + end + + def download_file(filename, destination, failure_message = "") + path = path_from_filename(filename) + Discourse::Utils.execute_command('cp', path, destination, failure_message: failure_message) + end + + private + + def unsorted_files + files = Dir.glob(File.join(@base_directory, "*.{gz,tgz}")) + files.map! { |filename| create_file_from_path(filename) } + files + end + + def path_from_filename(filename) + File.join(@base_directory, filename) + end + + def create_file_from_path(path, include_download_source = false) + BackupFile.new( + filename: File.basename(path), + size: File.size(path), + last_modified: File.mtime(path).utc, + source: include_download_source ? path : nil + ) + end + end +end diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index e8a98f0581..62cfd24b67 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -133,12 +133,12 @@ module BackupRestore def initialize_state @success = false + @store = BackupRestore::BackupStore.create @db_was_changed = false @current_db = RailsMultisite::ConnectionManagement.current_db @current_version = BackupRestore.current_version @timestamp = Time.now.strftime("%Y-%m-%d-%H%M%S") @tmp_directory = File.join(Rails.root, "tmp", "restores", @current_db, @timestamp) - @source_filename = File.join(Backup.base_directory, @filename) @archive_filename = File.join(@tmp_directory, @filename) @tar_filename = @archive_filename[0...-3] @meta_filename = File.join(@tmp_directory, BackupRestore::METADATA_FILE) @@ -195,8 +195,15 @@ module BackupRestore end def copy_archive_to_tmp_directory - log "Copying archive to tmp directory..." - Discourse::Utils.execute_command('cp', @source_filename, @archive_filename, failure_message: "Failed to copy archive to tmp directory.") + if @store.remote? + log "Downloading archive to tmp directory..." + failure_message = "Failed to download archive to tmp directory." + else + log "Copying archive to tmp directory..." + failure_message = "Failed to copy archive to tmp directory." + end + + @store.download_file(@filename, @archive_filename, failure_message) end def unzip_archive diff --git a/lib/backup_restore/s3_backup_store.rb b/lib/backup_restore/s3_backup_store.rb new file mode 100644 index 0000000000..cbbc916df0 --- /dev/null +++ b/lib/backup_restore/s3_backup_store.rb @@ -0,0 +1,95 @@ +require_dependency "backup_restore/backup_store" +require_dependency "s3_helper" + +module BackupRestore + class S3BackupStore < BackupStore + DOWNLOAD_URL_EXPIRES_AFTER_SECONDS ||= 15 + UPLOAD_URL_EXPIRES_AFTER_SECONDS ||= 21_600 # 6 hours + + def initialize(opts = {}) + s3_options = S3Helper.s3_options(SiteSetting) + s3_options.merge!(opts[:s3_options]) if opts[:s3_options] + @s3_helper = S3Helper.new(SiteSetting.s3_backup_bucket, '', s3_options) + end + + def remote? + true + end + + def file(filename, include_download_source: false) + obj = @s3_helper.object(filename) + create_file_from_object(obj, include_download_source) if obj.exists? + end + + def delete_file(filename) + obj = @s3_helper.object(filename) + obj.delete if obj.exists? + end + + def download_file(filename, destination_path, failure_message = nil) + unless @s3_helper.object(filename).download_file(destination_path) + raise failure_message&.to_s || "Failed to download file" + end + end + + def upload_file(filename, source_path, content_type) + obj = @s3_helper.object(filename) + raise BackupFileExists.new if obj.exists? + + obj.upload_file(source_path, content_type: content_type) + end + + def generate_upload_url(filename) + obj = @s3_helper.object(filename) + raise BackupFileExists.new if obj.exists? + + presigned_url(obj, :put, UPLOAD_URL_EXPIRES_AFTER_SECONDS) + end + + private + + def unsorted_files + objects = [] + + @s3_helper.list.each do |obj| + if obj.key.match?(/\.t?gz$/i) + objects << create_file_from_object(obj) + end + end + + objects + rescue Aws::Errors::ServiceError => e + Rails.logger.warn("Failed to list backups from S3: #{e.message.presence || e.class.name}") + raise StorageError + end + + def create_file_from_object(obj, include_download_source = false) + BackupFile.new( + filename: File.basename(obj.key), + size: obj.size, + last_modified: obj.last_modified, + source: include_download_source ? presigned_url(obj, :get, DOWNLOAD_URL_EXPIRES_AFTER_SECONDS) : nil + ) + end + + def presigned_url(obj, method, expires_in_seconds) + ensure_cors! + obj.presigned_url(method, expires_in: expires_in_seconds) + end + + def ensure_cors! + rule = { + allowed_headers: ["*"], + allowed_methods: ["PUT"], + allowed_origins: [Discourse.base_url_no_prefix], + max_age_seconds: 3000 + } + + @s3_helper.ensure_cors!([rule]) + end + + def cleanup_allowed? + !SiteSetting.s3_disable_cleanup + end + end +end diff --git a/lib/disk_space.rb b/lib/disk_space.rb index 8c5e385cec..bf9bb0db0f 100644 --- a/lib/disk_space.rb +++ b/lib/disk_space.rb @@ -2,8 +2,8 @@ class DiskSpace extend ActionView::Helpers::NumberHelper - DISK_SPACE_STATS_CACHE_KEY = 'disk_space_stats'.freeze - DISK_SPACE_STATS_UPDATED_CACHE_KEY = 'disk_space_stats_updated'.freeze + DISK_SPACE_STATS_CACHE_KEY ||= 'disk_space_stats'.freeze + DISK_SPACE_STATS_UPDATED_CACHE_KEY ||= 'disk_space_stats_updated'.freeze def self.uploads_used_bytes # used(uploads_path) @@ -24,7 +24,7 @@ class DiskSpace end def self.backups_path - Backup.base_directory + BackupRestore::LocalBackupStore.base_directory end def self.uploads_path diff --git a/lib/json_error.rb b/lib/json_error.rb index 1675ac562b..7d763a2308 100644 --- a/lib/json_error.rb +++ b/lib/json_error.rb @@ -24,6 +24,12 @@ module JsonError # If we're passed an array, it's an array of error messages return { errors: obj.map(&:to_s) } if obj.is_a?(Array) && obj.present? + if obj.is_a?(Exception) + message = obj.cause.message.presence || obj.cause.class.name if obj.cause + message = obj.message.presence || obj.class.name if message.blank? + return { errors: [message] } if message.present? + end + # Log a warning (unless obj is nil) Rails.logger.warn("create_errors_json called with unrecognized type: #{obj.inspect}") if obj diff --git a/lib/s3_helper.rb b/lib/s3_helper.rb index a46fdc2d22..b4bcb44519 100644 --- a/lib/s3_helper.rb +++ b/lib/s3_helper.rb @@ -51,7 +51,7 @@ class S3Helper # make sure we have a cors config for assets # otherwise we will have no fonts - def ensure_cors! + def ensure_cors!(rules = nil) rule = nil begin @@ -63,17 +63,17 @@ class S3Helper end unless rule - puts "installing CORS rule" + rules = [{ + allowed_headers: ["Authorization"], + allowed_methods: ["GET", "HEAD"], + allowed_origins: ["*"], + max_age_seconds: 3000 + }] if rules.nil? s3_resource.client.put_bucket_cors( bucket: @s3_bucket_name, cors_configuration: { - cors_rules: [{ - allowed_headers: ["Authorization"], - allowed_methods: ["GET", "HEAD"], - allowed_origins: ["*"], - max_age_seconds: 3000 - }] + cors_rules: rules } ) end @@ -137,10 +137,7 @@ class S3Helper end def list(prefix = "") - if @s3_bucket_folder_path.present? - prefix = File.join(@s3_bucket_folder_path, prefix) - end - + prefix = get_path_for_s3_upload(prefix) s3_bucket.objects(prefix: prefix) end @@ -159,6 +156,11 @@ class S3Helper ) end + def object(path) + path = get_path_for_s3_upload(path) + s3_bucket.object(path) + end + def self.s3_options(obj) opts = { region: obj.s3_region, endpoint: SiteSetting.s3_endpoint, diff --git a/lib/site_settings/validations.rb b/lib/site_settings/validations.rb index ae38285cc8..0d50f488b3 100644 --- a/lib/site_settings/validations.rb +++ b/lib/site_settings/validations.rb @@ -1,8 +1,8 @@ module SiteSettings; end module SiteSettings::Validations - def validate_error(key) - raise Discourse::InvalidParameters.new(I18n.t("errors.site_settings.#{key}")) + def validate_error(key, opts = {}) + raise Discourse::InvalidParameters.new(I18n.t("errors.site_settings.#{key}", opts)) end def validate_default_categories(new_val, default_categories_selected) @@ -53,4 +53,13 @@ module SiteSettings::Validations validate_error :s3_upload_bucket_is_required if new_val == "t" && SiteSetting.s3_upload_bucket.blank? end + def validate_backup_location(new_val) + return unless new_val == BackupLocationSiteSetting::S3 + validate_error(:s3_backup_requires_s3_settings, setting_name: "s3_backup_bucket") if SiteSetting.s3_backup_bucket.blank? + + unless SiteSetting.s3_use_iam_profile + validate_error(:s3_backup_requires_s3_settings, setting_name: "s3_access_key_id") if SiteSetting.s3_access_key_id.blank? + validate_error(:s3_backup_requires_s3_settings, setting_name: "s3_secret_access_key") if SiteSetting.s3_secret_access_key.blank? + end + end end diff --git a/lib/tasks/s3.rake b/lib/tasks/s3.rake index 7b2889ae93..b19da1de3a 100644 --- a/lib/tasks/s3.rake +++ b/lib/tasks/s3.rake @@ -84,6 +84,8 @@ end task 's3:upload_assets' => :environment do ensure_s3_configured! + + puts "installing CORS rule" helper.ensure_cors! assets.each do |asset| diff --git a/script/discourse b/script/discourse index d0fccded04..ddfc618193 100755 --- a/script/discourse +++ b/script/discourse @@ -62,18 +62,20 @@ class DiscourseCLI < Thor require "backup_restore/backuper" puts "Starting backup..." - backuper = BackupRestore::Backuper.new(Discourse.system_user.id) - backup = backuper.run - if filename.present? - puts "Moving '#{backup}' to '#{filename}'" - puts "Including version number into '#{filename}'" - version_string = File.basename(backup)[/-#{BackupRestore::VERSION_PREFIX}\d{14}/] - filename = filename.dup.insert(filename.index('.'), version_string) - FileUtils.mv(backup, filename) - backup = filename - end + backuper = BackupRestore::Backuper.new(Discourse.system_user.id, filename: filename) + backup_filename = backuper.run puts "Backup done." - puts "Output file is in: #{backup}", "" + + store = BackupRestore::BackupStore.create + + if store.remote? + location = BackupLocationSiteSetting.values.find { |v| v[:value] == SiteSetting.backup_location } + location = I18n.t("admin_js.#{location[:name]}") if location + puts "Output file is stored on #{location} as #{backup_filename}", "" + else + backup = store.file(backup_filename, include_download_source: true) + puts "Output file is in: #{backup.source}", "" + end exit(1) unless backuper.success end @@ -92,20 +94,22 @@ class DiscourseCLI < Thor discourse = './script/discourse' end + load_rails + require "backup_restore/backup_restore" + require "backup_restore/restorer" + require "backup_restore/backup_store" + if !filename puts "You must provide a filename to restore. Did you mean one of the following?\n\n" - Dir["public/backups/default/*"].sort_by { |path| File.mtime(path) }.reverse.each do |f| - puts "#{discourse} restore #{File.basename(f)}" + store = BackupRestore::BackupStore.create + store.files.each do |file| + puts "#{discourse} restore #{file.filename}" end return end - load_rails - require "backup_restore/backup_restore" - require "backup_restore/restorer" - begin puts "Starting restore: #{filename}" restorer = BackupRestore::Restorer.new(Discourse.system_user.id, filename: filename) diff --git a/spec/lib/backup_restore/local_backup_store_spec.rb b/spec/lib/backup_restore/local_backup_store_spec.rb new file mode 100644 index 0000000000..efe87583d9 --- /dev/null +++ b/spec/lib/backup_restore/local_backup_store_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' +require 'backup_restore/local_backup_store' +require_relative 'shared_examples_for_backup_store' + +describe BackupRestore::LocalBackupStore do + before(:all) do + @base_directory = Dir.mktmpdir + @paths = [] + end + + after(:all) do + FileUtils.remove_dir(@base_directory, true) + end + + before do + SiteSetting.backup_location = BackupLocationSiteSetting::LOCAL + end + + subject(:store) { BackupRestore::BackupStore.create(base_directory: @base_directory) } + let(:expected_type) { BackupRestore::LocalBackupStore } + + it_behaves_like "backup store" + + it "is not a remote store" do + expect(store.remote?).to eq(false) + end + + def create_backups + create_file(filename: "b.tar.gz", last_modified: "2018-09-13T15:10:00Z", size_in_bytes: 17) + create_file(filename: "a.tgz", last_modified: "2018-02-11T09:27:00Z", size_in_bytes: 29) + create_file(filename: "r.sql.gz", last_modified: "2017-12-20T03:48:00Z", size_in_bytes: 11) + create_file(filename: "no-backup.txt", last_modified: "2018-09-05T14:27:00Z", size_in_bytes: 12) + end + + def remove_backups + @paths.each { |path| File.delete(path) if File.exists?(path) } + @paths.clear + end + + def create_file(filename:, last_modified:, size_in_bytes:) + path = File.join(@base_directory, filename) + return if File.exists?(path) + + @paths << path + FileUtils.touch(path) + File.truncate(path, size_in_bytes) + + time = Time.parse(last_modified) + File.utime(time, time, path) + end + + def source_regex(filename) + path = File.join(@base_directory, filename) + /^#{Regexp.escape(path)}$/ + end +end diff --git a/spec/lib/backup_restore/s3_backup_store_spec.rb b/spec/lib/backup_restore/s3_backup_store_spec.rb new file mode 100644 index 0000000000..33cfa945b0 --- /dev/null +++ b/spec/lib/backup_restore/s3_backup_store_spec.rb @@ -0,0 +1,109 @@ +require 'rails_helper' +require 'backup_restore/s3_backup_store' +require_relative 'shared_examples_for_backup_store' + +describe BackupRestore::S3BackupStore do + before(:all) do + @s3_client = Aws::S3::Client.new(stub_responses: true) + @s3_options = { client: @s3_client } + + @objects = [] + + @s3_client.stub_responses(:list_objects, -> (context) do + expect(context.params[:bucket]).to eq(SiteSetting.s3_backup_bucket) + expect(context.params[:prefix]).to be_blank + + { contents: @objects } + end) + + @s3_client.stub_responses(:delete_object, -> (context) do + expect(context.params[:bucket]).to eq(SiteSetting.s3_backup_bucket) + expect do + @objects.delete_if { |obj| obj[:key] == context.params[:key] } + end.to change { @objects } + end) + + @s3_client.stub_responses(:head_object, -> (context) do + expect(context.params[:bucket]).to eq(SiteSetting.s3_backup_bucket) + + if object = @objects.find { |obj| obj[:key] == context.params[:key] } + { content_length: object[:size], last_modified: object[:last_modified] } + else + { status_code: 404, headers: {}, body: "", } + end + end) + + @s3_client.stub_responses(:get_object, -> (context) do + expect(context.params[:bucket]).to eq(SiteSetting.s3_backup_bucket) + + if object = @objects.find { |obj| obj[:key] == context.params[:key] } + { content_length: object[:size], body: "A" * object[:size] } + else + { status_code: 404, headers: {}, body: "", } + end + end) + + @s3_client.stub_responses(:put_object, -> (context) do + expect(context.params[:bucket]).to eq(SiteSetting.s3_backup_bucket) + + @objects << { + key: context.params[:key], + size: context.params[:body].size, + last_modified: Time.zone.now + } + end) + end + + before do + SiteSetting.s3_backup_bucket = "s3-backup-bucket" + SiteSetting.s3_access_key_id = "s3-access-key-id" + SiteSetting.s3_secret_access_key = "s3-secret-access-key" + SiteSetting.backup_location = BackupLocationSiteSetting::S3 + end + + subject(:store) { BackupRestore::BackupStore.create(s3_options: @s3_options) } + let(:expected_type) { BackupRestore::S3BackupStore } + + it_behaves_like "backup store" + it_behaves_like "remote backup store" + + context "S3 specific behavior" do + before { create_backups } + after(:all) { remove_backups } + + it "doesn't delete files when cleanup is disabled" do + SiteSetting.maximum_backups = 1 + SiteSetting.s3_disable_cleanup = true + + expect { store.delete_old }.to_not change { store.files } + end + end + + def create_backups + @objects.clear + @objects << { key: "b.tar.gz", size: 17, last_modified: Time.parse("2018-09-13T15:10:00Z") } + @objects << { key: "a.tgz", size: 29, last_modified: Time.parse("2018-02-11T09:27:00Z") } + @objects << { key: "r.sql.gz", size: 11, last_modified: Time.parse("2017-12-20T03:48:00Z") } + @objects << { key: "no-backup.txt", size: 12, last_modified: Time.parse("2018-09-05T14:27:00Z") } + end + + def remove_backups + @objects.clear + end + + def source_regex(filename) + bucket = Regexp.escape(SiteSetting.s3_backup_bucket) + filename = Regexp.escape(filename) + expires = BackupRestore::S3BackupStore::DOWNLOAD_URL_EXPIRES_AFTER_SECONDS + + /\Ahttps:\/\/#{bucket}.*\/#{filename}\?.*X-Amz-Expires=#{expires}.*X-Amz-Signature=.*\z/ + end + + def upload_url_regex(filename) + bucket = Regexp.escape(SiteSetting.s3_backup_bucket) + filename = Regexp.escape(filename) + expires = BackupRestore::S3BackupStore::UPLOAD_URL_EXPIRES_AFTER_SECONDS + + /\Ahttps:\/\/#{bucket}.*\/#{filename}\?.*X-Amz-Expires=#{expires}.*X-Amz-Signature=.*\z/ + end +end diff --git a/spec/lib/backup_restore/shared_examples_for_backup_store.rb b/spec/lib/backup_restore/shared_examples_for_backup_store.rb new file mode 100644 index 0000000000..2ab716b603 --- /dev/null +++ b/spec/lib/backup_restore/shared_examples_for_backup_store.rb @@ -0,0 +1,176 @@ +shared_context "backups" do + before { create_backups } + after(:all) { remove_backups } + + let(:backup1) { BackupFile.new(filename: "b.tar.gz", size: 17, last_modified: Time.parse("2018-09-13T15:10:00Z")) } + let(:backup2) { BackupFile.new(filename: "a.tgz", size: 29, last_modified: Time.parse("2018-02-11T09:27:00Z")) } + let(:backup3) { BackupFile.new(filename: "r.sql.gz", size: 11, last_modified: Time.parse("2017-12-20T03:48:00Z")) } +end + +shared_examples "backup store" do + it "creates the correct backup store" do + expect(store).to be_a(expected_type) + end + + context "without backup files" do + describe "#files" do + it "returns an empty array when there are no files" do + expect(store.files).to be_empty + end + end + + describe "#latest_file" do + it "returns nil when there are no files" do + expect(store.latest_file).to be_nil + end + end + end + + context "with backup files" do + include_context "backups" + + describe "#files" do + it "sorts files by last modified date in descending order" do + expect(store.files).to eq([backup1, backup2, backup3]) + end + + it "returns only *.gz and *.tgz files" do + files = store.files + expect(files).to_not be_empty + expect(files.map(&:filename)).to contain_exactly(backup1.filename, backup2.filename, backup3.filename) + end + end + + describe "#latest_file" do + it "returns the most recent backup file" do + expect(store.latest_file).to eq(backup1) + end + + it "returns nil when there are no files" do + store.files.each { |file| store.delete_file(file.filename) } + expect(store.latest_file).to be_nil + end + end + + describe "#delete_old" do + it "does nothing if the number of files is <= maximum_backups" do + SiteSetting.maximum_backups = 3 + + store.delete_old + expect(store.files).to eq([backup1, backup2, backup3]) + end + + it "deletes files starting by the oldest" do + SiteSetting.maximum_backups = 1 + + store.delete_old + expect(store.files).to eq([backup1]) + end + end + + describe "#file" do + it "returns information about the file when the file exists" do + expect(store.file(backup1.filename)).to eq(backup1) + end + + it "returns nil when the file doesn't exist" do + expect(store.file("foo.gz")).to be_nil + end + + it "includes the file's source location if it is requested" do + file = store.file(backup1.filename, include_download_source: true) + expect(file.source).to match(source_regex(backup1.filename)) + end + end + + describe "#delete_file" do + it "deletes file when the file exists" do + expect(store.files).to include(backup1) + store.delete_file(backup1.filename) + expect(store.files).to_not include(backup1) + + expect(store.file(backup1.filename)).to be_nil + end + + it "does nothing when the file doesn't exist" do + expect { store.delete_file("foo.gz") }.to_not change { store.files } + end + end + + describe "#download_file" do + it "downloads file to the destination" do + filename = backup1.filename + + Dir.mktmpdir do |path| + destination_path = File.join(path, File.basename(filename)) + store.download_file(filename, destination_path) + + expect(File.exists?(destination_path)).to eq(true) + expect(File.size(destination_path)).to eq(backup1.size) + end + end + + it "raises an exception when the download fails" do + filename = backup1.filename + destination_path = Dir.mktmpdir { |path| File.join(path, File.basename(filename)) } + + expect { store.download_file(filename, destination_path) }.to raise_exception(StandardError) + end + end + end +end + +shared_examples "remote backup store" do + it "is a remote store" do + expect(store.remote?).to eq(true) + end + + context "with backups" do + include_context "backups" + + describe "#upload_file" do + it "uploads file into store" do + freeze_time + + backup = BackupFile.new( + filename: "foo.tar.gz", + size: 33, + last_modified: Time.zone.now + ) + + expect(store.files).to_not include(backup) + + Tempfile.create(backup.filename) do |file| + file.write("A" * backup.size) + file.close + + store.upload_file(backup.filename, file.path, "application/gzip") + end + + expect(store.files).to include(backup) + expect(store.file(backup.filename)).to eq(backup) + end + + it "raises an exception when a file with same filename exists" do + Tempfile.create(backup1.filename) do |file| + expect { store.upload_file(backup1.filename, file.path, "application/gzip") } + .to raise_exception(BackupRestore::BackupStore::BackupFileExists) + end + end + end + + describe "#generate_upload_url" do + it "generates upload URL" do + filename = "foo.tar.gz" + url = store.generate_upload_url(filename) + + expect(url).to match(upload_url_regex(filename)) + end + + it "raises an exeption when a file with same filename exists" do + expect { store.generate_upload_url(backup1.filename) } + .to raise_exception(BackupRestore::BackupStore::BackupFileExists) + end + end + end +end diff --git a/spec/models/admin_dashboard_data_spec.rb b/spec/models/admin_dashboard_data_spec.rb index 6d81f623dd..690ed831e8 100644 --- a/spec/models/admin_dashboard_data_spec.rb +++ b/spec/models/admin_dashboard_data_spec.rb @@ -210,9 +210,9 @@ describe AdminDashboardData do end context 'when setting is enabled' do - let(:setting_enabled) { true } before do - SiteSetting.public_send("#{setting_key}=", setting_enabled) + all_setting_keys.each { |key| SiteSetting.public_send("#{key}=", 'foo') } + SiteSetting.public_send("#{setting[:key]}=", setting[:enabled_value]) SiteSetting.public_send("#{bucket_key}=", bucket_value) end @@ -229,7 +229,7 @@ describe AdminDashboardData do context 'when bucket is filled in' do let(:bucket_value) { 'a' } before do - SiteSetting.public_send("s3_use_iam_profile=", use_iam_profile) + SiteSetting.s3_use_iam_profile = use_iam_profile end context 'when using iam profile' do @@ -260,7 +260,7 @@ describe AdminDashboardData do context 'when setting is not enabled' do before do - SiteSetting.public_send("#{setting_key}=", false) + SiteSetting.public_send("#{setting[:key]}=", setting[:disabled_value]) end it "always returns nil" do @@ -272,13 +272,25 @@ describe AdminDashboardData do end describe 'uploads' do - let(:setting_key) { :enable_s3_uploads } + let(:setting) do + { + key: :enable_s3_uploads, + enabled_value: true, + disabled_value: false + } + end let(:bucket_key) { :s3_upload_bucket } include_examples 'problem detection for s3-dependent setting' end describe 'backups' do - let(:setting_key) { :enable_s3_backups } + let(:setting) do + { + key: :backup_location, + enabled_value: BackupLocationSiteSetting::S3, + disabled_value: BackupLocationSiteSetting::LOCAL + } + end let(:bucket_key) { :s3_backup_bucket } include_examples 'problem detection for s3-dependent setting' end diff --git a/spec/models/backup_spec.rb b/spec/models/backup_spec.rb deleted file mode 100644 index 1247ff3d57..0000000000 --- a/spec/models/backup_spec.rb +++ /dev/null @@ -1,130 +0,0 @@ -require 'rails_helper' -require "s3_helper" - -require_dependency 'backup' - -describe Backup do - - let(:b1) { Backup.new('backup1') } - let(:b2) { Backup.new('backup2') } - let(:b3) { Backup.new('backup3') } - - before do - Backup.stubs(:all).returns([b1, b2, b3]) - end - - context '#remove_old' do - it "does nothing if there aren't more backups than the setting" do - SiteSetting.maximum_backups = 3 - Backup.any_instance.expects(:remove).never - Backup.remove_old - end - - it "calls remove on the backups over our limit" do - SiteSetting.maximum_backups = 1 - b1.expects(:remove).never - b2.expects(:remove).once - b3.expects(:remove).once - Backup.remove_old - end - end - - shared_context 's3 helpers' do - let(:client) { Aws::S3::Client.new(stub_responses: true) } - let(:resource) { Aws::S3::Resource.new(client: client) } - let!(:s3_bucket) { resource.bucket("s3-upload-bucket") } - let(:s3_helper) { b1.s3 } - - before(:each) do - SiteSetting.s3_backup_bucket = "s3-upload-bucket" - SiteSetting.s3_access_key_id = "s3-access-key-id" - SiteSetting.s3_secret_access_key = "s3-secret-access-key" - end - end - - context ".after_create_hook" do - context "when SiteSetting is true" do - include_context "s3 helpers" - - before do - SiteSetting.enable_s3_backups = true - end - - it "should upload the backup to S3 with the right paths" do - b1.path = 'some/path/backup.gz' - File.expects(:open).with(b1.path).yields(stub) - - s3_helper.expects(:s3_bucket).returns(s3_bucket) - s3_object = stub - - s3_bucket.expects(:object).with(b1.filename).returns(s3_object) - s3_object.expects(:upload_file) - - b1.after_create_hook - end - - context "when s3_backup_bucket includes folders path" do - before do - SiteSetting.s3_backup_bucket = "s3-upload-bucket/discourse-backups" - end - - it "should upload the backup to S3 with the right paths" do - b1.path = 'some/path/backup.gz' - File.expects(:open).with(b1.path).yields(stub) - - s3_helper.expects(:s3_bucket).returns(s3_bucket) - s3_object = stub - - s3_bucket.expects(:object).with("discourse-backups/#{b1.filename}").returns(s3_object) - s3_object.expects(:upload_file) - - b1.after_create_hook - end - end - end - - it "calls upload_to_s3 if the SiteSetting is false" do - SiteSetting.enable_s3_backups = false - b1.expects(:upload_to_s3).never - b1.after_create_hook - end - end - - context ".after_remove_hook" do - include_context "s3 helpers" - - context "when SiteSetting is true" do - before do - SiteSetting.enable_s3_backups = true - end - - context "when s3_backup_bucket includes folders path" do - before do - SiteSetting.s3_backup_bucket = "s3-upload-bucket/discourse-backups" - end - - it "should upload the backup to S3 with the right paths" do - s3_helper.expects(:s3_bucket).returns(s3_bucket) - s3_object = stub - - s3_bucket.expects(:object).with("discourse-backups/#{b1.filename}").returns(s3_object) - s3_object.expects(:delete) - - b1.after_remove_hook - end - end - end - - context "when SiteSetting is false" do - before do - SiteSetting.enable_s3_backups = false - end - - it "doesn’t call remove_from_s3" do - b1.expects(:remove_from_s3).never - b1.after_remove_hook - end - end - end - -end diff --git a/spec/requests/admin/backups_controller_spec.rb b/spec/requests/admin/backups_controller_spec.rb index 4224bcb07b..f734413874 100644 --- a/spec/requests/admin/backups_controller_spec.rb +++ b/spec/requests/admin/backups_controller_spec.rb @@ -4,6 +4,25 @@ RSpec.describe Admin::BackupsController do let(:admin) { Fabricate(:admin) } let(:backup_filename) { "2014-02-10-065935.tar.gz" } let(:backup_filename2) { "2014-02-11-065935.tar.gz" } + let(:store) { BackupRestore::LocalBackupStore.new } + + def create_backup_files(*filenames) + @paths = filenames.map do |filename| + path = backup_path(filename) + File.open(path, "w") { |f| f.write("test backup") } + path + end + end + + def backup_path(filename) + File.join(BackupRestore::LocalBackupStore.base_directory, filename) + end + + def map_preloaded + controller.instance_variable_get("@preloaded").map do |key, value| + [key, JSON.parse(value)] + end.to_h + end it "is a subclass of AdminController" do expect(Admin::BackupsController < Admin::AdminController).to eq(true) @@ -11,10 +30,14 @@ RSpec.describe Admin::BackupsController do before do sign_in(admin) + SiteSetting.backup_location = BackupLocationSiteSetting::LOCAL end after do $redis.flushall + + @paths&.each { |path| File.delete(path) if File.exists?(path) } + @paths = nil end describe "#index" do @@ -29,11 +52,7 @@ RSpec.describe Admin::BackupsController do get "/admin/backups.html" expect(response.status).to eq(200) - preloaded = controller.instance_variable_get("@preloaded").map do |key, value| - [key, JSON.parse(value)] - end.to_h - - expect(preloaded["backups"].size).to eq(Backup.all.size) + preloaded = map_preloaded expect(preloaded["operations_status"].symbolize_keys).to eq(BackupRestore.operations_status) expect(preloaded["logs"].size).to eq(BackupRestore.logs.size) end @@ -42,23 +61,14 @@ RSpec.describe Admin::BackupsController do context "json format" do it "returns a list of all the backups" do begin - paths = [] - [backup_filename, backup_filename2].each do |name| - path = File.join(Backup.base_directory, name) - paths << path - File.open(path, "w") { |f| f.write("hello") } - Backup.create_from_filename(name) - end + create_backup_files(backup_filename, backup_filename2) get "/admin/backups.json" - expect(response.status).to eq(200) - json = JSON.parse(response.body).map { |backup| backup["filename"] } - expect(json).to include(backup_filename) - expect(json).to include(backup_filename2) - ensure - paths.each { |path| File.delete(path) } + filenames = JSON.parse(response.body).map { |backup| backup["filename"] } + expect(filenames).to include(backup_filename) + expect(filenames).to include(backup_filename2) end end end @@ -88,36 +98,23 @@ RSpec.describe Admin::BackupsController do it "uses send_file to transmit the backup" do begin token = EmailBackupToken.set(admin.id) - path = File.join(Backup.base_directory, backup_filename) - File.open(path, "w") { |f| f.write("hello") } - - Backup.create_from_filename(backup_filename) + create_backup_files(backup_filename) expect do get "/admin/backups/#{backup_filename}.json", params: { token: token } end.to change { UserHistory.where(action: UserHistory.actions[:backup_download]).count }.by(1) - expect(response.headers['Content-Length']).to eq("5") + expect(response.headers['Content-Length']).to eq("11") expect(response.headers['Content-Disposition']).to match(/attachment; filename/) - ensure - File.delete(path) - EmailBackupToken.del(admin.id) end end it "returns 422 when token is bad" do begin - path = File.join(Backup.base_directory, backup_filename) - File.open(path, "w") { |f| f.write("hello") } - - Backup.create_from_filename(backup_filename) - get "/admin/backups/#{backup_filename}.json", params: { token: "bad_value" } expect(response.status).to eq(422) expect(response.headers['Content-Disposition']).not_to match(/attachment; filename/) - ensure - File.delete(path) end end @@ -125,20 +122,16 @@ RSpec.describe Admin::BackupsController do token = EmailBackupToken.set(admin.id) get "/admin/backups/#{backup_filename}.json", params: { token: token } - EmailBackupToken.del(admin.id) expect(response.status).to eq(404) end end describe '#destroy' do - let(:b) { Backup.new(backup_filename) } - it "removes the backup if found" do begin - path = File.join(Backup.base_directory, backup_filename) - File.open(path, "w") { |f| f.write("hello") } - - Backup.create_from_filename(backup_filename) + path = backup_path(backup_filename) + create_backup_files(backup_filename) + expect(File.exists?(path)).to eq(true) expect do delete "/admin/backups/#{backup_filename}.json" @@ -146,8 +139,6 @@ RSpec.describe Admin::BackupsController do expect(response.status).to eq(200) expect(File.exists?(path)).to eq(false) - ensure - File.delete(path) if File.exists?(path) end end @@ -162,9 +153,7 @@ RSpec.describe Admin::BackupsController do get "/admin/backups/logs.html" expect(response.status).to eq(200) - preloaded = controller.instance_variable_get("@preloaded").map do |key, value| - [key, JSON.parse(value)] - end.to_h + preloaded = map_preloaded expect(preloaded["operations_status"].symbolize_keys).to eq(BackupRestore.operations_status) expect(preloaded["logs"].size).to eq(BackupRestore.logs.size) @@ -228,6 +217,7 @@ RSpec.describe Admin::BackupsController do described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) filename = 'test_Site-0123456789.tar.gz' + @paths = [backup_path(File.join('tmp', 'test', "#{filename}.part1"))] post "/admin/backups/upload.json", params: { resumableFilename: filename, @@ -241,13 +231,6 @@ RSpec.describe Admin::BackupsController do expect(response.status).to eq(200) expect(response.body).to eq("") - ensure - begin - File.delete( - File.join(Backup.base_directory, 'tmp', 'test', "#{filename}.part1") - ) - rescue Errno::ENOENT - end end end end @@ -284,18 +267,16 @@ RSpec.describe Admin::BackupsController do end describe "#email" do - let(:backup_filename) { "test.tar.gz" } - let(:backup) { Backup.new(backup_filename) } - it "enqueues email job" do - Backup.expects(:[]).with(backup_filename).returns(backup) + create_backup_files(backup_filename) - Jobs.expects(:enqueue).with(:download_backup_email, - user_id: admin.id, - backup_file_path: 'http://www.example.com/admin/backups/test.tar.gz' - ) + expect { + put "/admin/backups/#{backup_filename}.json" + }.to change { Jobs::DownloadBackupEmail.jobs.size }.by(1) - put "/admin/backups/#{backup_filename}.json" + job_args = Jobs::DownloadBackupEmail.jobs.last["args"].first + expect(job_args["user_id"]).to eq(admin.id) + expect(job_args["backup_file_path"]).to eq("http://www.example.com/admin/backups/#{backup_filename}") expect(response.status).to eq(200) end diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index ba2448b35c..56804e0aca 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -109,6 +109,14 @@ QUnit.test("ensures an authorized upload", assert => { ); }); +QUnit.test("skipping validation works", assert => { + const files = [{ name: "backup.tar.gz" }]; + sandbox.stub(bootbox, "alert"); + + assert.not(validUpload(files, { skipValidation: false })); + assert.ok(validUpload(files, { skipValidation: true })); +}); + QUnit.test("staff can upload anything in PM", assert => { const files = [{ name: "some.docx" }]; Discourse.SiteSettings.authorized_extensions = "jpeg"; From 2ce684b1345cd80bd081733521922922818cb84c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 15 Oct 2018 10:24:05 +0800 Subject: [PATCH 007/209] DEV: Clear `hex_cache` after each test. --- spec/rails_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 2a78230fbe..2deb20cf91 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -134,6 +134,7 @@ RSpec.configure do |config| unfreeze_time ActionMailer::Base.deliveries.clear + ColorScheme.hex_cache.clear raise if ActiveRecord::Base.connection_pool.stat[:busy] > 1 end From a4aa4a9be49ca667aeefc8fac23598c161993b75 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 15 Oct 2018 10:38:02 +0800 Subject: [PATCH 008/209] DEV: Remove the use of mocks in our tests. --- .../spec/controllers/polls_controller_spec.rb | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/plugins/poll/spec/controllers/polls_controller_spec.rb b/plugins/poll/spec/controllers/polls_controller_spec.rb index 5f377896bb..6b84f72ad4 100644 --- a/plugins/poll/spec/controllers/polls_controller_spec.rb +++ b/plugins/poll/spec/controllers/polls_controller_spec.rb @@ -12,17 +12,20 @@ describe ::DiscoursePoll::PollsController do describe "#vote" do it "works" do - MessageBus.expects(:publish) + message = MessageBus.track_publish do + put :vote, params: { + post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] + }, format: :json - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json + expect(response.status).to eq(200) + end.first - expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["poll"]["name"]).to eq("poll") expect(json["poll"]["voters"]).to eq(1) expect(json["vote"]).to eq(["5c24fc1df56d764b550ceae1b9319125"]) + + expect(message.channel).to eq("/polls/#{poll.topic_id}") end it "requires at least 1 valid option" do @@ -202,28 +205,33 @@ describe ::DiscoursePoll::PollsController do describe "#toggle_status" do it "works for OP" do - MessageBus.expects(:publish) + message = MessageBus.track_publish do + put :toggle_status, params: { + post_id: poll.id, poll_name: "poll", status: "closed" + }, format: :json - put :toggle_status, params: { - post_id: poll.id, poll_name: "poll", status: "closed" - }, format: :json + expect(response.status).to eq(200) + end.first - expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["poll"]["status"]).to eq("closed") + expect(message.channel).to eq("/polls/#{poll.topic_id}") end it "works for staff" do log_in(:moderator) - MessageBus.expects(:publish) - put :toggle_status, params: { - post_id: poll.id, poll_name: "poll", status: "closed" - }, format: :json + message = MessageBus.track_publish do + put :toggle_status, params: { + post_id: poll.id, poll_name: "poll", status: "closed" + }, format: :json + + expect(response.status).to eq(200) + end.first - expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["poll"]["status"]).to eq("closed") + expect(message.channel).to eq("/polls/#{poll.topic_id}") end it "ensures post is not trashed" do From 3aceda2dfd688d6f326c7935f62ce3f55a7a2a3d Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Oct 2018 13:49:04 +1100 Subject: [PATCH 009/209] Update to latest version of message bus This includes DistributedCache which we will be using and perf fixes --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index bdf24bd5ef..7842eb25de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -193,7 +193,7 @@ GEM mini_mime (>= 0.1.1) maxminddb (0.1.21) memory_profiler (0.9.12) - message_bus (2.1.5) + message_bus (2.1.6) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) From d408073fc295ce6117995c2f211b15e57e447793 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Mon, 15 Oct 2018 05:53:21 +0300 Subject: [PATCH 010/209] DEV: Update official plugins list canned replies is now named discourse-canned-replies which keeps our naming consistent --- lib/plugin/metadata.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb index 0c19c4fd7d..22204937dc 100644 --- a/lib/plugin/metadata.rb +++ b/lib/plugin/metadata.rb @@ -4,6 +4,8 @@ module Plugin; end class Plugin::Metadata OFFICIAL_PLUGINS ||= Set.new([ + # TODO: Remove this after everyone upgraded `discourse-canned-replies` + # to the renamed version. "Canned Replies", "customer-flair", "discourse-adplugin", @@ -15,6 +17,7 @@ class Plugin::Metadata "discourse-bbcode", "discourse-bbcode-color", "discourse-cakeday", + "discourse-canned-replies", "discourse-calendar", "discourse-characters-required", "discourse-chat-integration", From 57b52cd1de1b22413011285cd3b0a2c52c16ab83 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 15 Oct 2018 04:57:15 +0200 Subject: [PATCH 011/209] FIX: keep emoji syntax for custom emojis in quotes (#6488) --- .../javascripts/discourse/lib/to-markdown.js.es6 | 2 +- test/javascripts/lib/to-markdown-test.js.es6 | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 index 28b9e3bedc..22fbd80273 100644 --- a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 +++ b/app/assets/javascripts/discourse/lib/to-markdown.js.es6 @@ -227,7 +227,7 @@ export class Tag { const src = attr.src || pAttr.src; const cssClass = attr.class || pAttr.class; - if (cssClass === "emoji") { + if (cssClass && cssClass.includes("emoji")) { return attr.title || pAttr.title; } diff --git a/test/javascripts/lib/to-markdown-test.js.es6 b/test/javascripts/lib/to-markdown-test.js.es6 index 57a437a4a5..c79249c4d5 100644 --- a/test/javascripts/lib/to-markdown-test.js.es6 +++ b/test/javascripts/lib/to-markdown-test.js.es6 @@ -340,3 +340,15 @@ QUnit.test("keeps emoji and removes click count", assert => { assert.equal(toMarkdown(html), markdown); }); + +QUnit.test("keeps emoji syntax for custom emoji", assert => { + const html = ` +

+ :custom_emoji: +

+ `; + + const markdown = `:custom_emoji:`; + + assert.equal(toMarkdown(html), markdown); +}); From 2acb885c727050f773cf736dd4f7f5b7f0a97ead Mon Sep 17 00:00:00 2001 From: Joe <33972521+hnb-ku@users.noreply.github.com> Date: Mon, 15 Oct 2018 10:59:49 +0800 Subject: [PATCH 012/209] FEATURE: fullscreen composer mode on desktop Adds keyboard shortcut and icon that allows expanding composer to full screen. --- .../components/composer-messages.js.es6 | 2 +- .../components/composer-toggles.js.es6 | 28 ++-- .../discourse/controllers/composer.js.es6 | 27 +++- .../discourse/lib/keyboard-shortcuts.js.es6 | 8 + .../discourse/models/composer.js.es6 | 22 ++- .../templates/components/composer-toggles.hbs | 10 +- .../discourse/templates/composer.hbs | 146 +++++++++--------- .../modal/keyboard-shortcuts-help.hbs | 1 + .../stylesheets/common/base/compose.scss | 2 +- app/assets/stylesheets/desktop/compose.scss | 50 ++++++ config/locales/client.en.yml | 3 + 11 files changed, 209 insertions(+), 90 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-messages.js.es6 b/app/assets/javascripts/discourse/components/composer-messages.js.es6 index cf0fdaad8f..1ac68616f8 100644 --- a/app/assets/javascripts/discourse/components/composer-messages.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-messages.js.es6 @@ -13,7 +13,7 @@ export default Ember.Component.extend({ _yourselfConfirm: null, similarTopics: null, - hidden: Ember.computed.not("composer.viewOpen"), + hidden: Ember.computed.not("composer.viewOpenOrFullscreen"), didInsertElement() { this._super(); diff --git a/app/assets/javascripts/discourse/components/composer-toggles.js.es6 b/app/assets/javascripts/discourse/components/composer-toggles.js.es6 index 918cfe2bab..e1172df2e2 100644 --- a/app/assets/javascripts/discourse/components/composer-toggles.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-toggles.js.es6 @@ -4,18 +4,28 @@ export default Ember.Component.extend({ tagName: "", @computed("composeState") - title(composeState) { - if (composeState === "draft" || composeState === "saving") { - return "composer.abandon"; - } - return "composer.collapse"; + toggleTitle(composeState) { + return composeState === "draft" || composeState === "saving" + ? "composer.abandon" + : "composer.collapse"; + }, + + @computed("composeState") + fullscreenTitle(composeState) { + return composeState === "fullscreen" + ? "composer.exit_fullscreen" + : "composer.enter_fullscreen"; }, @computed("composeState") toggleIcon(composeState) { - if (composeState === "draft" || composeState === "saving") { - return "times"; - } - return "chevron-down"; + return composeState === "draft" || composeState === "saving" + ? "times" + : "chevron-down"; + }, + + @computed("composeState") + fullscreenIcon(composeState) { + return composeState === "fullscreen" ? "compress" : "expand"; } }); diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 1a6bf47dd0..8a0946c5e9 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -231,7 +231,7 @@ export default Ember.Controller.extend({ @computed("model.composeState", "model.creatingTopic") popupMenuOptions(composeState) { - if (composeState === "open") { + if (composeState === "open" || composeState === "fullscreen") { let options = []; options.push( @@ -386,7 +386,10 @@ export default Ember.Controller.extend({ ) { this.close(); } else { - if (this.get("model.composeState") === Composer.OPEN) { + if ( + this.get("model.composeState") === Composer.OPEN || + this.get("model.composeState") === Composer.FULLSCREEN + ) { this.shrink(); } else { this.cancelComposer(); @@ -396,6 +399,11 @@ export default Ember.Controller.extend({ return false; }, + fullscreenComposer() { + this.toggleFullscreen(); + return false; + }, + // Import a quote from the post importQuote(toolbarEvent) { const postStream = this.get("topic.postStream"); @@ -457,7 +465,7 @@ export default Ember.Controller.extend({ return; } - if (this.get("model.viewOpen")) { + if (this.get("model.viewOpen") || this.get("model.viewFullscreen")) { this.shrink(); } }, @@ -881,6 +889,10 @@ export default Ember.Controller.extend({ } ]); } else { + // in case the composer is + // cancelled while in fullscreen + $("html").removeClass("fullscreen-composer"); + // it is possible there is some sort of crazy draft with no body ... just give up on it this.destroyDraft(); this.get("model").clearState(); @@ -947,6 +959,15 @@ export default Ember.Controller.extend({ this.set("model.composeState", Composer.DRAFT); }, + toggleFullscreen() { + this._saveDraft(); + if (this.get("model.composeState") === Composer.FULLSCREEN) { + this.set("model.composeState", Composer.OPEN); + } else { + this.set("model.composeState", Composer.FULLSCREEN); + } + }, + close() { this.setProperties({ model: null, lastValidatedAt: null }); }, diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index 01b9e3372e..31f1c9a214 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -65,6 +65,7 @@ const bindings = { "shift+s": { click: "#topic-footer-buttons button.share", anonymous: true }, // share topic "shift+u": { handler: "goToUnreadPost" }, "shift+z shift+z": { handler: "logout" }, + "shift+f11": { handler: "fullscreenComposer" }, t: { postAction: "replyAsNewTopic" }, u: { handler: "goBack", anonymous: true }, "x r": { @@ -212,6 +213,13 @@ export default { } }, + fullscreenComposer() { + const composer = this.container.lookup("controller:composer"); + if (composer.get("model")) { + composer.toggleFullscreen(); + } + }, + pinUnpinTopic() { this.container.lookup("controller:topic").togglePinnedState(); }, diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 483bb66eef..a86e48de2d 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -26,6 +26,7 @@ const CLOSED = "closed", SAVING = "saving", OPEN = "open", DRAFT = "draft", + FULLSCREEN = "fullscreen", // When creating, these fields are moved into the post model from the composer model _create_serializer = { raw: "reply", @@ -144,15 +145,24 @@ const Composer = RestModel.extend({ viewOpen: Em.computed.equal("composeState", OPEN), viewDraft: Em.computed.equal("composeState", DRAFT), + viewFullscreen: Em.computed.equal("composeState", FULLSCREEN), + viewOpenOrFullscreen: Em.computed.or("viewOpen", "viewFullscreen"), composeStateChanged: function() { - var oldOpen = this.get("composerOpened"); + let oldOpen = this.get("composerOpened"), + elem = $("html"); + + if (this.get("composeState") === FULLSCREEN) { + elem.addClass("fullscreen-composer"); + } else { + elem.removeClass("fullscreen-composer"); + } if (this.get("composeState") === OPEN) { this.set("composerOpened", oldOpen || new Date()); } else { if (oldOpen) { - var oldTotal = this.get("composerTotalOpened") || 0; + let oldTotal = this.get("composerTotalOpened") || 0; this.set("composerTotalOpened", oldTotal + (new Date() - oldOpen)); } this.set("composerOpened", null); @@ -160,9 +170,8 @@ const Composer = RestModel.extend({ }.observes("composeState"), composerTime: function() { - var total = this.get("composerTotalOpened") || 0; - - var oldOpen = this.get("composerOpened"); + let total = this.get("composerTotalOpened") || 0, + oldOpen = this.get("composerOpened"); if (oldOpen) { total += new Date() - oldOpen; } @@ -183,7 +192,7 @@ const Composer = RestModel.extend({ // view detected user is typing typing: _.throttle( function() { - var typingTime = this.get("typingTime") || 0; + let typingTime = this.get("typingTime") || 0; this.set("typingTime", typingTime + 100); }, 100, @@ -1041,6 +1050,7 @@ Composer.reopenClass({ SAVING, OPEN, DRAFT, + FULLSCREEN, // The actions the composer can take CREATE_TOPIC, diff --git a/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs b/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs index 93263ae3cb..a9efe1f6d5 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-toggles.hbs @@ -10,5 +10,13 @@ class="toggler" icon=toggleIcon action=toggleComposer - title=title}} + title=toggleTitle}} + + {{#unless site.mobileView}} + {{flat-button + class="toggle-fullscreen" + icon=fullscreenIcon + action=toggleFullscreen + title=fullscreenTitle}} + {{/unless}}
diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index dc71db36f7..d60ed2e804 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -9,73 +9,77 @@ {{composer-messages composer=model messageCount=messageCount addLinkLookup="addLinkLookup"}} - {{#if model.viewOpen}} + {{#if model.viewOpenOrFullscreen}}
{{plugin-outlet name="composer-open" args=(hash model=model)}}
-
- {{composer-action-title model=model canWhisper=canWhisper tabindex=8}} + {{#unless model.viewFullscreen}} +
+ {{composer-action-title model=model canWhisper=canWhisper tabindex=8}} - {{#unless site.mobileView}} - {{#if whisperOrUnlistTopicText}} - ({{whisperOrUnlistTopicText}}) - {{/if}} - {{#if model.noBump}} - {{d-icon "anchor"}} - {{/if}} - {{/unless}} + {{#unless site.mobileView}} + {{#if whisperOrUnlistTopicText}} + ({{whisperOrUnlistTopicText}}) + {{/if}} + {{#if model.noBump}} + {{d-icon "anchor"}} + {{/if}} + {{/unless}} - {{#if canEdit}} - {{#link-to-input onClick=(action "displayEditReason") showInput=showEditReason key="composer.show_edit_reason" class="display-edit-reason"}} - {{text-field value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}} - {{/link-to-input}} - {{/if}} -
+ {{#if canEdit}} + {{#link-to-input onClick=(action "displayEditReason") showInput=showEditReason key="composer.show_edit_reason" class="display-edit-reason"}} + {{text-field value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}} + {{/link-to-input}} + {{/if}} +
+ {{/unless}} {{composer-toggles composeState=model.composeState toggleComposer=(action "toggle") - toggleToolbar=(action "toggleToolbar")}} + toggleToolbar=(action "toggleToolbar") + toggleFullscreen=(action "fullscreenComposer")}}
+ {{#unless model.viewFullscreen}} + {{#if model.canEditTitle}} + {{#if model.creatingPrivateMessage}} +
+ {{composer-user-selector topicId=topicModel.id + usernames=model.targetUsernames + hasGroups=model.hasTargetGroups + focusTarget=focusTarget + class="users-input"}} + {{#if showWarning}} + + {{/if}} +
+ {{/if}} - {{#if model.canEditTitle}} - {{#if model.creatingPrivateMessage}} -
- {{composer-user-selector topicId=topicModel.id - usernames=model.targetUsernames - hasGroups=model.hasTargetGroups - focusTarget=focusTarget - class="users-input"}} - {{#if showWarning}} - +
+ + {{composer-title composer=model lastValidatedAt=lastValidatedAt focusTarget=focusTarget}} + + {{#if model.showCategoryChooser}} +
+ {{category-chooser + fullWidthOnMobile=true + value=model.categoryId + scopedCategoryId=scopedCategoryId + tabindex="3"}} + {{popup-input-tip validation=categoryValidation}} +
+ {{/if}} + {{#if canEditTags}} + {{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId minimum=model.minimumRequiredTags}} + {{popup-input-tip validation=tagValidation}} {{/if}}
{{/if}} -
- - {{composer-title composer=model lastValidatedAt=lastValidatedAt focusTarget=focusTarget}} - - {{#if model.showCategoryChooser}} -
- {{category-chooser - fullWidthOnMobile=true - value=model.categoryId - scopedCategoryId=scopedCategoryId - tabindex="3"}} - {{popup-input-tip validation=categoryValidation}} -
- {{/if}} - {{#if canEditTags}} - {{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId minimum=model.minimumRequiredTags}} - {{popup-input-tip validation=tagValidation}} - {{/if}} -
- {{/if}} - - {{plugin-outlet name="composer-fields" args=(hash model=model)}} + {{plugin-outlet name="composer-fields" args=(hash model=model)}} + {{/unless}}
@@ -104,21 +108,24 @@ {{plugin-outlet name="composer-fields-below" args=(hash model=model)}}
- {{composer-save-button action=(action "save") - icon=model.saveIcon - label=model.saveLabel - disableSubmit=disableSubmit}} - {{#if site.mobileView}} - - {{#if canEdit}} - {{d-icon "times"}} - {{else}} - {{d-icon "trash-o"}} - {{/if}} - - {{else}} - {{i18n 'cancel'}} - {{/if}} + {{#unless model.viewFullscreen}} + {{composer-save-button action=(action "save") + icon=model.saveIcon + label=model.saveLabel + disableSubmit=disableSubmit}} + + {{#if site.mobileView}} + + {{#if canEdit}} + {{d-icon "times"}} + {{else}} + {{d-icon "trash-o"}} + {{/if}} + + {{else}} + {{i18n 'cancel'}} + {{/if}} + {{/unless}} {{#if site.mobileView}} @@ -165,7 +172,7 @@
- {{else}} + {{else}}
{{#if model.createdPost}} {{i18n 'composer.saved'}} @@ -183,6 +190,7 @@
{{composer-toggles composeState=model.composeState + toggleFullscreen=(action "fullscreenComposer") toggleComposer=(action "toggle") toggleToolbar=(action "toggleToolbar")}} diff --git a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs index 9d5ae70e07..58b432b06a 100644 --- a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs +++ b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs @@ -40,6 +40,7 @@

{{i18n 'keyboard_shortcuts_help.composing.title'}}

  • {{{i18n 'keyboard_shortcuts_help.composing.return'}}}
  • +
  • {{{i18n 'keyboard_shortcuts_help.composing.fullscreen'}}}
  • {{{i18n 'keyboard_shortcuts_help.application.create'}}}
  • {{{i18n 'keyboard_shortcuts_help.actions.reply_as_new_topic'}}}
  • {{{i18n 'keyboard_shortcuts_help.actions.reply_topic'}}}
  • diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 18b898a8c5..ba0e013c8b 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -147,7 +147,7 @@ margin-left: auto; margin-right: -5px; button { - padding: 0 8px; + padding: 0 2px; } } } diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 551b28b8b3..21152aa007 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -203,3 +203,53 @@ flex-grow: 1; text-align: right; } + +// fullscreen composer styles +.fullscreen-composer { + overflow: hidden; + + .profiler-results { + display: none; + } + #reply-control { + &.fullscreen { + // important needed because of inline styles when height is changed manually with grippie + height: 100vh !important; + z-index: z("header") + 1; + .d-editor-preview-wrapper { + margin-top: 1%; + } + .reply-to { + border-bottom: 1px solid $primary-low; + padding-bottom: 3px; + margin: 0; + .composer-controls { + margin-right: 0; + } + } + .d-editor-textarea-wrapper { + border: none; + } + &.show-preview .d-editor-textarea-wrapper { + border-right: 1px solid $primary-low; + } + #draft-status, + #file-uploading { + margin-left: 0; + text-align: initial; + } + .composer-popup { + top: 30px; + } + &:before { + content: ""; + background: $secondary; + width: 100%; + height: 100%; + position: fixed; + z-index: -1; + left: 0; + } + } + } +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 157b35bb0b..806b24ad8c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1427,6 +1427,8 @@ en: help: "Markdown Editing Help" collapse: "minimize the composer panel" abandon: "close composer and discard draft" + enter_fullscreen: "enter fullscreen composer" + exit_fullscreen: "exit fullscreen composer" modal_ok: "OK" modal_cancel: "Cancel" cant_send_pm: "Sorry, you can't send a message to %{username}." @@ -2613,6 +2615,7 @@ en: composing: title: 'Composing' return: 'shift+c Return to composer' + fullscreen: 'shift+F11 Fullscreen composer' actions: title: 'Actions' bookmark_topic: 'f Toggle bookmark topic' From 5ae4cbcf88d7a67b02fcf7601e491d1e43f5c7a0 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 15 Oct 2018 11:15:31 +0800 Subject: [PATCH 013/209] DEV: Clear `ColorScheme.hex_cache` to avoid leaking state. --- spec/models/color_scheme_color_spec.rb | 4 ++++ spec/models/color_scheme_spec.rb | 4 ++++ spec/models/topic_spec.rb | 9 +++++++-- spec/rails_helper.rb | 1 - 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/spec/models/color_scheme_color_spec.rb b/spec/models/color_scheme_color_spec.rb index 95830f4409..933698255e 100644 --- a/spec/models/color_scheme_color_spec.rb +++ b/spec/models/color_scheme_color_spec.rb @@ -1,6 +1,10 @@ require 'rails_helper' describe ColorSchemeColor do + after do + ColorScheme.hex_cache.clear + end + def test_invalid_hex(hex) c = described_class.new(hex: hex) expect(c).not_to be_valid diff --git a/spec/models/color_scheme_spec.rb b/spec/models/color_scheme_spec.rb index 974f9aae1f..fcc23866b0 100644 --- a/spec/models/color_scheme_spec.rb +++ b/spec/models/color_scheme_spec.rb @@ -1,8 +1,12 @@ require 'rails_helper' describe ColorScheme do + after do + ColorScheme.hex_cache.clear + end let(:valid_params) { { name: "Best Colors Evar", colors: valid_colors } } + let(:valid_colors) { [ { name: '$primary_background_color', hex: 'FFBB00' }, { name: '$secondary_background_color', hex: '888888' } diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 5e28d0d618..2e822abd50 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1040,9 +1040,14 @@ describe Topic do it "resets the topic archetype" do topic.expects(:add_moderator_post) - MessageBus.expects(:publish).with("/site/banner", nil) - topic.remove_banner!(user) + + message = MessageBus.track_publish do + topic.remove_banner!(user) + end.first + expect(topic.archetype).to eq(Archetype.default) + expect(message.channel).to eq("/site/banner") + expect(message.data).to eq(nil) end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 2deb20cf91..2a78230fbe 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -134,7 +134,6 @@ RSpec.configure do |config| unfreeze_time ActionMailer::Base.deliveries.clear - ColorScheme.hex_cache.clear raise if ActiveRecord::Base.connection_pool.stat[:busy] > 1 end From aa60936115f1f4dbaa2274b03b1198c4ef28a11b Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 15 Oct 2018 11:42:45 +0800 Subject: [PATCH 014/209] DEV: Add order to avoid randomly failing test. --- app/controllers/users_controller.rb | 4 +--- spec/requests/users_controller_spec.rb | 9 +++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8570540d5c..f3c837bf30 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1130,9 +1130,7 @@ class UsersController < ApplicationController MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id] - render json: { - success: true - } + render json: success_json end private diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index e64044ac07..67c96d6bde 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -3265,13 +3265,14 @@ describe UsersController do context 'while logged in' do before do - sign_in(user) - sign_in(user) + 2.times { sign_in(user) } end it 'logs user out' do - ids = user.user_auth_tokens.map { |token| token.id } - post "/u/#{user.username}/preferences/revoke-auth-token.json", params: { token_id: ids[0] } + ids = user.user_auth_tokens.order(:created_at).pluck(:id) + + post "/u/#{user.username}/preferences/revoke-auth-token.json", + params: { token_id: ids[0] } expect(response.status).to eq(200) From 6acdea37c4e77723c774152e9076dfedda99493a Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Mon, 15 Oct 2018 12:55:23 +0800 Subject: [PATCH 015/209] DEV: extract inline js when baking theme fields (#6447) * extract inline js when baking theme fields * destroy javascript cache when destroying theme fields This work is needed to support CSP work --- app/controllers/javascripts_controller.rb | 65 +++++++++++++++++++ app/models/javascript_cache.rb | 39 +++++++++++ app/models/theme_field.rb | 46 ++++++++----- config/routes.rb | 1 + ...20180927135248_create_javascript_caches.rb | 10 +++ spec/models/javascript_cache_spec.rb | 32 +++++++++ spec/models/theme_field_spec.rb | 40 ++++++++++-- spec/models/theme_spec.rb | 41 +++++++----- spec/requests/admin/themes_controller_spec.rb | 18 +++++ spec/requests/javascripts_controller_spec.rb | 54 +++++++++++++++ 10 files changed, 306 insertions(+), 40 deletions(-) create mode 100644 app/controllers/javascripts_controller.rb create mode 100644 app/models/javascript_cache.rb create mode 100644 db/migrate/20180927135248_create_javascript_caches.rb create mode 100644 spec/models/javascript_cache_spec.rb create mode 100644 spec/requests/javascripts_controller_spec.rb diff --git a/app/controllers/javascripts_controller.rb b/app/controllers/javascripts_controller.rb new file mode 100644 index 0000000000..5e861a3e65 --- /dev/null +++ b/app/controllers/javascripts_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true +class JavascriptsController < ApplicationController + DISK_CACHE_PATH = "#{Rails.root}/tmp/javascript-cache" + + skip_before_action( + :check_xhr, + :handle_theme, + :preload_json, + :redirect_to_login_if_required, + :verify_authenticity_token, + only: [:show] + ) + + before_action :is_asset_path, :no_cookies, only: [:show] + + def show + raise Discourse::NotFound unless last_modified.present? + return render body: nil, status: 304 if not_modified? + + # Security: safe due to route constraint + cache_file = "#{DISK_CACHE_PATH}/#{params[:digest]}.js" + + unless File.exist?(cache_file) + content = query.pluck(:content).first + raise Discourse::NotFound if content.nil? + + FileUtils.mkdir_p(DISK_CACHE_PATH) + File.write(cache_file, content) + end + + set_cache_control_headers + send_file(cache_file, disposition: :inline) + end + + private + + def query + @query ||= JavascriptCache.where(digest: params[:digest]).limit(1) + end + + def last_modified + @last_modified ||= query.pluck(:updated_at).first + end + + def not_modified? + cache_time = + begin + Time.rfc2822(request.env["HTTP_IF_MODIFIED_SINCE"]) + rescue ArgumentError + nil + end + + cache_time && last_modified && last_modified <= cache_time + end + + def set_cache_control_headers + if Rails.env.development? + response.headers['Last-Modified'] = Time.zone.now.httpdate + immutable_for(1.second) + else + response.headers['Last-Modified'] = last_modified.httpdate if last_modified + immutable_for(1.year) + end + end +end diff --git a/app/models/javascript_cache.rb b/app/models/javascript_cache.rb new file mode 100644 index 0000000000..fd0bbfa735 --- /dev/null +++ b/app/models/javascript_cache.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +class JavascriptCache < ActiveRecord::Base + belongs_to :theme_field + + validate :content_cannot_be_nil + + before_save :update_digest + + def url + "#{GlobalSetting.cdn_url}#{GlobalSetting.relative_url_root}/javascripts/#{digest}.js" + end + + private + + def update_digest + self.digest = Digest::SHA1.hexdigest(content) if content_changed? + end + + def content_cannot_be_nil + errors.add(:content, :empty) if content.nil? + end +end + +# == Schema Information +# +# Table name: javascript_caches +# +# id :bigint(8) not null, primary key +# theme_field_id :bigint(8) not null +# digest :string +# content :text not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_javascript_caches_on_digest (digest) +# index_javascript_caches_on_theme_field_id (theme_field_id) +# diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index c4d6ab4e94..84a6b9b561 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -3,6 +3,7 @@ require_dependency 'theme_settings_parser' class ThemeField < ActiveRecord::Base belongs_to :upload + has_one :javascript_cache, dependent: :destroy scope :find_by_theme_ids, ->(theme_ids) { return none unless theme_ids.present? @@ -68,6 +69,8 @@ PLUGIN_API_JS def process_html(html) errors = nil + javascript_cache || build_javascript_cache + javascript_cache.content = '' doc = Nokogiri::HTML.fragment(html) doc.css('script[type="text/x-handlebars"]').each do |node| @@ -83,43 +86,52 @@ PLUGIN_API_JS if is_raw template = "requirejs('discourse-common/lib/raw-handlebars').template(#{Barber::Precompiler.compile(hbs_template)})" - node.replace < - (function() { - if ('Discourse' in window) { + javascript_cache.content << < + } + })(); COMPILED else template = "Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(hbs_template)})" - node.replace < - (function() { - if ('Em' in window) { + javascript_cache.content << < + } + })(); COMPILED end + node.remove end doc.css('script[type="text/discourse-plugin"]').each do |node| if node['version'].present? begin - code = transpile(node.inner_html, node['version']) - node.replace("") + javascript_cache.content << transpile(node.inner_html, node['version']) rescue MiniRacer::RuntimeError => ex - node.replace("") + javascript_cache.content << "console.error('Theme Transpilation Error:', #{ex.message.inspect});" + errors ||= [] errors << ex.message end + + node.remove end end + doc.css('script').each do |node| + next if node['src'].present? + + javascript_cache.content << "(function() { #{node.inner_html} })();" + node.remove + end + + javascript_cache.save! + + doc.add_child("") if javascript_cache.content.present? [doc.to_s, errors&.join("\n")] end diff --git a/config/routes.rb b/config/routes.rb index 1d6caab2b0..f4769f67a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -450,6 +450,7 @@ Discourse::Application.routes.draw do get "stylesheets/:name.css.map" => "stylesheets#show_source_map", constraints: { name: /[-a-z0-9_]+/ } get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ } + get "javascripts/:digest.js" => "javascripts#show", constraints: { digest: /\h{40}/ } post "uploads" => "uploads#create" post "uploads/lookup-urls" => "uploads#lookup_urls" diff --git a/db/migrate/20180927135248_create_javascript_caches.rb b/db/migrate/20180927135248_create_javascript_caches.rb new file mode 100644 index 0000000000..c2b3e83d12 --- /dev/null +++ b/db/migrate/20180927135248_create_javascript_caches.rb @@ -0,0 +1,10 @@ +class CreateJavascriptCaches < ActiveRecord::Migration[5.2] + def change + create_table :javascript_caches do |t| + t.references :theme_field, null: false + t.string :digest, null: true, index: true + t.text :content, null: false + t.timestamps + end + end +end diff --git a/spec/models/javascript_cache_spec.rb b/spec/models/javascript_cache_spec.rb new file mode 100644 index 0000000000..1599fbe92e --- /dev/null +++ b/spec/models/javascript_cache_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe JavascriptCache, type: :model do + let(:theme) { Fabricate(:theme) } + let(:theme_field) { ThemeField.create!(theme: theme, target_id: 0, name: "header", value: "html") } + + describe '#save' do + it 'updates the digest only if the content has changed' do + javascript_cache = JavascriptCache.create!(content: 'console.log("hello");', theme_field: theme_field) + expect(javascript_cache.digest).to_not be_empty + + expect { javascript_cache.save! }.to_not change { javascript_cache.reload.digest } + + expect do + javascript_cache.content = 'console.log("world");' + javascript_cache.save! + end.to change { javascript_cache.reload.digest } + end + + it 'allows content to be empty, but not nil' do + javascript_cache = JavascriptCache.create!(content: 'console.log("hello");', theme_field: theme_field) + + javascript_cache.content = '' + expect(javascript_cache.valid?).to eq(true) + + javascript_cache.content = nil + expect(javascript_cache.valid?).to eq(false) + expect(javascript_cache.errors.details[:content]).to include(error: :empty) + end + end +end diff --git a/spec/models/theme_field_spec.rb b/spec/models/theme_field_spec.rb index 152531d8d1..89aa19ebfb 100644 --- a/spec/models/theme_field_spec.rb +++ b/spec/models/theme_field_spec.rb @@ -27,7 +27,31 @@ describe ThemeField do end end - it "correctly generates errors for transpiled js" do + it 'does not insert a script tag when there are no inline script' do + theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "body_tag", value: '
    new div
    ') + expect(theme_field.value_baked).to_not include(' + var a = "inline discourse plugin"; + + + +HTML + + theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html) + + expect(theme_field.value_baked).to include("") + expect(theme_field.value_baked).to include("external-script.js") + expect(theme_field.javascript_cache.content).to include('inline discourse plugin') + expect(theme_field.javascript_cache.content).to include('inline raw script') + end + + it "correctly extracts and generates errors for transpiled js" do html = < badJavaScript(; @@ -36,6 +60,8 @@ HTML field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html) expect(field.error).not_to eq(nil) + expect(field.value_baked).to include("") + expect(field.javascript_cache.content).to include("Theme Transpilation Error:") field.update!(value: '') expect(field.error).to eq(nil) @@ -49,12 +75,14 @@ HTML HTML ThemeField.create!(theme_id: 1, target_id: 3, name: "yaml", value: "string_setting: \"test text \\\" 123!\"") - baked_value = ThemeField.create!(theme_id: 1, target_id: 0, name: "head_tag", value: html).value_baked + theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "head_tag", value: html) + javascript_cache = theme_field.javascript_cache - expect(baked_value).to include("testing-div") - expect(baked_value).to include("theme-setting-injector") - expect(baked_value).to include("string_setting") - expect(baked_value).to include("test text \\\\\\\\u0022 123!") + expect(theme_field.value_baked).to include("") + expect(javascript_cache.content).to include("testing-div") + expect(javascript_cache.content).to include("theme-setting-injector") + expect(javascript_cache.content).to include("string_setting") + expect(javascript_cache.content).to include("test text \\\\\\\\u0022 123!") end it "correctly generates errors for transpiled css" do diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 15ad9318ca..9b7211de7a 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -120,10 +120,12 @@ HTML theme.set_field(target: :common, name: "header", value: with_template) theme.save! + field = theme.theme_fields.find_by(target_id: Theme.targets[:common], name: 'header') baked = Theme.lookup_field(theme.id, :mobile, "header") - expect(baked).to match(/HTMLBars/) - expect(baked).to match(/raw-handlebars/) + expect(baked).to include(field.javascript_cache.url) + expect(field.javascript_cache.content).to include('HTMLBars') + expect(field.javascript_cache.content).to include('raw-handlebars') end it 'should create body_tag_baked on demand if needed' do @@ -214,7 +216,7 @@ HTML context "plugin api" do def transpile(html) f = ThemeField.create!(target_id: Theme.targets[:mobile], theme_id: 1, name: "after_header", value: html) - f.value_baked + return f.value_baked, f.javascript_cache end it "transpiles ES6 code" do @@ -224,10 +226,10 @@ HTML HTML - transpiled = transpile(html) - expect(transpiled).to match(/\/) - expect(transpiled).to match(/var x = 1;/) - expect(transpiled).to match(/_registerPluginCode\('0.1'/) + baked, javascript_cache = transpile(html) + expect(baked).to include(javascript_cache.url) + expect(javascript_cache.content).to include('var x = 1;') + expect(javascript_cache.content).to include("_registerPluginCode('0.1'") end it "converts errors to a script type that is not evaluated" do @@ -238,9 +240,10 @@ HTML HTML - transpiled = transpile(html) - expect(transpiled).to match(/text\/discourse-js-error/) - expect(transpiled).to match(/read-only/) + baked, javascript_cache = transpile(html) + expect(baked).to include(javascript_cache.url) + expect(javascript_cache.content).to include('Theme Transpilation Error') + expect(javascript_cache.content).to include('read-only') end end @@ -319,33 +322,37 @@ HTML it "allows values to be used in JS" do theme.set_field(target: :settings, name: :yaml, value: "name: bob") - theme.set_field(target: :common, name: :after_header, value: '') + theme_field = theme.set_field(target: :common, name: :after_header, value: '') theme.save! transpiled = <<~HTML - + } HTML - expect(Theme.lookup_field(theme.id, :desktop, :after_header)).to eq(transpiled.strip) + theme_field.reload + expect(Theme.lookup_field(theme.id, :desktop, :after_header)).to include(theme_field.javascript_cache.url) + expect(theme_field.javascript_cache.content).to eq(transpiled.strip) setting = theme.settings.find { |s| s.name == :name } setting.value = 'bill' transpiled = <<~HTML - + } HTML - expect(Theme.lookup_field(theme.id, :desktop, :after_header)).to eq(transpiled.strip) + theme_field.reload + expect(Theme.lookup_field(theme.id, :desktop, :after_header)).to include(theme_field.javascript_cache.url) + expect(theme_field.javascript_cache.content).to eq(transpiled.strip) end end diff --git a/spec/requests/admin/themes_controller_spec.rb b/spec/requests/admin/themes_controller_spec.rb index cff9336735..2d9d34d54d 100644 --- a/spec/requests/admin/themes_controller_spec.rb +++ b/spec/requests/admin/themes_controller_spec.rb @@ -210,4 +210,22 @@ describe Admin::ThemesController do expect(JSON.parse(response.body)["errors"].first).to include(I18n.t("themes.errors.component_no_default")) end end + + describe '#destroy' do + let(:theme) { Fabricate(:theme) } + + it "deletes the field's javascript cache" do + theme.set_field(target: :common, name: :header, value: '') + theme.save! + + javascript_cache = theme.theme_fields.find_by(target_id: Theme.targets[:common], name: :header).javascript_cache + expect(javascript_cache).to_not eq(nil) + + delete "/admin/themes/#{theme.id}.json" + + expect(response.status).to eq(204) + expect { theme.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { javascript_cache.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end end diff --git a/spec/requests/javascripts_controller_spec.rb b/spec/requests/javascripts_controller_spec.rb new file mode 100644 index 0000000000..a0a985a63b --- /dev/null +++ b/spec/requests/javascripts_controller_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +require 'rails_helper' + +describe JavascriptsController do + let(:theme) { Fabricate(:theme) } + let(:theme_field) { ThemeField.create!(theme: theme, target_id: 0, name: "header", value: "html") } + let(:javascript_cache) { JavascriptCache.create!(content: 'console.log("hello");', theme_field: theme_field) } + + describe '#show' do + def update_digest_and_get(digest) + # actually set digest to make sure 404 is raised by router + javascript_cache.update_attributes(digest: digest) + + get "/javascripts/#{digest}.js" + end + + it 'only accepts 40-char hexdecimal digest name' do + update_digest_and_get('0123456789abcdefabcd0123456789abcdefabcd') + expect(response.status).to eq(200) + + update_digest_and_get('0123456789abcdefabcd0123456789abcdefabc') + expect(response.status).to eq(404) + + update_digest_and_get('gggggggggggggggggggggggggggggggggggggggg') + expect(response.status).to eq(404) + + update_digest_and_get('0123456789abcdefabc_0123456789abcdefabcd') + expect(response.status).to eq(404) + + update_digest_and_get('0123456789abcdefabc-0123456789abcdefabcd') + expect(response.status).to eq(404) + + update_digest_and_get('../../Gemfile') + expect(response.status).to eq(404) + end + + it 'considers the database record as the source of truth' do + clear_disk_cache + + get "/javascripts/#{javascript_cache.digest}.js" + expect(response.status).to eq(200) + expect(response.body).to eq(javascript_cache.content) + + javascript_cache.destroy! + + get "/javascripts/#{javascript_cache.digest}.js" + expect(response.status).to eq(404) + end + + def clear_disk_cache + `rm #{JavascriptsController::DISK_CACHE_PATH}/*` + end + end +end From 27e732a58d92b051ce36ca6886a119581c335a59 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 15 Oct 2018 07:03:53 +0200 Subject: [PATCH 016/209] FEATURE: allow multiple secrets for Discourse SSO provider This splits off the logic between SSO keys used incoming vs outgoing, it allows to far better restrict who is allowed to log in using a site. This allows for better auditing of the SSO provider feature --- .../admin/components/secret-value-list.js.es6 | 87 +++++ .../admin/mixins/setting-component.js.es6 | 3 +- .../components/secret-value-list.hbs | 22 ++ .../components/site-settings/secret-list.hbs | 3 + .../stylesheets/common/admin/admin_base.scss | 54 ++- app/controllers/session_controller.rb | 2 +- app/services/wildcard_domain_checker.rb | 10 + config/locales/server.en.yml | 6 + config/site_settings.yml | 7 + ...d_sso_provider_secrets_to_site_settings.rb | 12 + lib/single_sign_on.rb | 27 +- lib/site_setting_extension.rb | 9 +- spec/requests/session_controller_spec.rb | 349 +++++++----------- spec/services/wildcard_domain_checker_spec.rb | 35 ++ .../components/secret-value-list-test.js.es6 | 63 ++++ 15 files changed, 459 insertions(+), 230 deletions(-) create mode 100644 app/assets/javascripts/admin/components/secret-value-list.js.es6 create mode 100644 app/assets/javascripts/admin/templates/components/secret-value-list.hbs create mode 100644 app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs create mode 100644 app/services/wildcard_domain_checker.rb create mode 100644 db/migrate/20181005084357_add_sso_provider_secrets_to_site_settings.rb create mode 100644 spec/services/wildcard_domain_checker_spec.rb create mode 100644 test/javascripts/components/secret-value-list-test.js.es6 diff --git a/app/assets/javascripts/admin/components/secret-value-list.js.es6 b/app/assets/javascripts/admin/components/secret-value-list.js.es6 new file mode 100644 index 0000000000..939ba45c9e --- /dev/null +++ b/app/assets/javascripts/admin/components/secret-value-list.js.es6 @@ -0,0 +1,87 @@ +import { on } from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + classNameBindings: [":value-list", ":secret-value-list"], + inputInvalidKey: Ember.computed.empty("newKey"), + inputInvalidSecret: Ember.computed.empty("newSecret"), + inputDelimiter: null, + collection: null, + values: null, + + @on("didReceiveAttrs") + _setupCollection() { + const values = this.get("values"); + + this.set( + "collection", + this._splitValues(values, this.get("inputDelimiter") || "\n") + ); + }, + + actions: { + changeKey(index, newValue) { + this._replaceValue(index, newValue, "key"); + }, + + changeSecret(index, newValue) { + this._replaceValue(index, newValue, "secret"); + }, + + addValue() { + if (this.get("inputInvalidKey") || this.get("inputInvalidSecret")) return; + this._addValue(this.get("newKey"), this.get("newSecret")); + this.setProperties({ newKey: "", newSecret: "" }); + }, + + removeValue(value) { + this._removeValue(value); + } + }, + + _addValue(value, secret) { + this.get("collection").addObject({ key: value, secret: secret }); + this._saveValues(); + }, + + _removeValue(value) { + const collection = this.get("collection"); + collection.removeObject(value); + this._saveValues(); + }, + + _replaceValue(index, newValue, keyName) { + let item = this.get("collection")[index]; + Ember.set(item, keyName, newValue); + + this._saveValues(); + }, + + _saveValues() { + this.set( + "values", + this.get("collection") + .map(function(elem) { + return `${elem.key}|${elem.secret}`; + }) + .join("\n") + ); + }, + + _splitValues(values, delimiter) { + if (values && values.length) { + const keys = ["key", "secret"]; + var res = []; + values.split(delimiter).forEach(function(str) { + var object = {}; + str.split("|").forEach(function(a, i) { + object[keys[i]] = a; + }); + res.push(object); + }); + + return res; + } else { + return []; + } + } +}); diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6 index 532abe8f20..9408bfbceb 100644 --- a/app/assets/javascripts/admin/mixins/setting-component.js.es6 +++ b/app/assets/javascripts/admin/mixins/setting-component.js.es6 @@ -11,7 +11,8 @@ const CUSTOM_TYPES = [ "value_list", "category", "uploaded_image_list", - "compact_list" + "compact_list", + "secret_list" ]; export default Ember.Mixin.create({ diff --git a/app/assets/javascripts/admin/templates/components/secret-value-list.hbs b/app/assets/javascripts/admin/templates/components/secret-value-list.hbs new file mode 100644 index 0000000000..7058504fac --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/secret-value-list.hbs @@ -0,0 +1,22 @@ +{{#if collection}} +
    + {{#each collection as |value index|}} +
    + {{d-button action="removeValue" + actionParam=value + icon="times" + class="remove-value-btn btn-small"}} + {{input value=value.key class="value-input" focus-out=(action "changeKey" index)}} + {{input value=value.secret class="value-input" focus-out=(action "changeSecret" index) type="password"}} +
    + {{/each}} +
    +{{/if}} + +
    + {{text-field value=newKey class="new-value-input key" placeholder=setting.placeholder.key}} + {{input type="password" value=newSecret class="new-value-input secret" placeholder=setting.placeholder.value}} + {{d-button action="addValue" + icon="plus" + class="add-value-btn btn-small"}} +
    diff --git a/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs new file mode 100644 index 0000000000..1e71c18c63 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs @@ -0,0 +1,3 @@ +{{secret-value-list setting=setting values=value}} +{{setting-validation-message message=validationMessage}} +
    {{{unbound setting.description}}}
    diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 69269d1f12..24721a26be 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -865,6 +865,17 @@ table#user-badges { } } +@mixin value-btn { + width: 29px; + border: 1px solid $primary-low; + outline: none; + padding: 0; + + &:focus { + border-color: $tertiary; + } +} + .value-list { .value { padding: 0.125em 0; @@ -891,15 +902,8 @@ table#user-badges { } .remove-value-btn { + @include value-btn; margin-right: 0.25em; - width: 29px; - border: 1px solid $primary-low; - outline: none; - padding: 0; - - &:focus { - border-color: $tertiary; - } } } .values { @@ -907,6 +911,40 @@ table#user-badges { } } +.secret-value-list { + .value { + flex-flow: row wrap; + margin-left: -0.25em; + margin-top: -0.125em; + .new-value-input { + flex: 1; + } + .value-input, + .new-value-input { + margin-top: 0.125em; + &:last-of-type { + margin-left: 0.25em; + } + } + .remove-value-btn { + margin-left: 0.25em; + margin-top: 0.125em; + } + .add-value-btn { + @include value-btn; + margin-left: 0.25em; + margin-top: 0.125em; + } + &:last-of-type { + .new-value-input { + &:first-of-type { + margin-left: 0.25em; + } + } + } + } +} + // Mobile view text-inputs need some padding .mobile-view .admin-contents { input[type="text"] { diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index b523943e8d..af1e761a65 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -46,7 +46,7 @@ class SessionController < ApplicationController payload ||= request.query_string if SiteSetting.enable_sso_provider - sso = SingleSignOn.parse(payload, SiteSetting.sso_secret) + sso = SingleSignOn.parse(payload) if sso.return_sso_url.blank? render plain: "return_sso_url is blank, it must be provided", status: 400 diff --git a/app/services/wildcard_domain_checker.rb b/app/services/wildcard_domain_checker.rb new file mode 100644 index 0000000000..3f91811f07 --- /dev/null +++ b/app/services/wildcard_domain_checker.rb @@ -0,0 +1,10 @@ +module WildcardDomainChecker + + def self.check_domain(domain, external_domain) + escaped_domain = domain[0] == "*" ? Regexp.escape(domain).sub("\\*", '\S*') : Regexp.escape(domain) + domain_regex = Regexp.new("^#{escaped_domain}$", 'i') + + external_domain.match(domain_regex) + end + +end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 6a5e45265a..2606cee026 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1319,6 +1319,7 @@ en: enable_sso_provider: "Implement Discourse SSO provider protocol at the /session/sso_provider endpoint, requires sso_secret to be set" sso_url: "URL of single sign on endpoint (must include http:// or https://)" sso_secret: "Secret string used to cryptographically authenticate SSO information, be sure it is 10 characters or longer" + sso_provider_secrets: "A list of domain-secret pairs that are using Discourse as a SSO provider. Make sure SSO secret is 10 characters or longer. Wildcard symbol * can be used to match any domain or only a part of it (e.g. *.example.com)." sso_overrides_bio: "Overrides user bio in user profile and prevents user from changing it" sso_overrides_groups: "Synchronize all manual group membership with groups specified in the groups sso attribute (WARNING: if you do not specify groups all manual group membership will be cleared for user)" sso_overrides_email: "Overrides local email with external site email from SSO payload on every login, and prevent local changes. (WARNING: discrepancies can occur due to normalization of local emails)" @@ -1862,6 +1863,11 @@ en: max_username_length_exists: "You cannot set the maximum username length below the longest username (%{username})." max_username_length_range: "You cannot set the maximum below the minimum." + placeholder: + sso_provider_secrets: + key: "www.example.com" + value: "SSO secret" + search: within_post: "#%{post_number} by %{username}" types: diff --git a/config/site_settings.yml b/config/site_settings.yml index 1a882a9c1e..c98dc0610d 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -341,6 +341,13 @@ login: sso_secret: default: '' secret: true + sso_provider_secrets: + default: '' + type: list + list_type: secret + placeholder: + key: "sso_provider.key_placeholder" + value: "sso_provider.value_placeholder" sso_overrides_groups: false sso_overrides_bio: false sso_overrides_email: diff --git a/db/migrate/20181005084357_add_sso_provider_secrets_to_site_settings.rb b/db/migrate/20181005084357_add_sso_provider_secrets_to_site_settings.rb new file mode 100644 index 0000000000..4e95023b43 --- /dev/null +++ b/db/migrate/20181005084357_add_sso_provider_secrets_to_site_settings.rb @@ -0,0 +1,12 @@ +class AddSsoProviderSecretsToSiteSettings < ActiveRecord::Migration[5.2] + def up + return unless SiteSetting.enable_sso_provider && SiteSetting.sso_secret.present? + sso_secret = SiteSetting.sso_secret + execute "INSERT INTO site_settings(name, data_type, value, created_at, updated_at) + VALUES ('sso_provider_secrets', 8, '*|#{sso_secret}', now(), now())" + end + + def down + execute "DELETE FROM site_settings WHERE name = 'sso_provider_secrets'" + end +end diff --git a/lib/single_sign_on.rb b/lib/single_sign_on.rb index 6900b633b2..f5a59e26a6 100644 --- a/lib/single_sign_on.rb +++ b/lib/single_sign_on.rb @@ -50,9 +50,14 @@ class SingleSignOn def self.parse(payload, sso_secret = nil) sso = new - sso.sso_secret = sso_secret if sso_secret parsed = Rack::Utils.parse_query(payload) + decoded = Base64.decode64(parsed["sso"]) + decoded_hash = Rack::Utils.parse_query(decoded) + + return_sso_url = decoded_hash['return_sso_url'] + sso.sso_secret = sso_secret || (provider_secret(return_sso_url) if return_sso_url) + if sso.sign(parsed["sso"]) != parsed["sig"] diags = "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}" if parsed["sso"] =~ /[^a-zA-Z0-9=\r\n\/+]/m @@ -83,6 +88,17 @@ class SingleSignOn sso end + def self.provider_secret(return_sso_url) + provider_secrets = SiteSetting.sso_provider_secrets.split(/[\|,\n]/) + provider_secrets_hash = Hash[*provider_secrets] + return_url_host = URI.parse(return_sso_url).host + + secret = provider_secrets_hash.select do |domain, _| + WildcardDomainChecker.check_domain(domain, return_url_host) + end + secret.present? ? secret.values.first : nil + end + def diagnostics SingleSignOn::ACCESSORS.map { |a| "#{a}: #{send(a)}" }.join("\n") end @@ -99,8 +115,9 @@ class SingleSignOn @custom_fields ||= {} end - def sign(payload) - OpenSSL::HMAC.hexdigest("sha256", sso_secret, payload) + def sign(payload, provider_secret = nil) + secret = provider_secret || sso_secret + OpenSSL::HMAC.hexdigest("sha256", secret, payload) end def to_url(base_url = nil) @@ -108,9 +125,9 @@ class SingleSignOn "#{base}#{base.include?('?') ? '&' : '?'}#{payload}" end - def payload + def payload(provider_secret = nil) payload = Base64.strict_encode64(unsigned_payload) - "sso=#{CGI::escape(payload)}&sig=#{sign(payload)}" + "sso=#{CGI::escape(payload)}&sig=#{sign(payload, provider_secret)}" end def unsigned_payload diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 490cc26b6d..9122da94d9 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -220,7 +220,8 @@ module SiteSettingExtension value: value.to_s, category: categories[s], preview: previews[s], - secret: secret_settings.include?(s) + secret: secret_settings.include?(s), + placeholder: placeholder(s) }.merge(type_supervisor.type_hash(s)) opts @@ -231,6 +232,12 @@ module SiteSettingExtension I18n.t("site_settings.#{setting}") end + def placeholder(setting) + if !I18n.t("site_settings.placeholder.#{setting}", default: "").empty? + I18n.t("site_settings.placeholder.#{setting}") + end + end + def self.client_settings_cache_key # NOTE: we use the git version in the key to ensure # that we don't end up caching the incorrect version diff --git a/spec/requests/session_controller_spec.rb b/spec/requests/session_controller_spec.rb index 8a540829d0..2f8b8a4bf7 100644 --- a/spec/requests/session_controller_spec.rb +++ b/spec/requests/session_controller_spec.rb @@ -521,153 +521,6 @@ RSpec.describe SessionController do expect(response.status).to eq(419) end - describe 'can act as an SSO provider' do - before do - stub_request(:any, /#{Discourse.current_hostname}\/uploads/).to_return( - status: 200, - body: lambda { |request| file_from_fixtures("logo.png") } - ) - - SiteSetting.enable_sso_provider = true - SiteSetting.enable_sso = false - SiteSetting.enable_local_logins = true - SiteSetting.sso_secret = "topsecret" - - @sso = SingleSignOn.new - @sso.nonce = "mynonce" - @sso.sso_secret = SiteSetting.sso_secret - @sso.return_sso_url = "http://somewhere.over.rainbow/sso" - - @user = Fabricate(:user, password: "myfrogs123ADMIN", active: true, admin: true) - group = Fabricate(:group) - group.add(@user) - - @user.create_user_avatar! - UserAvatar.import_url_for_user(logo_fixture, @user) - UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: false) - UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: true) - - @user.reload - @user.user_avatar.reload - @user.user_profile.reload - EmailToken.update_all(confirmed: true) - end - - it "successfully logs in and redirects user to return_sso_url when the user is not logged in" do - get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload), headers: headers - - expect(response).to redirect_to("/login") - - post "/session.json", - params: { login: @user.username, password: "myfrogs123ADMIN" }, xhr: true - location = response.cookies["sso_destination_url"] - # javascript code will handle redirection of user to return_sso_url - expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/) - - payload = location.split("?")[1] - sso2 = SingleSignOn.parse(payload, "topsecret") - - expect(sso2.email).to eq(@user.email) - expect(sso2.name).to eq(@user.name) - expect(sso2.username).to eq(@user.username) - expect(sso2.external_id).to eq(@user.id.to_s) - expect(sso2.admin).to eq(true) - expect(sso2.moderator).to eq(false) - expect(sso2.groups).to eq(@user.groups.pluck(:name).join(",")) - - expect(sso2.avatar_url.blank?).to_not eq(true) - expect(sso2.profile_background_url.blank?).to_not eq(true) - expect(sso2.card_background_url.blank?).to_not eq(true) - - expect(sso2.avatar_url).to start_with(Discourse.base_url) - expect(sso2.profile_background_url).to start_with(Discourse.base_url) - expect(sso2.card_background_url).to start_with(Discourse.base_url) - end - - it "successfully redirects user to return_sso_url when the user is logged in" do - sign_in(@user) - - get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload), headers: headers - - location = response.header["Location"] - expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/) - - payload = location.split("?")[1] - sso2 = SingleSignOn.parse(payload, "topsecret") - - expect(sso2.email).to eq(@user.email) - expect(sso2.name).to eq(@user.name) - expect(sso2.username).to eq(@user.username) - expect(sso2.external_id).to eq(@user.id.to_s) - expect(sso2.admin).to eq(true) - expect(sso2.moderator).to eq(false) - expect(sso2.groups).to eq(@user.groups.pluck(:name).join(",")) - - expect(sso2.avatar_url.blank?).to_not eq(true) - expect(sso2.profile_background_url.blank?).to_not eq(true) - expect(sso2.card_background_url.blank?).to_not eq(true) - - expect(sso2.avatar_url).to start_with(Discourse.base_url) - expect(sso2.profile_background_url).to start_with(Discourse.base_url) - expect(sso2.card_background_url).to start_with(Discourse.base_url) - end - - it 'handles non local content correctly' do - SiteSetting.avatar_sizes = "100|49" - SiteSetting.enable_s3_uploads = true - SiteSetting.s3_access_key_id = "XXX" - SiteSetting.s3_secret_access_key = "XXX" - SiteSetting.s3_upload_bucket = "test" - SiteSetting.s3_cdn_url = "http://cdn.com" - - stub_request(:any, /test.s3.dualstack.us-east-1.amazonaws.com/).to_return(status: 200, body: "", headers: {}) - - @user.create_user_avatar! - upload = Fabricate(:upload, url: "//test.s3.dualstack.us-east-1.amazonaws.com/something") - - Fabricate(:optimized_image, - sha1: SecureRandom.hex << "A" * 8, - upload: upload, - width: 98, - height: 98, - url: "//test.s3.amazonaws.com/something/else" - ) - - @user.update_columns(uploaded_avatar_id: upload.id) - @user.user_profile.update_columns( - profile_background: "//test.s3.dualstack.us-east-1.amazonaws.com/something", - card_background: "//test.s3.dualstack.us-east-1.amazonaws.com/something" - ) - - @user.reload - @user.user_avatar.reload - @user.user_profile.reload - - sign_in(@user) - - stub_request(:get, "http://cdn.com/something/else").to_return( - body: lambda { |request| File.new(Rails.root + 'spec/fixtures/images/logo.png') } - ) - - get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload), headers: headers - - location = response.header["Location"] - # javascript code will handle redirection of user to return_sso_url - expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/) - - payload = location.split("?")[1] - sso2 = SingleSignOn.parse(payload, "topsecret") - - expect(sso2.avatar_url.blank?).to_not eq(true) - expect(sso2.profile_background_url.blank?).to_not eq(true) - expect(sso2.card_background_url.blank?).to_not eq(true) - - expect(sso2.avatar_url).to start_with("#{SiteSetting.s3_cdn_url}/original") - expect(sso2.profile_background_url).to start_with(SiteSetting.s3_cdn_url) - expect(sso2.card_background_url).to start_with(SiteSetting.s3_cdn_url) - end - end - describe 'local attribute override from SSO payload' do before do SiteSetting.email_editable = false @@ -724,91 +577,159 @@ RSpec.describe SessionController do end describe '#sso_provider' do - before do - stub_request(:any, /#{Discourse.current_hostname}\/uploads/).to_return( - status: 200, - body: lambda { |request| file_from_fixtures("logo.png") } - ) + let(:headers) { { host: Discourse.current_hostname } } - SiteSetting.enable_sso_provider = true - SiteSetting.enable_sso = false - SiteSetting.enable_local_logins = true - SiteSetting.sso_secret = "topsecret" + describe 'can act as an SSO provider' do + before do + stub_request(:any, /#{Discourse.current_hostname}\/uploads/).to_return( + status: 200, + body: lambda { |request| file_from_fixtures("logo.png") } + ) - @sso = SingleSignOn.new - @sso.nonce = "mynonce" - @sso.sso_secret = SiteSetting.sso_secret - @sso.return_sso_url = "http://somewhere.over.rainbow/sso" + SiteSetting.enable_sso_provider = true + SiteSetting.enable_sso = false + SiteSetting.enable_local_logins = true + SiteSetting.sso_provider_secrets = "www.random.site|secretForRandomSite\nsomewhere.over.rainbow|secretForOverRainbow" - @user = Fabricate(:user, password: "myfrogs123ADMIN", active: true, admin: true) - @user.create_user_avatar! - UserAvatar.import_url_for_user(logo_fixture, @user) - UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: false) - UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: true) + @sso = SingleSignOn.new + @sso.nonce = "mynonce" + @sso.return_sso_url = "http://somewhere.over.rainbow/sso" - @user.reload - @user.user_avatar.reload - @user.user_profile.reload - EmailToken.update_all(confirmed: true) - end + @user = Fabricate(:user, password: "myfrogs123ADMIN", active: true, admin: true) + group = Fabricate(:group) + group.add(@user) - it "successfully logs in and redirects user to return_sso_url when the user is not logged in" do - get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload) - expect(response).to redirect_to("/login") + @user.create_user_avatar! + UserAvatar.import_url_for_user(logo_fixture, @user) + UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: false) + UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: true) - post "/session.json", - params: { login: @user.username, password: "myfrogs123ADMIN" }, - xhr: true + @user.reload + @user.user_avatar.reload + @user.user_profile.reload + EmailToken.update_all(confirmed: true) + end - location = response.cookies["sso_destination_url"] - # javascript code will handle redirection of user to return_sso_url - expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/) + it "successfully logs in and redirects user to return_sso_url when the user is not logged in" do + get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload("secretForOverRainbow")) - payload = location.split("?")[1] - sso2 = SingleSignOn.parse(payload, "topsecret") + expect(response).to redirect_to("/login") - expect(sso2.email).to eq(@user.email) - expect(sso2.name).to eq(@user.name) - expect(sso2.username).to eq(@user.username) - expect(sso2.external_id).to eq(@user.id.to_s) - expect(sso2.admin).to eq(true) - expect(sso2.moderator).to eq(false) - expect(sso2.groups).to eq(@user.groups.pluck(:name).join(",")) + post "/session.json", + params: { login: @user.username, password: "myfrogs123ADMIN" }, xhr: true, headers: headers - expect(sso2.avatar_url.blank?).to_not eq(true) - expect(sso2.profile_background_url.blank?).to_not eq(true) - expect(sso2.card_background_url.blank?).to_not eq(true) + location = response.cookies["sso_destination_url"] + # javascript code will handle redirection of user to return_sso_url + expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/) - expect(sso2.avatar_url).to start_with(Discourse.base_url) - expect(sso2.profile_background_url).to start_with(Discourse.base_url) - expect(sso2.card_background_url).to start_with(Discourse.base_url) - end + payload = location.split("?")[1] + sso2 = SingleSignOn.parse(payload) - it "successfully redirects user to return_sso_url when the user is logged in" do - sign_in(@user) + expect(sso2.email).to eq(@user.email) + expect(sso2.name).to eq(@user.name) + expect(sso2.username).to eq(@user.username) + expect(sso2.external_id).to eq(@user.id.to_s) + expect(sso2.admin).to eq(true) + expect(sso2.moderator).to eq(false) + expect(sso2.groups).to eq(@user.groups.pluck(:name).join(",")) - get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload) + expect(sso2.avatar_url.blank?).to_not eq(true) + expect(sso2.profile_background_url.blank?).to_not eq(true) + expect(sso2.card_background_url.blank?).to_not eq(true) - location = response.header["Location"] - expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/) + expect(sso2.avatar_url).to start_with(Discourse.base_url) + expect(sso2.profile_background_url).to start_with(Discourse.base_url) + expect(sso2.card_background_url).to start_with(Discourse.base_url) + end - payload = location.split("?")[1] - sso2 = SingleSignOn.parse(payload, "topsecret") + it "it fails to log in if secret is wrong" do + get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload("secretForRandomSite")) - expect(sso2.email).to eq(@user.email) - expect(sso2.name).to eq(@user.name) - expect(sso2.username).to eq(@user.username) - expect(sso2.external_id).to eq(@user.id.to_s) - expect(sso2.admin).to eq(true) - expect(sso2.moderator).to eq(false) + expect(response.status).to eq(500) + end - expect(sso2.avatar_url.blank?).to_not eq(true) - expect(sso2.profile_background_url.blank?).to_not eq(true) - expect(sso2.card_background_url.blank?).to_not eq(true) + it "successfully redirects user to return_sso_url when the user is logged in" do + sign_in(@user) - expect(sso2.avatar_url).to start_with("#{Discourse.store.absolute_base_url}/original") - expect(sso2.profile_background_url).to start_with(Discourse.base_url) - expect(sso2.card_background_url).to start_with(Discourse.base_url) + get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload("secretForOverRainbow")) + + location = response.header["Location"] + expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/) + + payload = location.split("?")[1] + sso2 = SingleSignOn.parse(payload) + + expect(sso2.email).to eq(@user.email) + expect(sso2.name).to eq(@user.name) + expect(sso2.username).to eq(@user.username) + expect(sso2.external_id).to eq(@user.id.to_s) + expect(sso2.admin).to eq(true) + expect(sso2.moderator).to eq(false) + expect(sso2.groups).to eq(@user.groups.pluck(:name).join(",")) + + expect(sso2.avatar_url.blank?).to_not eq(true) + expect(sso2.profile_background_url.blank?).to_not eq(true) + expect(sso2.card_background_url.blank?).to_not eq(true) + + expect(sso2.avatar_url).to start_with(Discourse.base_url) + expect(sso2.profile_background_url).to start_with(Discourse.base_url) + expect(sso2.card_background_url).to start_with(Discourse.base_url) + end + + it 'handles non local content correctly' do + SiteSetting.avatar_sizes = "100|49" + SiteSetting.enable_s3_uploads = true + SiteSetting.s3_access_key_id = "XXX" + SiteSetting.s3_secret_access_key = "XXX" + SiteSetting.s3_upload_bucket = "test" + SiteSetting.s3_cdn_url = "http://cdn.com" + + stub_request(:any, /test.s3.dualstack.us-east-1.amazonaws.com/).to_return(status: 200, body: "", headers: { referer: "fgdfds" }) + + @user.create_user_avatar! + upload = Fabricate(:upload, url: "//test.s3.dualstack.us-east-1.amazonaws.com/something") + + Fabricate(:optimized_image, + sha1: SecureRandom.hex << "A" * 8, + upload: upload, + width: 98, + height: 98, + url: "//test.s3.amazonaws.com/something/else" + ) + + @user.update_columns(uploaded_avatar_id: upload.id) + @user.user_profile.update_columns( + profile_background: "//test.s3.dualstack.us-east-1.amazonaws.com/something", + card_background: "//test.s3.dualstack.us-east-1.amazonaws.com/something" + ) + + @user.reload + @user.user_avatar.reload + @user.user_profile.reload + + sign_in(@user) + + stub_request(:get, "http://cdn.com/something/else").to_return( + body: lambda { |request| File.new(Rails.root + 'spec/fixtures/images/logo.png') } + ) + + get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload("secretForOverRainbow")) + + location = response.header["Location"] + # javascript code will handle redirection of user to return_sso_url + expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/) + + payload = location.split("?")[1] + sso2 = SingleSignOn.parse(payload) + + expect(sso2.avatar_url.blank?).to_not eq(true) + expect(sso2.profile_background_url.blank?).to_not eq(true) + expect(sso2.card_background_url.blank?).to_not eq(true) + + expect(sso2.avatar_url).to start_with("#{SiteSetting.s3_cdn_url}/original") + expect(sso2.profile_background_url).to start_with(SiteSetting.s3_cdn_url) + expect(sso2.card_background_url).to start_with(SiteSetting.s3_cdn_url) + end end end diff --git a/spec/services/wildcard_domain_checker_spec.rb b/spec/services/wildcard_domain_checker_spec.rb new file mode 100644 index 0000000000..806ca99246 --- /dev/null +++ b/spec/services/wildcard_domain_checker_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe WildcardDomainChecker do + + describe 'check_domain' do + context 'valid domain' do + it 'returns correct domain' do + result1 = WildcardDomainChecker.check_domain('*.discourse.org', 'anything.is.possible.discourse.org') + expect(result1[0]).to eq('anything.is.possible.discourse.org') + + result2 = WildcardDomainChecker.check_domain('www.discourse.org', 'www.discourse.org') + expect(result2[0]).to eq('www.discourse.org') + + result3 = WildcardDomainChecker.check_domain('*', 'hello.discourse.org') + expect(result3[0]).to eq('hello.discourse.org') + end + end + + context 'invalid domain' do + it "doesn't return the domain" do + result1 = WildcardDomainChecker.check_domain('*.discourse.org', 'bad-domain.discourse.org.evil.com') + expect(result1).to eq(nil) + + result2 = WildcardDomainChecker.check_domain('www.discourse.org', 'www.discourse.org.evil.com') + expect(result2).to eq(nil) + + result3 = WildcardDomainChecker.check_domain('www.discourse.org', 'www.www.discourse.org') + expect(result3).to eq(nil) + + result4 = WildcardDomainChecker.check_domain('www.*.discourse.org', 'www.www.discourse.org') + expect(result4).to eq(nil) + end + end + end +end diff --git a/test/javascripts/components/secret-value-list-test.js.es6 b/test/javascripts/components/secret-value-list-test.js.es6 new file mode 100644 index 0000000000..1f602cbdbd --- /dev/null +++ b/test/javascripts/components/secret-value-list-test.js.es6 @@ -0,0 +1,63 @@ +import componentTest from "helpers/component-test"; +moduleForComponent("secret-value-list", { integration: true }); + +componentTest("adding a value", { + template: "{{secret-value-list values=values}}", + + async test(assert) { + this.set("values", "firstKey|FirstValue\nsecondKey|secondValue"); + + await fillIn(".new-value-input.key", "thirdKey"); + await click(".add-value-btn"); + + assert.ok( + find(".values .value").length === 2, + "it doesn't add the value to the list if secret is missing" + ); + + await fillIn(".new-value-input.key", ""); + await fillIn(".new-value-input.secret", "thirdValue"); + await click(".add-value-btn"); + + assert.ok( + find(".values .value").length === 2, + "it doesn't add the value to the list if key is missing" + ); + + await fillIn(".new-value-input.key", "thirdKey"); + await fillIn(".new-value-input.secret", "thirdValue"); + await click(".add-value-btn"); + + assert.ok( + find(".values .value").length === 3, + "it adds the value to the list of values" + ); + + assert.deepEqual( + this.get("values"), + "firstKey|FirstValue\nsecondKey|secondValue\nthirdKey|thirdValue", + "it adds the value to the list of values" + ); + } +}); + +componentTest("removing a value", { + template: "{{secret-value-list values=values}}", + + async test(assert) { + this.set("values", "firstKey|FirstValue\nsecondKey|secondValue"); + + await click(".values .value[data-index='0'] .remove-value-btn"); + + assert.ok( + find(".values .value").length === 1, + "it removes the value from the list of values" + ); + + assert.equal( + this.get("values"), + "secondKey|secondValue", + "it removes the expected value" + ); + } +}); From 4c8fe13500c5b79bb93366fdfb4003e82f585220 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 15 Oct 2018 17:29:10 +1100 Subject: [PATCH 017/209] FIX: remove code that restricted "header" theme field from admin There was some old code that restricted a percentage of a themes code from admin, only when admin was refreshed, this leads to lots of confusion Conditional is now removed --- app/views/layouts/application.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index a64d8997c3..83a9b6753f 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -86,7 +86,7 @@ - <%- unless customization_disabled? || loading_admin? %> + <%- unless customization_disabled? %> <%= theme_lookup("header") %> <%= build_plugin_html 'server:header' %> <%- end %> From 8fa59f05480b9d7ae596b923a069af9f74e0852d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 15 Oct 2018 14:45:28 +0800 Subject: [PATCH 018/209] FIX: Can't clean a tag if the given string is frozen. --- lib/discourse_tagging.rb | 9 ++++++--- spec/components/discourse_tagging_spec.rb | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/discourse_tagging.rb b/lib/discourse_tagging.rb index 2c0519cd19..ac2061b41f 100644 --- a/lib/discourse_tagging.rb +++ b/lib/discourse_tagging.rb @@ -202,10 +202,13 @@ module DiscourseTagging end def self.clean_tag(tag) + tag = tag.dup tag.downcase! if SiteSetting.force_lowercase_tags - tag.strip - .gsub(/\s+/, '-').squeeze('-') - .gsub(TAGS_FILTER_REGEXP, '')[0...SiteSetting.max_tag_length] + tag.strip! + tag.gsub!(/\s+/, '-') + tag.squeeze!('-') + tag.gsub!(TAGS_FILTER_REGEXP, '') + tag[0...SiteSetting.max_tag_length] end def self.tags_for_saving(tags_arg, guardian, opts = {}) diff --git a/spec/components/discourse_tagging_spec.rb b/spec/components/discourse_tagging_spec.rb index 5d52375406..f080a03f8c 100644 --- a/spec/components/discourse_tagging_spec.rb +++ b/spec/components/discourse_tagging_spec.rb @@ -209,7 +209,8 @@ describe DiscourseTagging do describe "clean_tag" do it "downcases new tags if setting enabled" do - expect(DiscourseTagging.clean_tag("HeLlO")).to eq("hello") + expect(DiscourseTagging.clean_tag("HeLlO".freeze)).to eq("hello") + SiteSetting.force_lowercase_tags = false expect(DiscourseTagging.clean_tag("HeLlO")).to eq("HeLlO") end From 7ac08f936e24572842bd93c122a794dd9c0ec3d5 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 15 Oct 2018 09:12:54 +0100 Subject: [PATCH 019/209] FEATURE: Upload tags from CSV (#6484) --- .../admin/components/tags-uploader.js.es6 | 19 ++++++++ .../templates/components/tags-uploader.hbs | 6 +++ .../components/tags-admin-dropdown.js.es6 | 9 +++- .../discourse/controllers/tags-index.js.es6 | 5 ++ .../discourse/routes/tags-index.js.es6 | 4 ++ .../discourse/templates/modal/tag-upload.hbs | 3 ++ app/controllers/tags_controller.rb | 29 ++++++++++++ config/locales/client.en.yml | 5 +- config/locales/server.en.yml | 1 + config/routes.rb | 1 + spec/fixtures/csv/tags.csv | 6 +++ spec/fixtures/csv/tags_invalid.csv | 5 ++ spec/requests/tags_controller_spec.rb | 46 +++++++++++++++++++ 13 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/admin/components/tags-uploader.js.es6 create mode 100644 app/assets/javascripts/admin/templates/components/tags-uploader.hbs create mode 100644 app/assets/javascripts/discourse/templates/modal/tag-upload.hbs create mode 100644 spec/fixtures/csv/tags.csv create mode 100644 spec/fixtures/csv/tags_invalid.csv diff --git a/app/assets/javascripts/admin/components/tags-uploader.js.es6 b/app/assets/javascripts/admin/components/tags-uploader.js.es6 new file mode 100644 index 0000000000..621045b731 --- /dev/null +++ b/app/assets/javascripts/admin/components/tags-uploader.js.es6 @@ -0,0 +1,19 @@ +import UploadMixin from "discourse/mixins/upload"; + +export default Em.Component.extend(UploadMixin, { + type: "csv", + uploadUrl: "/tags/upload", + addDisabled: Em.computed.alias("uploading"), + elementId: "tag-uploader", + + validateUploadedFilesOptions() { + return { csvOnly: true }; + }, + + uploadDone() { + bootbox.alert(I18n.t("tagging.upload_successful"), () => { + this.sendAction("refresh"); + this.sendAction("closeModal"); + }); + } +}); diff --git a/app/assets/javascripts/admin/templates/components/tags-uploader.hbs b/app/assets/javascripts/admin/templates/components/tags-uploader.hbs new file mode 100644 index 0000000000..eca270f369 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/tags-uploader.hbs @@ -0,0 +1,6 @@ + + {{i18n 'tagging.upload_instructions'}} diff --git a/app/assets/javascripts/discourse/components/tags-admin-dropdown.js.es6 b/app/assets/javascripts/discourse/components/tags-admin-dropdown.js.es6 index e599f3d9b5..03b5a3c471 100644 --- a/app/assets/javascripts/discourse/components/tags-admin-dropdown.js.es6 +++ b/app/assets/javascripts/discourse/components/tags-admin-dropdown.js.es6 @@ -16,6 +16,12 @@ export default DropdownSelectBoxComponent.extend({ name: I18n.t("tagging.manage_groups"), description: I18n.t("tagging.manage_groups_description"), icon: "wrench" + }, + { + id: "uploadTags", + name: I18n.t("tagging.upload"), + description: I18n.t("tagging.upload_description"), + icon: "upload" } ]; @@ -23,7 +29,8 @@ export default DropdownSelectBoxComponent.extend({ }, actionNames: { - manageGroups: "showTagGroups" + manageGroups: "showTagGroups", + uploadTags: "showUploader" }, mutateValue(id) { diff --git a/app/assets/javascripts/discourse/controllers/tags-index.js.es6 b/app/assets/javascripts/discourse/controllers/tags-index.js.es6 index 1baa4315dd..8f4cf5ba29 100644 --- a/app/assets/javascripts/discourse/controllers/tags-index.js.es6 +++ b/app/assets/javascripts/discourse/controllers/tags-index.js.es6 @@ -1,4 +1,5 @@ import computed from "ember-addons/ember-computed-decorators"; +import showModal from "discourse/lib/show-modal"; export default Ember.Controller.extend({ sortProperties: ["totalCount:desc", "id"], @@ -33,6 +34,10 @@ export default Ember.Controller.extend({ sortedByCount: false, sortedByName: true }); + }, + + showUploader() { + showModal("tag-upload"); } } }); diff --git a/app/assets/javascripts/discourse/routes/tags-index.js.es6 b/app/assets/javascripts/discourse/routes/tags-index.js.es6 index 1ab8c6fb1c..c9436e876d 100644 --- a/app/assets/javascripts/discourse/routes/tags-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/tags-index.js.es6 @@ -41,6 +41,10 @@ export default Discourse.Route.extend({ showTagGroups() { this.transitionTo("tagGroups"); return true; + }, + + refresh() { + this.refresh(); } } }); diff --git a/app/assets/javascripts/discourse/templates/modal/tag-upload.hbs b/app/assets/javascripts/discourse/templates/modal/tag-upload.hbs new file mode 100644 index 0000000000..d79cb8de40 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/tag-upload.hbs @@ -0,0 +1,3 @@ +{{#d-modal-body title='tagging.upload'}} + {{tags-uploader closeModal=(action "closeModal") refresh=(route-action "refresh")}} +{{/d-modal-body}} diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 8c7c854e7c..a493d9c25c 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -117,6 +117,35 @@ class TagsController < ::ApplicationController end end + def upload + guardian.ensure_can_admin_tags! + + file = params[:file] || params[:files].first + + hijack do + begin + Tag.transaction do + CSV.foreach(file.tempfile) do |row| + raise Discourse::InvalidParameters.new(I18n.t("tags.upload_row_too_long")) if row.length > 2 + + tag_name = DiscourseTagging.clean_tag(row[0]) + tag_group_name = row[1] || nil + + tag = Tag.find_by_name(tag_name) || Tag.create!(name: tag_name) + + if tag_group_name + tag_group = TagGroup.find_by(name: tag_group_name) || TagGroup.create!(name: tag_group_name) + tag.tag_groups << tag_group unless tag.tag_groups.include?(tag_group) + end + end + end + render json: success_json + rescue Discourse::InvalidParameters => e + render json: failed_json.merge(errors: [e.message]), status: 422 + end + end + end + def destroy guardian.ensure_can_admin_tags! tag_name = params[:tag_id] diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 806b24ad8c..a02309cc0e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2700,7 +2700,10 @@ en: sort_by_name: "name" manage_groups: "Manage Tag Groups" manage_groups_description: "Define groups to organize tags" - + upload: "Upload Tags" + upload_description: "Upload a text file to create tags in bulk" + upload_instructions: "One per line, optionally with a tag group in the format 'tag_name,tag_group'." + upload_successful: "Tags uploaded successfully" filters: without_category: "%{filter} %{tag} topics" with_category: "%{filter} %{tag} topics in %{category}" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2606cee026..adadf4da71 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3903,6 +3903,7 @@ en: staff_tag_disallowed: "The tag \"%{tag}\" may only be applied by staff." staff_tag_remove_disallowed: "The tag \"%{tag}\" may only be removed by staff." minimum_required_tags: "You must select at least %{count} tags." + upload_row_too_long: "The CSV file should have one tag per line. Optionally the tag can be followed by a comma, then the tag group name." rss_by_tag: "Topics tagged %{tag}" finish_installation: diff --git a/config/routes.rb b/config/routes.rb index f4769f67a2..a65c0f0297 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -775,6 +775,7 @@ Discourse::Application.routes.draw do get '/filter/search' => 'tags#search' get '/check' => 'tags#check_hashtag' get '/personal_messages/:username' => 'tags#personal_messages' + post '/upload' => 'tags#upload' constraints(tag_id: /[^\/]+?/, format: /json|rss/) do get '/:tag_id.rss' => 'tags#tag_feed' get '/:tag_id' => 'tags#show', as: 'tag_show' diff --git a/spec/fixtures/csv/tags.csv b/spec/fixtures/csv/tags.csv new file mode 100644 index 0000000000..9d44199dd8 --- /dev/null +++ b/spec/fixtures/csv/tags.csv @@ -0,0 +1,6 @@ +tag1 +Capitaltag2 +spaced tag +tag1 +tag3,taggroup1 +tag4,taggroup1 diff --git a/spec/fixtures/csv/tags_invalid.csv b/spec/fixtures/csv/tags_invalid.csv new file mode 100644 index 0000000000..e1bae58199 --- /dev/null +++ b/spec/fixtures/csv/tags_invalid.csv @@ -0,0 +1,5 @@ +tag1 +tag2 +tag3,taggroup1 +tag4,taggroup2 +tag5,with,too,many,columns diff --git a/spec/requests/tags_controller_spec.rb b/spec/requests/tags_controller_spec.rb index 9be7b71022..c24712daff 100644 --- a/spec/requests/tags_controller_spec.rb +++ b/spec/requests/tags_controller_spec.rb @@ -369,4 +369,50 @@ describe TagsController do end end end + + context '#upload_csv' do + it 'requires you to be logged in' do + post "/tags/upload.json" + expect(response.status).to eq(403) + end + + context 'while logged in' do + let(:csv_file) { File.new("#{Rails.root}/spec/fixtures/csv/tags.csv") } + let(:invalid_csv_file) { File.new("#{Rails.root}/spec/fixtures/csv/tags_invalid.csv") } + + let(:file) do + Rack::Test::UploadedFile.new(File.open(csv_file)) + end + + let(:invalid_file) do + Rack::Test::UploadedFile.new(File.open(invalid_csv_file)) + end + + let(:filename) { 'tags.csv' } + + it "fails if you can't manage tags" do + sign_in(Fabricate(:user)) + post "/tags/upload.json", params: { file: file, name: filename } + expect(response.status).to eq(403) + end + + it "allows staff to bulk upload tags" do + sign_in(Fabricate(:moderator)) + post "/tags/upload.json", params: { file: file, name: filename } + expect(response.status).to eq(200) + expect(Tag.pluck(:name)).to contain_exactly("tag1", "capitaltag2", "spaced-tag", "tag3", "tag4") + expect(Tag.find_by_name("tag3").tag_groups.pluck(:name)).to contain_exactly("taggroup1") + expect(Tag.find_by_name("tag4").tag_groups.pluck(:name)).to contain_exactly("taggroup1") + end + + it "fails gracefully with invalid input" do + sign_in(Fabricate(:moderator)) + + expect do + post "/tags/upload.json", params: { file: invalid_file, name: filename } + expect(response.status).to eq(422) + end.not_to change { [Tag.count, TagGroup.count] } + end + end + end end From a552a39f5322759ed7dac31794a26570d89ad8a6 Mon Sep 17 00:00:00 2001 From: Joe <33972521+hnb-ku@users.noreply.github.com> Date: Mon, 15 Oct 2018 16:25:28 +0800 Subject: [PATCH 020/209] UX: presence-users overlaps with composer toggles --- plugins/discourse-presence/assets/stylesheets/presence.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/discourse-presence/assets/stylesheets/presence.scss b/plugins/discourse-presence/assets/stylesheets/presence.scss index 85f9950ca7..f7bae24433 100644 --- a/plugins/discourse-presence/assets/stylesheets/presence.scss +++ b/plugins/discourse-presence/assets/stylesheets/presence.scss @@ -48,8 +48,8 @@ .composer-fields .presence-users { position: absolute; - top: 18px; - right: 40px; + top: 20px; + right: 55px; @media screen and (max-width: $small-width) { max-width: 318px; .presence-avatars { From d76658ff8c4c6fa79b1f9b7a5ff80c1bb694603c Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 15 Oct 2018 16:19:25 +0530 Subject: [PATCH 021/209] FEATURE: new rake task to anonymize all users --- lib/tasks/users.rake | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/tasks/users.rake b/lib/tasks/users.rake index 9bfaa79c9c..6e82657a84 100644 --- a/lib/tasks/users.rake +++ b/lib/tasks/users.rake @@ -147,6 +147,30 @@ task "users:disable_2fa", [:username] => [:environment] do |_, args| puts "2FA disabled for #{username}" end +desc "Anonymize all users except staff" +task "users:anonymize_all" => :environment do + require 'highline/import' + + non_staff_users = User.where('NOT admin AND NOT moderator') + total = non_staff_users.count + anonymized = 0 + + confirm_anonymize = ask("Are you sure you want to anonymize #{total} users? (Y/n)") + exit 1 unless (confirm_anonymize == "" || confirm_anonymize.downcase == 'y') + + system_user = Discourse.system_user + non_staff_users.each do |user| + begin + UserAnonymizer.new(user, system_user).make_anonymous + print_status(anonymized += 1, total) + rescue + # skip + end + end + + puts "", "#{total} users anonymized.", "" +end + def find_user(username) user = User.find_by_username(username) From f6eff38c0e0656d5df8f991a296494afa55c0e57 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 15 Oct 2018 15:48:35 +0200 Subject: [PATCH 022/209] FEATURE: adds list#(unread|new) to user api key routes (#6494) --- app/models/user_api_key.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/models/user_api_key.rb b/app/models/user_api_key.rb index 13eddc9274..5ed0d47b4d 100644 --- a/app/models/user_api_key.rb +++ b/app/models/user_api_key.rb @@ -6,7 +6,12 @@ class UserApiKey < ActiveRecord::Base message_bus: [[:post, 'message_bus']], push: nil, notifications: [[:post, 'message_bus'], [:get, 'notifications#index'], [:put, 'notifications#mark_read']], - session_info: [[:get, 'session#current'], [:get, 'users#topic_tracking_state']] + session_info: [ + [:get, 'session#current'], + [:get, 'users#topic_tracking_state'], + [:get, 'list#unread'], + [:get, 'list#new'] + ] } belongs_to :user From c1042569912b465468725d26db19700d1e2773cb Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Mon, 15 Oct 2018 12:57:45 +0200 Subject: [PATCH 023/209] FIX: SSO provider secrets - check wildcard domains last, toggle secrets visibility --- .../admin/templates/components/secret-value-list.hbs | 2 +- .../admin/templates/components/site-settings/secret-list.hbs | 2 +- config/site_settings.yml | 1 + lib/single_sign_on.rb | 4 +++- spec/requests/session_controller_spec.rb | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/templates/components/secret-value-list.hbs b/app/assets/javascripts/admin/templates/components/secret-value-list.hbs index 7058504fac..6bdc972ddc 100644 --- a/app/assets/javascripts/admin/templates/components/secret-value-list.hbs +++ b/app/assets/javascripts/admin/templates/components/secret-value-list.hbs @@ -7,7 +7,7 @@ icon="times" class="remove-value-btn btn-small"}} {{input value=value.key class="value-input" focus-out=(action "changeKey" index)}} - {{input value=value.secret class="value-input" focus-out=(action "changeSecret" index) type="password"}} + {{input value=value.secret class="value-input" focus-out=(action "changeSecret" index) type=(if isSecret "password" "text")}}
{{/each}}
diff --git a/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs index 1e71c18c63..e77ddbb520 100644 --- a/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs @@ -1,3 +1,3 @@ -{{secret-value-list setting=setting values=value}} +{{secret-value-list setting=setting values=value isSecret=isSecret}} {{setting-validation-message message=validationMessage}}
{{{unbound setting.description}}}
diff --git a/config/site_settings.yml b/config/site_settings.yml index c98dc0610d..4af22bda68 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -345,6 +345,7 @@ login: default: '' type: list list_type: secret + secret: true placeholder: key: "sso_provider.key_placeholder" value: "sso_provider.value_placeholder" diff --git a/lib/single_sign_on.rb b/lib/single_sign_on.rb index f5a59e26a6..2758ad65f9 100644 --- a/lib/single_sign_on.rb +++ b/lib/single_sign_on.rb @@ -92,8 +92,10 @@ class SingleSignOn provider_secrets = SiteSetting.sso_provider_secrets.split(/[\|,\n]/) provider_secrets_hash = Hash[*provider_secrets] return_url_host = URI.parse(return_sso_url).host + # moves wildcard domains to the end of hash + sorted_secrets = provider_secrets_hash.sort_by { |k, _| k }.reverse.to_h - secret = provider_secrets_hash.select do |domain, _| + secret = sorted_secrets.select do |domain, _| WildcardDomainChecker.check_domain(domain, return_url_host) end secret.present? ? secret.values.first : nil diff --git a/spec/requests/session_controller_spec.rb b/spec/requests/session_controller_spec.rb index 2f8b8a4bf7..a6c5e69d86 100644 --- a/spec/requests/session_controller_spec.rb +++ b/spec/requests/session_controller_spec.rb @@ -589,7 +589,7 @@ RSpec.describe SessionController do SiteSetting.enable_sso_provider = true SiteSetting.enable_sso = false SiteSetting.enable_local_logins = true - SiteSetting.sso_provider_secrets = "www.random.site|secretForRandomSite\nsomewhere.over.rainbow|secretForOverRainbow" + SiteSetting.sso_provider_secrets = "*|secretforAll\n*.rainbow|wrongSecretForOverRainbow\nwww.random.site|secretForRandomSite\nsomewhere.over.rainbow|secretForOverRainbow" @sso = SingleSignOn.new @sso.nonce = "mynonce" From 99d1ded3b3a2767617bbee9824e93eb57f135864 Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Mon, 15 Oct 2018 11:32:52 -0400 Subject: [PATCH 024/209] rename route `/javascripts` to `/theme-javascripts` (#6495) --- ...s_controller.rb => theme_javascripts_controller.rb} | 2 +- app/models/javascript_cache.rb | 2 +- config/nginx.sample.conf | 2 +- config/routes.rb | 2 +- ...er_spec.rb => theme_javascripts_controller_spec.rb} | 10 +++++----- 5 files changed, 9 insertions(+), 9 deletions(-) rename app/controllers/{javascripts_controller.rb => theme_javascripts_controller.rb} (96%) rename spec/requests/{javascripts_controller_spec.rb => theme_javascripts_controller_spec.rb} (85%) diff --git a/app/controllers/javascripts_controller.rb b/app/controllers/theme_javascripts_controller.rb similarity index 96% rename from app/controllers/javascripts_controller.rb rename to app/controllers/theme_javascripts_controller.rb index 5e861a3e65..bbee8f480a 100644 --- a/app/controllers/javascripts_controller.rb +++ b/app/controllers/theme_javascripts_controller.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -class JavascriptsController < ApplicationController +class ThemeJavascriptsController < ApplicationController DISK_CACHE_PATH = "#{Rails.root}/tmp/javascript-cache" skip_before_action( diff --git a/app/models/javascript_cache.rb b/app/models/javascript_cache.rb index fd0bbfa735..e11c40feb2 100644 --- a/app/models/javascript_cache.rb +++ b/app/models/javascript_cache.rb @@ -7,7 +7,7 @@ class JavascriptCache < ActiveRecord::Base before_save :update_digest def url - "#{GlobalSetting.cdn_url}#{GlobalSetting.relative_url_root}/javascripts/#{digest}.js" + "#{GlobalSetting.cdn_url}#{GlobalSetting.relative_url_root}/theme-javascripts/#{digest}.js" end private diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf index 24e8646950..b1a5944718 100644 --- a/config/nginx.sample.conf +++ b/config/nginx.sample.conf @@ -191,7 +191,7 @@ server { # This big block is needed so we can selectively enable # acceleration for backups and avatars # see note about repetition above - location ~ ^/(letter_avatar/|user_avatar|highlight-js|stylesheets|favicon/proxied|service-worker) { + location ~ ^/(letter_avatar/|user_avatar|highlight-js|stylesheets|theme-javascripts|favicon/proxied|service-worker) { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Request-Start "t=${msec}"; diff --git a/config/routes.rb b/config/routes.rb index a65c0f0297..3d072a4691 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -450,7 +450,7 @@ Discourse::Application.routes.draw do get "stylesheets/:name.css.map" => "stylesheets#show_source_map", constraints: { name: /[-a-z0-9_]+/ } get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ } - get "javascripts/:digest.js" => "javascripts#show", constraints: { digest: /\h{40}/ } + get "theme-javascripts/:digest.js" => "theme_javascripts#show", constraints: { digest: /\h{40}/ } post "uploads" => "uploads#create" post "uploads/lookup-urls" => "uploads#lookup_urls" diff --git a/spec/requests/javascripts_controller_spec.rb b/spec/requests/theme_javascripts_controller_spec.rb similarity index 85% rename from spec/requests/javascripts_controller_spec.rb rename to spec/requests/theme_javascripts_controller_spec.rb index a0a985a63b..fd186ffc17 100644 --- a/spec/requests/javascripts_controller_spec.rb +++ b/spec/requests/theme_javascripts_controller_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rails_helper' -describe JavascriptsController do +describe ThemeJavascriptsController do let(:theme) { Fabricate(:theme) } let(:theme_field) { ThemeField.create!(theme: theme, target_id: 0, name: "header", value: "html") } let(:javascript_cache) { JavascriptCache.create!(content: 'console.log("hello");', theme_field: theme_field) } @@ -11,7 +11,7 @@ describe JavascriptsController do # actually set digest to make sure 404 is raised by router javascript_cache.update_attributes(digest: digest) - get "/javascripts/#{digest}.js" + get "/theme-javascripts/#{digest}.js" end it 'only accepts 40-char hexdecimal digest name' do @@ -37,18 +37,18 @@ describe JavascriptsController do it 'considers the database record as the source of truth' do clear_disk_cache - get "/javascripts/#{javascript_cache.digest}.js" + get "/theme-javascripts/#{javascript_cache.digest}.js" expect(response.status).to eq(200) expect(response.body).to eq(javascript_cache.content) javascript_cache.destroy! - get "/javascripts/#{javascript_cache.digest}.js" + get "/theme-javascripts/#{javascript_cache.digest}.js" expect(response.status).to eq(404) end def clear_disk_cache - `rm #{JavascriptsController::DISK_CACHE_PATH}/*` + `rm #{ThemeJavascriptsController::DISK_CACHE_PATH}/*` end end end From d166c38ab7b96b3f72eefc41ae3287dfc1ea453b Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 15 Oct 2018 15:01:25 -0400 Subject: [PATCH 025/209] REFACTOR: distributed_cache is moved to the message_bus gem --- lib/distributed_cache.rb | 155 ++-------------------- spec/components/distributed_cache_spec.rb | 134 ------------------- 2 files changed, 8 insertions(+), 281 deletions(-) delete mode 100644 spec/components/distributed_cache_spec.rb diff --git a/lib/distributed_cache.rb b/lib/distributed_cache.rb index 203bb2ae84..31cb69ffc2 100644 --- a/lib/distributed_cache.rb +++ b/lib/distributed_cache.rb @@ -1,153 +1,14 @@ # frozen_string_literal: true -# Like a hash, just does its best to stay in sync across the farm -# On boot all instances are blank, but they populate as various processes -# fill it up - -require 'weakref' -require 'base64' - -class DistributedCache - - class Manager - CHANNEL_NAME ||= '/distributed_hash'.freeze - - def initialize(message_bus = nil) - @subscribers = [] - @subscribed = false - @lock = Mutex.new - @message_bus = message_bus || MessageBus - end - - def subscribers - @subscribers - end - - def process_message(message) - i = @subscribers.length - 1 - - payload = message.data - - while i >= 0 - begin - current = @subscribers[i] - - next if payload["origin"] == current.identity && !Rails.env.test? - next if current.key != payload["hash_key"] - next if payload["discourse_version"] != Discourse.git_version - - hash = current.hash(message.site_id) - - case payload["op"] - when "set" then hash[payload["key"]] = payload["marshalled"] ? Marshal.load(Base64.decode64(payload["value"])) : payload["value"] - when "delete" then hash.delete(payload["key"]) - when "clear" then hash.clear - end - - rescue WeakRef::RefError - @subscribers.delete_at(i) - ensure - i -= 1 - end - end - end - - def ensure_subscribe! - return if @subscribed - @lock.synchronize do - return if @subscribed - @message_bus.subscribe(CHANNEL_NAME) do |message| - @lock.synchronize do - process_message(message) - end - end - @subscribed = true - end - end - - def publish(hash, message) - message[:origin] = hash.identity - message[:hash_key] = hash.key - message[:discourse_version] = Discourse.git_version - @message_bus.publish(CHANNEL_NAME, message, user_ids: [-1]) - end - - def set(hash, key, value) - # special support for set - marshal = (Set === value || Hash === value || Array === value) - value = Base64.encode64(Marshal.dump(value)) if marshal - publish(hash, op: :set, key: key, value: value, marshalled: marshal) - end - - def delete(hash, key) - publish(hash, op: :delete, key: key) - end - - def clear(hash) - publish(hash, op: :clear) - end - - def register(hash) - @lock.synchronize do - @subscribers << WeakRef.new(hash) - end - end - end - - @default_manager = Manager.new - - def self.default_manager - @default_manager - end - - attr_reader :key +require 'message_bus/distributed_cache' +class DistributedCache < MessageBus::DistributedCache def initialize(key, manager: nil, namespace: true) - @key = key - @data = {} - @manager = manager || DistributedCache.default_manager - @namespace = namespace - - @manager.ensure_subscribe! - @manager.register(self) + super( + key, + manager: manager, + namespace: namespace, + app_version: Discourse.git_version + ) end - - def identity - # fork resilient / multi machine identity - (@seed_id ||= SecureRandom.hex) + "#{Process.pid}" - end - - def []=(k, v) - k = k.to_s if Symbol === k - @manager.set(self, k, v) - hash[k] = v - end - - def [](k) - k = k.to_s if Symbol === k - hash[k] - end - - def delete(k, publish: true) - k = k.to_s if Symbol === k - @manager.delete(self, k) if publish - hash.delete(k) - end - - def clear - @manager.clear(self) - hash.clear - end - - def hash(db = nil) - db = - if @namespace - db || RailsMultisite::ConnectionManagement.current_db - else - RailsMultisite::ConnectionManagement::DEFAULT - end - - @data[db] ||= ThreadSafe::Hash.new - end - end diff --git a/spec/components/distributed_cache_spec.rb b/spec/components/distributed_cache_spec.rb deleted file mode 100644 index 4ff420c96e..0000000000 --- a/spec/components/distributed_cache_spec.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'rails_helper' -require 'distributed_cache' - -describe DistributedCache do - - before :all do - @bus = MessageBus::Instance.new - @bus.configure(backend: :memory) - @manager = DistributedCache::Manager.new(@bus) - end - - after :all do - @bus.destroy - end - - def cache(name) - DistributedCache.new(name, manager: @manager) - end - - let :cache_name do - SecureRandom.hex - end - - let! :cache1 do - cache(cache_name) - end - - let! :cache2 do - cache(cache_name) - end - - it 'supports arrays with hashes' do - - c1 = cache("test1") - c2 = cache("test1") - - c1["test"] = [{ test: :test }] - - wait_for do - c2["test"] == [{ test: :test }] - end - - expect(c2[:test]).to eq([{ test: :test }]) - end - - it 'allows us to store Set' do - c1 = cache("test1") - c2 = cache("test1") - - set = Set.new - set << 1 - set << "b" - set << 92803984 - set << 93739739873973 - - c1["cats"] = set - - wait_for do - c2["cats"] == set - end - - expect(c2["cats"]).to eq(set) - - set << 5 - - c2["cats"] = set - - wait_for do - c1["cats"] == set - end - - expect(c1["cats"]).to eq(set) - end - - it 'does not leak state across caches' do - c2 = cache("test1") - c3 = cache("test1") - c2["hi"] = "hi" - wait_for do - c3["hi"] == "hi" - end - - Thread.pass - expect(cache1["hi"]).to eq(nil) - - end - - it 'allows coerces symbol keys to strings' do - cache1[:key] = "test" - expect(cache1["key"]).to eq("test") - - wait_for do - cache2[:key] == "test" - end - expect(cache2["key"]).to eq("test") - end - - it 'sets other caches' do - cache1["test"] = "world" - wait_for do - cache2["test"] == "world" - end - end - - it 'deletes from other caches' do - cache1["foo"] = "bar" - - wait_for do - cache2["foo"] == "bar" - end - - cache1.delete("foo") - expect(cache1["foo"]).to eq(nil) - - wait_for do - cache2["foo"] == nil - end - end - - it 'clears cache on request' do - cache1["foo"] = "bar" - - wait_for do - cache2["foo"] == "bar" - end - - cache1.clear - expect(cache1["foo"]).to eq(nil) - wait_for do - cache2["boom"] == nil - end - end - -end From 0724948878e5f728150212fb6c96e7241b62d5a8 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 15 Oct 2018 15:06:02 -0400 Subject: [PATCH 026/209] fix failing spec when HUB_BASE_URL is present --- spec/components/discourse_hub_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/components/discourse_hub_spec.rb b/spec/components/discourse_hub_spec.rb index 31b0c9651b..d4e6a05a47 100644 --- a/spec/components/discourse_hub_spec.rb +++ b/spec/components/discourse_hub_spec.rb @@ -87,7 +87,7 @@ describe DiscourseHub do end it 'should log correctly on error' do - stub_request(:get, (ENV['HUB_BASE_URL'] || "http://local.hub:3000/api/test")). + stub_request(:get, (ENV['HUB_BASE_URL'] || "http://local.hub:3000/api") + '/test'). to_return(status: 500, body: "", headers: {}) DiscourseHub.collection_action(:get, '/test') From 4c2331260ec11673e76fad6b084cafea145dcf78 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 16 Oct 2018 01:26:24 +0530 Subject: [PATCH 027/209] run specs on discourse-calendar plugin --- lib/tasks/plugin.rake | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/tasks/plugin.rake b/lib/tasks/plugin.rake index 1f158cd692..2726c759be 100644 --- a/lib/tasks/plugin.rake +++ b/lib/tasks/plugin.rake @@ -7,7 +7,6 @@ task 'plugin:install_all_official' do 'discourse-nginx-performance-report', 'lazyYT', 'poll', - 'discourse-calendar', 'discourse-prometheus-alert-receiver' ]) From 8d06731484f84ee680d12d355c3b82ddc1191140 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 16 Oct 2018 10:29:16 +1100 Subject: [PATCH 028/209] FIX: reduce amount of work onceoff does In the past onceoff was forcing inline download of gravatars, this can be so expensive that it will never finish This fix ensures it only marks avatars stale which will be picked up by regular schedules --- app/jobs/onceoff/fix_invalid_gravatar_uploads.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb index 1374401101..397ad1b6c4 100644 --- a/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb +++ b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb @@ -2,11 +2,13 @@ module Jobs class FixInvalidGravatarUploads < Jobs::Onceoff def execute_onceoff(args) Upload.where(original_filename: "gravatar.png").find_each do |upload| + # note, this still feels pretty expensive for a once off + # we may need to re-evaluate this extension = FastImage.type(Discourse.store.path_for(upload)) current_extension = upload.extension if extension.to_s.downcase != current_extension.to_s.downcase - upload.user.user_avatar.update_gravatar! + upload&.user&.user_avatar&.update_columns(last_gravatar_download_attempt: nil) end end end From 2c8c1bf1887c50cc1cbecc36dc3479fb2aa1a906 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 16 Oct 2018 05:04:55 +0530 Subject: [PATCH 029/209] Rename timezone attribute and add it to local date details field --- .../assets/javascripts/discourse-local-dates.js | 6 +++--- .../discourse-markdown/discourse-local-dates.js.es6 | 12 ++++++------ plugins/discourse-local-dates/plugin.rb | 5 +++-- .../spec/integration/local_dates_spec.rb | 4 ++-- .../discourse-local-dates/spec/models/post_spec.rb | 3 ++- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js index abced1835d..a043ec3ea7 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js +++ b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js @@ -12,9 +12,9 @@ } var relativeTime; - if (options.forceTimezone) { + if (options.timezone) { relativeTime = moment - .tz(options.date + " " + options.time, options.forceTimezone) + .tz(options.date + " " + options.time, options.timezone) .utc(); } else { relativeTime = moment.utc(options.date + " " + options.time); @@ -104,7 +104,7 @@ options.time = $this.attr("data-time") || "00:00:00"; options.recurring = $this.attr("data-recurring"); options.timezones = $this.attr("data-timezones"); - options.forceTimezone = $this.attr("data-force-timezone"); + options.timezone = $this.attr("data-timezone"); processElement($this, options); }); diff --git a/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 index 83bd99c903..ab4386f622 100644 --- a/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 +++ b/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 @@ -6,7 +6,7 @@ function addLocalDate(buffer, matches, state) { let config = { date: null, time: null, - forceTimezone: null, + timezone: null, format: "YYYY-MM-DD HH:mm:ss", timezones: "Etc/UTC" }; @@ -19,7 +19,7 @@ function addLocalDate(buffer, matches, state) { config.date = parsed.attrs.date; config.time = parsed.attrs.time; - config.forceTimezone = parsed.attrs.forceTimezone || parsed.attrs.timezone; + config.timezone = parsed.attrs.timezone; config.recurring = parsed.attrs.recurring; config.format = parsed.attrs.format || config.format; config.timezones = parsed.attrs.timezones || config.timezones; @@ -37,12 +37,12 @@ function addLocalDate(buffer, matches, state) { } let dateTime; - if (config.forceTimezone) { + if (config.timezone) { token.attrs.push([ - "data-force-timezone", - state.md.utils.escapeHtml(config.forceTimezone) + "data-timezone", + state.md.utils.escapeHtml(config.timezone) ]); - dateTime = moment.tz(`${config.date} ${config.time}`, config.forceTimezone); + dateTime = moment.tz(`${config.date} ${config.time}`, config.timezone); } else { dateTime = moment.utc(`${config.date} ${config.time}`); } diff --git a/plugins/discourse-local-dates/plugin.rb b/plugins/discourse-local-dates/plugin.rb index c2b299b521..54fe359ff0 100644 --- a/plugins/discourse-local-dates/plugin.rb +++ b/plugins/discourse-local-dates/plugin.rb @@ -27,9 +27,10 @@ after_initialize do dates = doc.css('span.discourse-local-date').map do |cooked_date| date = {} cooked_date.attributes.values.each do |attribute| - if attribute.name && ['data-date', 'data-time'].include?(attribute.name) + data_name = attribute.name&.gsub('data-', '') + if data_name && ['date', 'time', 'timezone'].include?(data_name) unless attribute.value == 'undefined' - date[attribute.name.gsub('data-', '')] = CGI.escapeHTML(attribute.value || "") + date[data_name] = CGI.escapeHTML(attribute.value || "") end end end diff --git a/plugins/discourse-local-dates/spec/integration/local_dates_spec.rb b/plugins/discourse-local-dates/spec/integration/local_dates_spec.rb index 2fed2e2dc0..738eb8f332 100644 --- a/plugins/discourse-local-dates/spec/integration/local_dates_spec.rb +++ b/plugins/discourse-local-dates/spec/integration/local_dates_spec.rb @@ -15,7 +15,7 @@ RSpec.describe "Local Dates" do expect(cooked).to include('class="discourse-local-date"') expect(cooked).to include('data-date="2018-05-08"') expect(cooked).to include('data-format="L LTS"') - expect(cooked).not_to include('data-force-timezone=') + expect(cooked).not_to include('data-timezone=') expect(cooked).to include( 'data-timezones="Europe/Paris|America/Los_Angeles"' @@ -32,7 +32,7 @@ RSpec.describe "Local Dates" do cooked = post.cooked - expect(cooked).to include('data-force-timezone="Asia/Calcutta"') + expect(cooked).to include('data-timezone="Asia/Calcutta"') expect(cooked).to include('05/08/2018 4:30:00 PM') end diff --git a/plugins/discourse-local-dates/spec/models/post_spec.rb b/plugins/discourse-local-dates/spec/models/post_spec.rb index 29793fb541..13e7e9a5ac 100644 --- a/plugins/discourse-local-dates/spec/models/post_spec.rb +++ b/plugins/discourse-local-dates/spec/models/post_spec.rb @@ -9,13 +9,14 @@ describe Post do describe '#local_dates' do it "should have correct custom fields" do post = Fabricate(:post, raw: <<~SQL) - [date=2018-09-17 time=01:39:00 format="LLL" timezones="Europe/Paris|America/Los_Angeles"] + [date=2018-09-17 time=01:39:00 format="LLL" timezone="Europe/Paris" timezones="Europe/Paris|America/Los_Angeles"] SQL CookedPostProcessor.new(post).post_process expect(post.local_dates.count).to eq(1) expect(post.local_dates[0]["date"]).to eq("2018-09-17") expect(post.local_dates[0]["time"]).to eq("01:39:00") + expect(post.local_dates[0]["timezone"]).to eq("Europe/Paris") post.raw = "Text removed" post.save From c68a456baa1922082d7ecc409d04a58ed7b6ec2b Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Tue, 16 Oct 2018 02:38:59 +0300 Subject: [PATCH 030/209] FIX: Do not award badges for links in restricted categories. (#6492) --- lib/badge_queries.rb | 3 +-- spec/models/badge_spec.rb | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index c2ce008150..f28e98f873 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -189,8 +189,7 @@ SQL <<-SQL SELECT tl.user_id, post_id, current_timestamp granted_at FROM topic_links tl - JOIN posts p ON p.id = post_id AND p.deleted_at IS NULL - JOIN topics t ON t.id = p.topic_id AND t.deleted_at IS NULL AND t.archetype <> 'private_message' + JOIN badge_posts p ON p.id = post_id WHERE NOT tl.internal AND tl.clicks >= #{count} GROUP BY tl.user_id, tl.post_id diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb index 5bc1d892eb..4b34e907fc 100644 --- a/spec/models/badge_spec.rb +++ b/spec/models/badge_spec.rb @@ -97,4 +97,37 @@ describe Badge do expect(Badge.display_name('Not In Translations')).to eq('Not In Translations') end end + + context "PopularLink badge" do + before do + badge = Badge.find(Badge::PopularLink) + badge.query = BadgeQueries.linking_badge(2) + badge.save! + end + + it "is awarded" do + post = create_post(raw: "https://www.discourse.org/") + + TopicLinkClick.create_from(url: "https://www.discourse.org/", post_id: post.id, topic_id: post.topic.id, ip: "192.168.0.100") + BadgeGranter.backfill(Badge.find(Badge::PopularLink)) + expect(UserBadge.where(user_id: post.user.id, badge_id: Badge::PopularLink).count).to eq(0) + + TopicLinkClick.create_from(url: "https://www.discourse.org/", post_id: post.id, topic_id: post.topic.id, ip: "192.168.0.101") + BadgeGranter.backfill(Badge.find(Badge::PopularLink)) + expect(UserBadge.where(user_id: post.user.id, badge_id: Badge::PopularLink).count).to eq(1) + end + + it "is not awarded for links in a restricted category" do + category = Fabricate(:category) + post = create_post(raw: "https://www.discourse.org/", category: category) + + category.set_permissions({}) + category.save! + + TopicLinkClick.create_from(url: "https://www.discourse.org/", post_id: post.id, topic_id: post.topic.id, ip: "192.168.0.100") + TopicLinkClick.create_from(url: "https://www.discourse.org/", post_id: post.id, topic_id: post.topic.id, ip: "192.168.0.101") + BadgeGranter.backfill(Badge.find(Badge::PopularLink)) + expect(UserBadge.where(user_id: post.user.id, badge_id: Badge::PopularLink).count).to eq(0) + end + end end From fc94732f88bab9d7dd8c2b2cfcdfac94f9e4533f Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 16 Oct 2018 10:42:16 +1100 Subject: [PATCH 031/209] avoid looking up badge multiple times in spec --- spec/models/badge_spec.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb index 4b34e907fc..47f2ec7bbb 100644 --- a/spec/models/badge_spec.rb +++ b/spec/models/badge_spec.rb @@ -99,21 +99,25 @@ describe Badge do end context "PopularLink badge" do + + let(:popular_link_badge) do + Badge.find(Badge::PopularLink) + end + before do - badge = Badge.find(Badge::PopularLink) - badge.query = BadgeQueries.linking_badge(2) - badge.save! + popular_link_badge.query = BadgeQueries.linking_badge(2) + popular_link_badge.save! end it "is awarded" do post = create_post(raw: "https://www.discourse.org/") TopicLinkClick.create_from(url: "https://www.discourse.org/", post_id: post.id, topic_id: post.topic.id, ip: "192.168.0.100") - BadgeGranter.backfill(Badge.find(Badge::PopularLink)) + BadgeGranter.backfill(popular_link_badge) expect(UserBadge.where(user_id: post.user.id, badge_id: Badge::PopularLink).count).to eq(0) TopicLinkClick.create_from(url: "https://www.discourse.org/", post_id: post.id, topic_id: post.topic.id, ip: "192.168.0.101") - BadgeGranter.backfill(Badge.find(Badge::PopularLink)) + BadgeGranter.backfill(popular_link_badge) expect(UserBadge.where(user_id: post.user.id, badge_id: Badge::PopularLink).count).to eq(1) end @@ -126,7 +130,7 @@ describe Badge do TopicLinkClick.create_from(url: "https://www.discourse.org/", post_id: post.id, topic_id: post.topic.id, ip: "192.168.0.100") TopicLinkClick.create_from(url: "https://www.discourse.org/", post_id: post.id, topic_id: post.topic.id, ip: "192.168.0.101") - BadgeGranter.backfill(Badge.find(Badge::PopularLink)) + BadgeGranter.backfill(popular_link_badge) expect(UserBadge.where(user_id: post.user.id, badge_id: Badge::PopularLink).count).to eq(0) end end From 005e1f53738e681c678d77d4210eace7d9f17881 Mon Sep 17 00:00:00 2001 From: Davide Porrovecchio Date: Tue, 16 Oct 2018 01:46:55 +0200 Subject: [PATCH 032/209] Add Cache-Control header to CORS (#6490) --- config/initializers/008-rack-cors.rb | 2 +- spec/components/hijack_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/008-rack-cors.rb b/config/initializers/008-rack-cors.rb index 1512c2bda3..8c6560d476 100644 --- a/config/initializers/008-rack-cors.rb +++ b/config/initializers/008-rack-cors.rb @@ -39,7 +39,7 @@ class Discourse::Cors end headers['Access-Control-Allow-Origin'] = origin || cors_origins[0] - headers['Access-Control-Allow-Headers'] = 'Content-Type, X-Requested-With, X-CSRF-Token, Discourse-Visible, User-Api-Key, User-Api-Client-Id' + headers['Access-Control-Allow-Headers'] = 'Content-Type, Cache-Control, X-Requested-With, X-CSRF-Token, Discourse-Visible, User-Api-Key, User-Api-Client-Id' headers['Access-Control-Allow-Credentials'] = 'true' headers['Access-Control-Allow-Methods'] = 'POST, PUT, GET, OPTIONS, DELETE' end diff --git a/spec/components/hijack_spec.rb b/spec/components/hijack_spec.rb index e06532e9dc..9969f418de 100644 --- a/spec/components/hijack_spec.rb +++ b/spec/components/hijack_spec.rb @@ -107,7 +107,7 @@ describe Hijack do expected = { "Access-Control-Allow-Origin" => "www.rainbows.com", - "Access-Control-Allow-Headers" => "Content-Type, X-Requested-With, X-CSRF-Token, Discourse-Visible, User-Api-Key, User-Api-Client-Id", + "Access-Control-Allow-Headers" => "Content-Type, Cache-Control, X-Requested-With, X-CSRF-Token, Discourse-Visible, User-Api-Key, User-Api-Client-Id", "Access-Control-Allow-Credentials" => "true", "Access-Control-Allow-Methods" => "POST, PUT, GET, OPTIONS, DELETE" } From b06dccac499f09891ba33580151d59ddf63fc447 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Mon, 15 Oct 2018 19:51:57 -0400 Subject: [PATCH 033/209] FIX: force enable a user's email_private_messages option when user replies via email (#6478) * Enable user email PM when posting to group or replying to topic via email * remove extra line * Add test and fix snake_case * Only reenable email_private_messages for PM replies --- lib/email/receiver.rb | 15 ++++++++++----- spec/components/email/receiver_spec.rb | 14 +++++++++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index d0d6379880..a601b27eb7 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -621,11 +621,6 @@ module Email end def create_group_post(group, user, body, elided, hidden_reason_id) - # ensure user PM emails are enabled (since user is posting via email) - if !user.staged && !user.user_option.email_private_messages - user.user_option.update!(email_private_messages: true) - end - message_ids = Email::Receiver.extract_reply_message_ids(@mail, max_message_id_count: 5) post_ids = [] @@ -648,6 +643,8 @@ module Email topic: post.topic, skip_validations: true) else + enable_email_pm_setting(user) + create_topic(user: user, raw: body, elided: elided, @@ -856,6 +853,7 @@ module Email def create_reply(options = {}) raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed? options[:post] = nil if options[:post]&.trashed? + enable_email_pm_setting(options[:user]) if options[:topic].archetype == Archetype.private_message if post_action_type = post_action_for(options[:raw]) create_post_action(options[:user], options[:post], post_action_type) @@ -1073,6 +1071,13 @@ module Email end end end + + def enable_email_pm_setting(user) + # ensure user PM emails are enabled (since user is posting via email) + if !user.staged && !user.user_option.email_private_messages + user.user_option.update!(email_private_messages: true) + end + end end end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 3eface946d..a13a825108 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -531,6 +531,18 @@ describe Email::Receiver do expect { process(:reply_user_not_matching_but_known) }.to change { topic.posts.count } end + + it "re-enables user's email_private_messages setting when user replies to a private topic" do + topic.update_columns(category_id: nil, archetype: Archetype.private_message) + topic.allowed_users << user + topic.save + + user.user_option.update_columns(email_private_messages: false) + expect { process(:reply_user_matching) }.to change { topic.posts.count } + user.reload + expect(user.user_option.email_private_messages).to eq(true) + end + end context "new message to a group" do @@ -629,7 +641,7 @@ describe Email::Receiver do expect(Post.last.raw).to match(/discourse\.rb/) end - it "enables user's email_private_messages option when user emails group" do + it "enables user's email_private_messages setting when user emails new topic to group" do user = Fabricate(:user, email: "existing@bar.com") user.user_option.update_columns(email_private_messages: false) expect { process(:group_existing_user) }.to change(Topic, :count) From 08c404e138c5dfbb6e82ac7f6e54976d30b4530a Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 16 Oct 2018 06:12:32 +0530 Subject: [PATCH 034/209] FIX: Do not set null value to remove cookie --- .../javascripts/discourse/components/global-notice.js.es6 | 2 +- .../javascripts/discourse/controllers/create-account.js.es6 | 4 ++-- app/assets/javascripts/discourse/controllers/login.js.es6 | 4 ++-- app/assets/javascripts/discourse/lib/theme-selector.js.es6 | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/components/global-notice.js.es6 b/app/assets/javascripts/discourse/components/global-notice.js.es6 index 327f6aa79a..5041da1713 100644 --- a/app/assets/javascripts/discourse/components/global-notice.js.es6 +++ b/app/assets/javascripts/discourse/components/global-notice.js.es6 @@ -11,7 +11,7 @@ export default Ember.Component.extend( let notices = []; if ($.cookie("dosp") === "1") { - $.cookie("dosp", null, { path: "/" }); + $.removeCookie("dosp", { path: "/" }); notices.push([I18n.t("forced_anonymous"), "forced-anonymous"]); } diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index 22848d8a2f..c4f1b20fc5 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -260,12 +260,12 @@ export default Ember.Controller.extend( this.get("rejectedPasswords").pushObject(attrs.accountPassword); } this.set("formSubmitted", false); - $.cookie("destination_url", null); + $.removeCookie("destination_url"); } }, () => { this.set("formSubmitted", false); - $.cookie("destination_url", null); + $.removeCookie("destination_url"); return this.flash(I18n.t("create_account.failed"), "error"); } ); diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index 7d10818420..c3aecebae0 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -158,12 +158,12 @@ export default Ember.Controller.extend(ModalFunctionality, { .val(self.get("loginPassword")); if (ssoDestinationUrl) { - $.cookie("sso_destination_url", null); + $.removeCookie("sso_destination_url"); window.location.assign(ssoDestinationUrl); return; } else if (destinationUrl) { // redirect client to the original URL - $.cookie("destination_url", null); + $.removeCookie("destination_url"); $hidden_login_form .find("input[name=redirect]") .val(destinationUrl); diff --git a/app/assets/javascripts/discourse/lib/theme-selector.js.es6 b/app/assets/javascripts/discourse/lib/theme-selector.js.es6 index ce806b4496..69a2eeb935 100644 --- a/app/assets/javascripts/discourse/lib/theme-selector.js.es6 +++ b/app/assets/javascripts/discourse/lib/theme-selector.js.es6 @@ -38,7 +38,7 @@ export function setLocalTheme(ids, themeSeq) { expires: 9999 }); } else { - $.cookie("theme_ids", null, { path: "/", expires: 1 }); + $.removeCookie("theme_ids", { path: "/", expires: 1 }); } } From 19d7543004b5bc77902b0be0e2b7148879a20654 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 16 Oct 2018 12:00:33 +1100 Subject: [PATCH 035/209] FIX: clear color scheme cache when clearing theme cache --- app/models/theme.rb | 1 + spec/models/theme_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/models/theme.rb b/app/models/theme.rb index ee6c3188cc..f3b90ff6f0 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -109,6 +109,7 @@ class Theme < ActiveRecord::Base Site.clear_anon_cache! clear_cache! ApplicationSerializer.expire_cache_fragment!("user_themes") + ColorScheme.hex_cache.clear end def self.clear_default! diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 9b7211de7a..859421ce04 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -417,6 +417,30 @@ HTML Theme.find_by(id: id).included_settings.to_json end + it 'clears color scheme cache correctly' do + Theme.destroy_all + + cs = Fabricate(:color_scheme, name: 'Fancy', color_scheme_colors: [ + Fabricate(:color_scheme_color, name: 'header_primary', hex: 'F0F0F0'), + Fabricate(:color_scheme_color, name: 'header_background', hex: '1E1E1E'), + Fabricate(:color_scheme_color, name: 'tertiary', hex: '858585') + ]) + + theme = Fabricate(:theme, + user_selectable: true, + user: Fabricate(:admin), + color_scheme_id: cs.id + ) + + theme.set_default! + + expect(ColorScheme.hex_for_name('header_primary')).to eq('F0F0F0') + + Theme.clear_default! + + expect(ColorScheme.hex_for_name('header_primary')).to eq('333333') + end + it 'handles settings cache correctly' do Theme.destroy_all From e3c6dd26c40777f87e41b5bb3e29108b5208aa55 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 16 Oct 2018 06:48:54 +0530 Subject: [PATCH 036/209] FIX: Do not set null value to remove cookie --- app/assets/javascripts/discourse/controllers/login.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index c3aecebae0..1729950acf 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -327,7 +327,7 @@ export default Ember.Controller.extend(ModalFunctionality, { $.cookie("destination_url") || options.destination_url; if (destinationUrl) { // redirect client to the original URL - $.cookie("destination_url", null); + $.removeCookie("destination_url"); window.location.href = destinationUrl; } else if (window.location.pathname === Discourse.getURL("/login")) { window.location.pathname = Discourse.getURL("/"); From bfa25487eb36185b427e83fb3a7c5c7ddf76b6d3 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 16 Oct 2018 16:19:43 +0530 Subject: [PATCH 037/209] FIX: Support for local-date email preview without time attribute --- .../lib/discourse-markdown/discourse-local-dates.js.es6 | 7 ++++--- .../discourse-local-dates/spec/lib/pretty_text_spec.rb | 9 +++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 index ab4386f622..5b63766e1a 100644 --- a/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 +++ b/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 @@ -32,19 +32,20 @@ function addLocalDate(buffer, matches, state) { ["data-timezones", state.md.utils.escapeHtml(config.timezones)] ]; + let dateTime = config.date; if (config.time) { token.attrs.push(["data-time", state.md.utils.escapeHtml(config.time)]); + dateTime = `${dateTime} ${config.time}`; } - let dateTime; if (config.timezone) { token.attrs.push([ "data-timezone", state.md.utils.escapeHtml(config.timezone) ]); - dateTime = moment.tz(`${config.date} ${config.time}`, config.timezone); + dateTime = moment.tz(dateTime, config.timezone); } else { - dateTime = moment.utc(`${config.date} ${config.time}`); + dateTime = moment.utc(dateTime); } if (config.recurring) { diff --git a/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb b/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb index 617753415f..216ac55d25 100644 --- a/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb +++ b/plugins/discourse-local-dates/spec/lib/pretty_text_spec.rb @@ -11,5 +11,14 @@ describe PrettyText do HTML expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail) + + cooked = PrettyText.cook <<~MD + [date=2018-05-08 format=LLL timezone="Europe/Berlin" timezones="Europe/Paris|America/Los_Angeles"] + MD + cooked_mail = <<~HTML +

May 8, 2018 12:00 AM (Europe: Paris)

+ HTML + + expect(PrettyText.format_for_email(cooked)).to match_html(cooked_mail) end end From d20fd66286a2125fe48ab85ed4a1557f54902cd4 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Tue, 16 Oct 2018 11:10:11 -0400 Subject: [PATCH 038/209] bump onebox to 1.8.64 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index dd079ee829..ad652e5d47 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.63' +gem 'onebox', '1.8.64' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 7842eb25de..d1d7d85d3b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -258,7 +258,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.63) + onebox (1.8.64) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -512,7 +512,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.63) + onebox (= 1.8.64) openid-redis-store pg pry-nav From b23ebf10c276cab85e3a4f8c373e6e3e897411ee Mon Sep 17 00:00:00 2001 From: Kris Date: Tue, 16 Oct 2018 12:39:55 -0400 Subject: [PATCH 039/209] Minor post alignment fixes --- app/assets/stylesheets/desktop/topic-post.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 626f52ee97..c2b7cae95b 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -48,6 +48,10 @@ section.post-menu-area { padding-left: 11px; } +.post-links-container { + margin-left: 11px; +} + nav.post-controls { padding: 0; .like-button { @@ -247,7 +251,6 @@ nav.post-controls { // WARNING: overflow hide is required for quoted / embedded images // which expect "normal" post width, but expansions are narrower overflow: hidden; - padding: 15px 25px 0 15px; } // this is covered by .topic-body .regular on a normal post // but no such class structure exists for an embedded, expanded post .cooked { @@ -273,6 +276,7 @@ nav.post-controls { &.bottom { border-top: none; margin-bottom: 20px; + margin-left: 11px; &.hidden { display: block; opacity: 0; @@ -345,7 +349,7 @@ nav.post-controls { } .topic-map { - margin: 20px 0; + margin: 20px 0 20px 11px; .map { .secondary { text-align: center; From 0db3e27ce48706e0c1b10fd1ef25e28619092f5a Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Tue, 16 Oct 2018 15:11:24 -0700 Subject: [PATCH 040/209] =?UTF-8?q?remove=20windows=20phone=20references,?= =?UTF-8?q?=20it=20is=20=E2=98=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/locales/client.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a02309cc0e..7eb98f0e67 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -772,11 +772,11 @@ en: confirm_password_description: "Please confirm your password to continue" label: "Code" enable_description: | - Scan this QR code in a supported app (AndroidiOSWindows Phone) and enter your authentication code. + Scan this QR code in a supported app (AndroidiOS and enter your authentication code. disable_description: "Please enter the authentication code from your app" show_key_description: "Enter manually" extended_description: | - Two factor authentication adds extra security to your account by requiring a one-time token in addition to your password. Tokens can be generated on Android, iOS, and Windows Phone devices. + Two factor authentication adds extra security to your account by requiring a one-time token in addition to your password. Tokens can be generated on Android and iOS devices. oauth_enabled_warning: "Please note that social logins will be disabled once two factor authentication has been enabled on your account." change_about: From f367eebb10f73dd4d1513be02c1759f09a8cc826 Mon Sep 17 00:00:00 2001 From: Matt Palmer Date: Wed, 17 Oct 2018 15:31:45 +1100 Subject: [PATCH 041/209] Override problematic .gemrc setting --- lib/plugin_gem.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugin_gem.rb b/lib/plugin_gem.rb index ee470960d6..6bf9d96a2b 100644 --- a/lib/plugin_gem.rb +++ b/lib/plugin_gem.rb @@ -6,7 +6,7 @@ module PluginGem spec_path = gems_path + "/specifications" spec_file = spec_path + "/#{name}-#{version}.gemspec" unless File.exists? spec_file - command = "gem install #{name} -v #{version} -i #{gems_path} --no-document --ignore-dependencies" + command = "gem install #{name} -v #{version} -i #{gems_path} --no-document --ignore-dependencies --no-user-install" if opts[:source] command << " --source #{opts[:source]}" end From b1d7582abe5ad52c0a092ce0f2a77ccc9ca9ca68 Mon Sep 17 00:00:00 2001 From: Matt Palmer Date: Wed, 17 Oct 2018 15:32:57 +1100 Subject: [PATCH 042/209] Run specs on discourse-prometheus-alert-receiver Sam wants to watch the world burn. --- lib/tasks/plugin.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/plugin.rake b/lib/tasks/plugin.rake index 2726c759be..c77525038f 100644 --- a/lib/tasks/plugin.rake +++ b/lib/tasks/plugin.rake @@ -7,7 +7,7 @@ task 'plugin:install_all_official' do 'discourse-nginx-performance-report', 'lazyYT', 'poll', - 'discourse-prometheus-alert-receiver' + 'discourse-calendar' ]) map = { From 5815a33a9ae728f1d39436f0d7eca81fbb0002bb Mon Sep 17 00:00:00 2001 From: Joe <33972521+hnb-ku@users.noreply.github.com> Date: Wed, 17 Oct 2018 13:52:47 +0800 Subject: [PATCH 043/209] FIX: closing an empty fullscreen composer with toggler prevents scrolling --- .../javascripts/discourse/controllers/composer.js.es6 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 8a0946c5e9..25927a467f 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -889,10 +889,6 @@ export default Ember.Controller.extend({ } ]); } else { - // in case the composer is - // cancelled while in fullscreen - $("html").removeClass("fullscreen-composer"); - // it is possible there is some sort of crazy draft with no body ... just give up on it this.destroyDraft(); this.get("model").clearState(); @@ -969,6 +965,10 @@ export default Ember.Controller.extend({ }, close() { + // the 'fullscreen-composer' class is added to remove scrollbars from the + // document while in fullscreen mode. If the composer is closed for any reason + // this class should be removed + $("html").removeClass("fullscreen-composer"); this.setProperties({ model: null, lastValidatedAt: null }); }, From 1b5ba899a1b88f186750b080a7cdb18f7cd05cdd Mon Sep 17 00:00:00 2001 From: Joe <33972521+hnb-ku@users.noreply.github.com> Date: Wed, 17 Oct 2018 14:19:20 +0800 Subject: [PATCH 044/209] UX: header items wrap on small screens for anon --- app/assets/stylesheets/mobile/header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/mobile/header.scss b/app/assets/stylesheets/mobile/header.scss index 3a7ecb8797..e4d2abb904 100644 --- a/app/assets/stylesheets/mobile/header.scss +++ b/app/assets/stylesheets/mobile/header.scss @@ -13,7 +13,7 @@ .d-header { #site-logo { - max-width: 9.2857em; + max-width: 8.8em; } // some protection for text-only site titles From 42c405a8200f9e481bcd64a18beabed9a8dea8d4 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 17 Oct 2018 13:49:32 +0530 Subject: [PATCH 045/209] FIX: use topic summary for meta description if topic excerpt is blank --- app/controllers/topics_controller.rb | 2 +- spec/requests/topics_controller_spec.rb | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index a647d466f7..fd93edbb7b 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -845,7 +845,7 @@ class TopicsController < ApplicationController respond_to do |format| format.html do - @description_meta = @topic_view.topic.excerpt || @topic_view.summary + @description_meta = @topic_view.topic.excerpt.present? ? @topic_view.topic.excerpt : @topic_view.summary store_preloaded("topic_#{@topic_view.topic.id}", MultiJson.dump(topic_view_serializer)) render :show end diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index 94c9a01005..c15c9770eb 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -1407,6 +1407,17 @@ RSpec.describe TopicsController do expect(response.headers['Discourse-Readonly']).to eq('true') end end + + describe "image only topic" do + it "uses image alt tag for meta description" do + post = Fabricate(:post, raw: "![image_description|690x405](upload://sdtr5O5xaxf0iEOxICxL36YRj86.png)") + + get post.topic.url + + body = response.body + expect(body).to have_tag(:meta, with: { name: 'description', content: '[image_description]' }) + end + end end describe '#post_ids' do From c6f364224e4b0c228e3005f918632cb32657f06a Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 17 Oct 2018 10:33:27 +0100 Subject: [PATCH 046/209] FEATURE: Allow plugins to whitelist user custom fields for public display (#6499) This works exactly the same as `whitelist_staff_user_custom_fields`, but is not limited to staff --- app/models/user.rb | 12 ++++++++++++ lib/plugin/instance.rb | 6 ++++++ spec/serializers/user_serializer_spec.rb | 7 +++++++ 3 files changed, 25 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 4f3c56ee34..06b6ba89b6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -252,9 +252,21 @@ class User < ActiveRecord::Base plugin_staff_user_custom_fields[custom_field_name] = plugin end + def self.plugin_public_user_custom_fields + @plugin_public_user_custom_fields ||= {} + end + + def self.register_plugin_public_custom_field(custom_field_name, plugin) + plugin_public_user_custom_fields[custom_field_name] = plugin + end + def self.whitelisted_user_custom_fields(guardian) fields = [] + plugin_public_user_custom_fields.each do |k, v| + fields << k if v.enabled? + end + if SiteSetting.public_user_custom_fields.present? fields += SiteSetting.public_user_custom_fields.split('|') end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index bc198b3932..ac575b1c72 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -121,6 +121,12 @@ class Plugin::Instance end end + def whitelist_public_user_custom_field(field) + reloadable_patch do |plugin| + ::User.register_plugin_public_custom_field(field, plugin) # plugin.enabled? is checked at runtime + end + end + def register_editable_user_custom_field(field) reloadable_patch do |plugin| ::User.register_plugin_editable_user_custom_field(field, plugin) # plugin.enabled? is checked at runtime diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb index a637d13137..c02ac89f30 100644 --- a/spec/serializers/user_serializer_spec.rb +++ b/spec/serializers/user_serializer_spec.rb @@ -195,6 +195,13 @@ describe UserSerializer do expect(json[:custom_fields]['public_field']).to eq(user.custom_fields['public_field']) expect(json[:custom_fields]['secret_field']).to eq(nil) end + + it "serializes the fields listed in plugin_public_user_custom_fields" do + plugin = Plugin::Instance.new + plugin.whitelist_public_user_custom_field :public_field + expect(json[:custom_fields]['public_field']).to eq(user.custom_fields['public_field']) + expect(json[:custom_fields]['secret_field']).to eq(nil) + end end context "with user_api_keys" do From 501ac4dfa6bb44cca0b71057449550bf53734ad8 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 17 Oct 2018 10:54:22 +0100 Subject: [PATCH 047/209] DEV: Cleanup properly after user_serializer test --- spec/serializers/user_serializer_spec.rb | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb index c02ac89f30..b977a3aaab 100644 --- a/spec/serializers/user_serializer_spec.rb +++ b/spec/serializers/user_serializer_spec.rb @@ -196,11 +196,20 @@ describe UserSerializer do expect(json[:custom_fields]['secret_field']).to eq(nil) end - it "serializes the fields listed in plugin_public_user_custom_fields" do - plugin = Plugin::Instance.new - plugin.whitelist_public_user_custom_field :public_field - expect(json[:custom_fields]['public_field']).to eq(user.custom_fields['public_field']) - expect(json[:custom_fields]['secret_field']).to eq(nil) + context "with user custom field" do + before do + plugin = Plugin::Instance.new + plugin.whitelist_public_user_custom_field :public_field + end + + after do + User.plugin_public_user_custom_fields.clear + end + + it "serializes the fields listed in plugin_public_user_custom_fields" do + expect(json[:custom_fields]['public_field']).to eq(user.custom_fields['public_field']) + expect(json[:custom_fields]['secret_field']).to eq(nil) + end end end From 065bf0762cc0d058eabc2db33c56f026e4a60230 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 17 Oct 2018 14:15:48 +0100 Subject: [PATCH 048/209] FEATURE: New plugin outlets for user card customization --- .../discourse/templates/components/user-card-contents.hbs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs index 39aed70a28..a207813eda 100644 --- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -142,6 +142,9 @@ {{plugin-outlet name="user-card-metadata" args=(hash user=user)}} {{/unless}} + + {{plugin-outlet name="user-card-after-metadata" args=(hash user=user)}} + {{/if}} {{#if publicUserFields}} @@ -157,6 +160,8 @@ {{/if}} + {{plugin-outlet name="user-card-before-badges" args=(hash user=user)}} + {{#if showBadges}} {{#if user.featured_user_badges}}
From f60b10d090171c49134b20abcfb089ebb94fad21 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Wed, 17 Oct 2018 16:35:32 +0300 Subject: [PATCH 049/209] UX: Warn users if the post that's currently edited has changed. (#6498) --- .../discourse/models/composer.js.es6 | 20 +++++++++++---- app/controllers/posts_controller.rb | 7 +++++- config/locales/client.en.yml | 1 + config/locales/server.en.yml | 1 + spec/requests/posts_controller_spec.rb | 7 ++++++ .../composer-edit-conflict-test.js.es6 | 25 +++++++++++++++++++ 6 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 test/javascripts/acceptance/composer-edit-conflict-test.js.es6 diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index a86e48de2d..aae9a31055 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -387,9 +387,14 @@ const Composer = RestModel.extend({ return SAVE_ICONS[action]; }, - @computed("action", "whisper") - saveLabel(action, whisper) { - return whisper ? "composer.create_whisper" : SAVE_LABELS[action]; + @computed("action", "whisper", "editConflict") + saveLabel(action, whisper, editConflict) { + if (editConflict) { + return "composer.overwrite_edit"; + } else if (whisper) { + return "composer.create_whisper"; + } + return SAVE_LABELS[action]; }, hasMetaData: function() { @@ -727,7 +732,8 @@ const Composer = RestModel.extend({ composerOpened: null, composerTotalOpened: 0, featuredLink: null, - noBump: false + noBump: false, + editConflict: false }); }, @@ -762,6 +768,7 @@ const Composer = RestModel.extend({ const props = { raw: this.get("reply"), + raw_old: this.get("editConflict") ? null : this.get("originalText"), edit_reason: opts.editReason, image_sizes: opts.imageSizes, cooked: this.getCookedHtml() @@ -769,9 +776,12 @@ const Composer = RestModel.extend({ this.set("composeState", SAVING); - let rollback = throwAjaxError(() => { + let rollback = throwAjaxError(error => { post.set("cooked", oldCooked); this.set("composeState", OPEN); + if (error.jqXHR && error.jqXHR.status === 409) { + this.set("editConflict", true); + } }); return promise diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index eb30dd808c..640679f655 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -200,7 +200,7 @@ class PostsController < ApplicationController post.image_sizes = params[:image_sizes] if params[:image_sizes].present? if !guardian.send("can_edit?", post) && post.user_id == current_user.id && post.edit_time_limit_expired? - return render json: { errors: [I18n.t('too_late_to_edit')] }, status: 422 + return render_json_error(I18n.t('too_late_to_edit')) end guardian.ensure_can_edit!(post) @@ -210,6 +210,11 @@ class PostsController < ApplicationController edit_reason: params[:post][:edit_reason] } + raw_old = params[:post][:raw_old] + if raw_old.present? && raw_old != post.raw + return render_json_error(I18n.t('edit_conflict'), status: 409) + end + # to stay consistent with the create api, we allow for title & category changes here if post.is_first_post? changes[:title] = params[:title] if params[:title] diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 7eb98f0e67..918e12d8b0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1372,6 +1372,7 @@ en: tags_missing: "You must choose at least {{count}} tags" save_edit: "Save Edit" + overwrite_edit: "Overwrite Edit" reply_original: "Reply on Original Topic" reply_here: "Reply Here" reply: "Reply" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index adadf4da71..1b841ad84c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -304,6 +304,7 @@ en: badge: "%{display_name} badge on %{site_title}" too_late_to_edit: "That post was created too long ago. It can no longer be edited or deleted." + edit_conflict: "That post was edited by another user and your changes can no longer be saved." revert_version_same: "The current version is same as the version you are trying to revert to." excerpt_image: "image" diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb index 57ffcea60b..fcf74331fb 100644 --- a/spec/requests/posts_controller_spec.rb +++ b/spec/requests/posts_controller_spec.rb @@ -295,6 +295,13 @@ describe PostsController do expect(post.raw).to eq("edited body") end + it 'checks for an edit conflict' do + update_params[:post][:raw_old] = 'old body' + put "/posts/#{post.id}.json", params: update_params + + expect(response.status).to eq(409) + end + it "raises an error when the post parameter is missing" do update_params.delete(:post) put "/posts/#{post.id}.json", params: update_params diff --git a/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 new file mode 100644 index 0000000000..3dc48b8f1d --- /dev/null +++ b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 @@ -0,0 +1,25 @@ +import { acceptance } from "helpers/qunit-helpers"; + +acceptance("Composer - Edit conflict", { + loggedIn: true, + + pretend(server, helper) { + server.put("/posts/398", () => { + return helper.response(409, { errors: ["edit conflict"] }); + }); + } +}); + +QUnit.test("Edit a post that causes an edit conflict", async assert => { + 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 click("#reply-control button.create"); + assert.equal( + find("#reply-control button.create") + .text() + .trim(), + I18n.t("composer.overwrite_edit"), + "it shows the overwrite button" + ); +}); From ee18d9ace09b0b2919867c905cda72f85f61efb4 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 17 Oct 2018 16:04:43 +0200 Subject: [PATCH 050/209] FIX: mbox importer and rake task were broken --- lib/email/receiver.rb | 6 +++--- script/import_scripts/mbox/importer.rb | 18 +++++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index a601b27eb7..8a95dbf6d3 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -890,12 +890,12 @@ module Email def create_post_with_attachments(options = {}) # deal with attachments - options[:raw] = add_attachments(options[:raw], options[:user], options) + options[:raw] = add_attachments(options[:raw], options[:user].id, options) create_post(options) end - def add_attachments(raw, user, options = {}) + def add_attachments(raw, user_id, options = {}) rejected_attachments = [] attachments.each do |attachment| tmp = Tempfile.new(["discourse-email-attachment", File.extname(attachment.filename)]) @@ -904,7 +904,7 @@ module Email File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } # create the upload for the user opts = { for_group_message: options[:is_group_message] } - upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(user.id) + upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(user_id) if upload&.valid? # try to inline images if attachment.content_type&.start_with?("image/") diff --git a/script/import_scripts/mbox/importer.rb b/script/import_scripts/mbox/importer.rb index db0269d888..dbccf64c32 100644 --- a/script/import_scripts/mbox/importer.rb +++ b/script/import_scripts/mbox/importer.rb @@ -93,13 +93,17 @@ module ImportScripts::Mbox next if all_records_exist?(:posts, rows.map { |row| row['msg_id'] }) create_posts(rows, total: total_count, offset: offset) do |row| - if row['email_date'].blank? - puts "Date is missing. Skipping #{row['msg_id']}" - nil - elsif row['in_reply_to'].blank? - map_first_post(row) - else - map_reply(row) + begin + if row['email_date'].blank? + puts "Date is missing. Skipping #{row['msg_id']}" + nil + elsif row['in_reply_to'].blank? + map_first_post(row) + else + map_reply(row) + end + rescue => e + puts "Failed to map post for #{row['msg_id']}", e, e.backtrace.join("\n") end end end From 341836eb42a3377f730c2bdf5f1ca086245a5955 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 17 Oct 2018 16:48:09 +0200 Subject: [PATCH 051/209] Fix the rake task and importer instead --- lib/email/receiver.rb | 6 +++--- lib/tasks/posts.rake | 2 +- script/import_scripts/mbox/importer.rb | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 8a95dbf6d3..a601b27eb7 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -890,12 +890,12 @@ module Email def create_post_with_attachments(options = {}) # deal with attachments - options[:raw] = add_attachments(options[:raw], options[:user].id, options) + options[:raw] = add_attachments(options[:raw], options[:user], options) create_post(options) end - def add_attachments(raw, user_id, options = {}) + def add_attachments(raw, user, options = {}) rejected_attachments = [] attachments.each do |attachment| tmp = Tempfile.new(["discourse-email-attachment", File.extname(attachment.filename)]) @@ -904,7 +904,7 @@ module Email File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } # create the upload for the user opts = { for_group_message: options[:is_group_message] } - upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(user_id) + upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(user.id) if upload&.valid? # try to inline images if attachment.content_type&.start_with?("image/") diff --git a/lib/tasks/posts.rake b/lib/tasks/posts.rake index d28dc66562..0c78987a0f 100644 --- a/lib/tasks/posts.rake +++ b/lib/tasks/posts.rake @@ -277,7 +277,7 @@ task 'posts:refresh_emails', [:topic_id] => [:environment] do |_, args| receiver = Email::Receiver.new(post.raw_email) body, elided = receiver.select_body - body = receiver.add_attachments(body || '', post.user_id) + body = receiver.add_attachments(body || '', post.user) body << Email::Receiver.elided_html(elided) if elided.present? post.revise(Discourse.system_user, { raw: body, cook_method: Post.cook_methods[:regular] }, diff --git a/script/import_scripts/mbox/importer.rb b/script/import_scripts/mbox/importer.rb index dbccf64c32..30807b8486 100644 --- a/script/import_scripts/mbox/importer.rb +++ b/script/import_scripts/mbox/importer.rb @@ -131,7 +131,8 @@ module ImportScripts::Mbox if row['attachment_count'].positive? receiver = Email::Receiver.new(row['raw_message']) - body = receiver.add_attachments(body, user_id) + user = User.find(user_id) + body = receiver.add_attachments(body, user) end body << Email::Receiver.elided_html(elided) if elided.present? From 0abc932056629e595c00391baff76dc07f76090a Mon Sep 17 00:00:00 2001 From: Guto Foletto Date: Wed, 17 Oct 2018 12:37:14 -0300 Subject: [PATCH 052/209] add styles so permalinks admin could fit mobile screen (#6496) --- .../admin/templates/permalinks.hbs | 6 +++-- .../stylesheets/common/admin/customize.scss | 25 ++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/templates/permalinks.hbs b/app/assets/javascripts/admin/templates/permalinks.hbs index 068d1afdca..4dd4300599 100644 --- a/app/assets/javascripts/admin/templates/permalinks.hbs +++ b/app/assets/javascripts/admin/templates/permalinks.hbs @@ -1,5 +1,7 @@ - -
+ + {{permalink-form action="recordAdded"}} diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index ce73260b58..9c8cadc664 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -574,19 +574,38 @@ .external_url, .post { text-overflow: ellipsis; - white-space: nowrap; + } + + &.grid tr.admin-list-item { + grid-template-columns: unset; + } +} + +.permalink-search { + text-align: left; + @media screen and (min-width: map-get($breakpoints, tablet)) { + text-align: right; } } .permalink-form { + align-items: flex-start; display: flex; - align-items: center; + flex-direction: column; + flex-wrap: wrap; + @media screen and (min-width: map-get($breakpoints, tablet)) { + align-items: center; + flex-direction: row; + } .select-kit { width: 150px; } input { - margin: 0 5px; + margin: 5px 0; + @media screen and (min-width: map-get($breakpoints, tablet)) { + margin: 0 5px; + } } } From 21d804fc9217f949bc88742e4abcdc5b794cd2c6 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Wed, 17 Oct 2018 18:39:25 +0300 Subject: [PATCH 053/209] DEV: Fix build. (#6500) --- .../javascripts/acceptance/composer-edit-conflict-test.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 index 3dc48b8f1d..7616e1453b 100644 --- a/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 +++ b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 @@ -4,14 +4,14 @@ acceptance("Composer - Edit conflict", { loggedIn: true, pretend(server, helper) { - server.put("/posts/398", () => { + server.put("/posts/18", () => { return helper.response(409, { errors: ["edit conflict"] }); }); } }); QUnit.test("Edit a post that causes an edit conflict", async assert => { - await visit("/t/internationalization-localization/280"); + await visit("/t/this-is-a-test-topic/9"); await click(".topic-post:eq(0) button.show-more-actions"); await click(".topic-post:eq(0) button.edit"); await click("#reply-control button.create"); From cc27d61f9e9a387413884e5f21e8595449f2ea30 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 17 Oct 2018 18:32:07 +0200 Subject: [PATCH 054/209] FIX: discourse script didn't allow backups with paths anymore This restores the previous functionality. The script now allows the following options: * `discourse backup` (uses the system generated filename) * `discourse backup ` (uses the provided filename) * `discourse backup ` (moves the backup to the provided path with the given filename) Remote backup stores do not support the last option. Some file extensions (like `.tar.gz`) are automatically removed from the provided filename. --- script/discourse | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/script/discourse b/script/discourse index ddfc618193..205b9be545 100755 --- a/script/discourse +++ b/script/discourse @@ -61,23 +61,43 @@ class DiscourseCLI < Thor require "backup_restore/backup_restore" require "backup_restore/backuper" - puts "Starting backup..." - backuper = BackupRestore::Backuper.new(Discourse.system_user.id, filename: filename) - backup_filename = backuper.run - puts "Backup done." - store = BackupRestore::BackupStore.create + if filename + destination_directory = File.dirname(filename).sub(/^\.$/, '') + + if destination_directory.present? && store.remote? + puts "Only local backup storage supports paths." + exit(1) + end + + filename_without_extension = File.basename(filename).sub(/\.(sql\.)?(tar\.gz|t?gz)$/i, '') + end + + puts "Starting backup..." + backuper = BackupRestore::Backuper.new(Discourse.system_user.id, filename: filename_without_extension) + backup_filename = backuper.run + exit(1) unless backuper.success + + puts "Backup done." + if store.remote? location = BackupLocationSiteSetting.values.find { |v| v[:value] == SiteSetting.backup_location } location = I18n.t("admin_js.#{location[:name]}") if location puts "Output file is stored on #{location} as #{backup_filename}", "" else backup = store.file(backup_filename, include_download_source: true) - puts "Output file is in: #{backup.source}", "" - end - exit(1) unless backuper.success + if destination_directory.present? + puts "Moving backup file..." + backup_path = File.join(destination_directory, backup_filename) + FileUtils.mv(backup.source, backup_path) + else + backup_path = backup.source + end + + puts "Output file is in: #{backup_path}", "" + end end desc "export", "Backup a Discourse forum" From 69dbd9b0692dbc36c3116658abfa3e56abc3fc9a Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Wed, 17 Oct 2018 20:27:56 +0300 Subject: [PATCH 055/209] DEV: Comment test causing issues. --- .../composer-edit-conflict-test.js.es6 | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 index 7616e1453b..7936bc26b3 100644 --- a/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 +++ b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 @@ -1,25 +1,25 @@ -import { acceptance } from "helpers/qunit-helpers"; +// import { acceptance } from "helpers/qunit-helpers"; -acceptance("Composer - Edit conflict", { - loggedIn: true, +// acceptance("Composer - Edit conflict", { +// loggedIn: true, - pretend(server, helper) { - server.put("/posts/18", () => { - return helper.response(409, { errors: ["edit conflict"] }); - }); - } -}); +// pretend(server, helper) { +// server.put("/posts/18", () => { +// return helper.response(409, { errors: ["edit conflict"] }); +// }); +// } +// }); -QUnit.test("Edit a post that causes an edit conflict", async assert => { - await visit("/t/this-is-a-test-topic/9"); - await click(".topic-post:eq(0) button.show-more-actions"); - await click(".topic-post:eq(0) button.edit"); - await click("#reply-control button.create"); - assert.equal( - find("#reply-control button.create") - .text() - .trim(), - I18n.t("composer.overwrite_edit"), - "it shows the overwrite button" - ); -}); +// QUnit.test("Edit a post that causes an edit conflict", async assert => { +// await visit("/t/this-is-a-test-topic/9"); +// await click(".topic-post:eq(0) button.show-more-actions"); +// await click(".topic-post:eq(0) button.edit"); +// await click("#reply-control button.create"); +// assert.equal( +// find("#reply-control button.create") +// .text() +// .trim(), +// I18n.t("composer.overwrite_edit"), +// "it shows the overwrite button" +// ); +// }); From 44eba0bb608d2cef9651cc25eeb06e3e8841ee7d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 18 Oct 2018 10:52:45 +0800 Subject: [PATCH 056/209] FIX: Don't rescue `PG::UniqueViolation` within a transaction. Also acquire a transaction per link instead of failing when any of the links can't be processed. This prevents ActiveRecord from rolling back the transaction and the next SQL statement sent to PG will fail. This is however hard to test as it only happens when there are two competing process trying to process this method at the same time. --- app/models/topic_link.rb | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb index 22f8feeab7..229e2eb835 100644 --- a/app/models/topic_link.rb +++ b/app/models/topic_link.rb @@ -112,24 +112,23 @@ SQL return if post.blank? || post.whisper? added_urls = [] - TopicLink.transaction do + reflected_ids = [] - added_urls = [] - reflected_ids = [] - - PrettyText - .extract_links(post.cooked) - .map do |u| - uri = begin - URI.parse(u.url) - rescue URI::Error - end - - [u, uri] + PrettyText + .extract_links(post.cooked) + .map do |u| + uri = begin + URI.parse(u.url) + rescue URI::Error end - .reject { |_, p| p.nil? || "mailto".freeze == p.scheme } - .uniq { |_, p| p } - .each do |link, parsed| + + [u, uri] + end + .reject { |_, p| p.nil? || "mailto".freeze == p.scheme } + .uniq { |_, p| p } + .each do |link, parsed| + + TopicLink.transaction do begin url = link.url internal = false @@ -185,7 +184,7 @@ SQL link_post_id: reflected_post.try(:id), quote: link.is_quote, extension: file_extension) - rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation + rescue ActiveRecord::RecordNotUnique # it's fine end end From 287e780f223247e52ac4e74c5ea936a81a3fa0ba Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 18 Oct 2018 11:05:47 +0800 Subject: [PATCH 057/209] DEV: Skip tests instead of commenting out. --- .../composer-edit-conflict-test.js.es6 | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 index 7936bc26b3..9cd0eb104b 100644 --- a/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 +++ b/test/javascripts/acceptance/composer-edit-conflict-test.js.es6 @@ -1,25 +1,25 @@ -// import { acceptance } from "helpers/qunit-helpers"; +import { acceptance } from "helpers/qunit-helpers"; -// acceptance("Composer - Edit conflict", { -// loggedIn: true, +acceptance("Composer - Edit conflict", { + loggedIn: true, -// pretend(server, helper) { -// server.put("/posts/18", () => { -// return helper.response(409, { errors: ["edit conflict"] }); -// }); -// } -// }); + pretend(server, helper) { + server.put("/posts/18", () => { + return helper.response(409, { errors: ["edit conflict"] }); + }); + } +}); -// QUnit.test("Edit a post that causes an edit conflict", async assert => { -// await visit("/t/this-is-a-test-topic/9"); -// await click(".topic-post:eq(0) button.show-more-actions"); -// await click(".topic-post:eq(0) button.edit"); -// await click("#reply-control button.create"); -// assert.equal( -// find("#reply-control button.create") -// .text() -// .trim(), -// I18n.t("composer.overwrite_edit"), -// "it shows the overwrite button" -// ); -// }); +QUnit.skip("Edit a post that causes an edit conflict", async assert => { + await visit("/t/this-is-a-test-topic/9"); + await click(".topic-post:eq(0) button.show-more-actions"); + await click(".topic-post:eq(0) button.edit"); + await click("#reply-control button.create"); + assert.equal( + find("#reply-control button.create") + .text() + .trim(), + I18n.t("composer.overwrite_edit"), + "it shows the overwrite button" + ); +}); From 22408f93c946eb0cb08593ace15a29923fc2a4ce Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 18 Oct 2018 12:23:04 +0800 Subject: [PATCH 058/209] FIX: Wrap custom fields database statements in a transaction. Kind of strange that we don't do it because a database statement may fail and leave us in a weird state. --- app/models/concerns/has_custom_fields.rb | 77 ++++++++++++------------ 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb index 0a0b2edebe..d10f180d90 100644 --- a/app/models/concerns/has_custom_fields.rb +++ b/app/models/concerns/has_custom_fields.rb @@ -194,54 +194,55 @@ module HasCustomFields def save_custom_fields(force = false) if force || !custom_fields_clean? dup = @custom_fields.dup - array_fields = {} - _custom_fields.reload.each do |f| - if dup[f.name].is_a?(Array) - # we need to collect Arrays fully before we can compare them - if !array_fields.has_key?(f.name) - array_fields[f.name] = [f] + ActiveRecord::Base.transaction do + _custom_fields.reload.each do |f| + if dup[f.name].is_a?(Array) + # we need to collect Arrays fully before we can compare them + if !array_fields.has_key?(f.name) + array_fields[f.name] = [f] + else + array_fields[f.name] << f + end + elsif dup[f.name].is_a?(Hash) + if dup[f.name].to_json != f.value + f.destroy! + else + dup.delete(f.name) + end else - array_fields[f.name] << f - end - elsif dup[f.name].is_a? Hash - if dup[f.name].to_json != f.value - f.destroy! - else - dup.delete(f.name) - end - else - t = {} - self.class.append_custom_field(t, f.name, f.value) + t = {} + self.class.append_custom_field(t, f.name, f.value) - if dup[f.name] != t[f.name] - f.destroy! - else - dup.delete(f.name) + if dup[f.name] != t[f.name] + f.destroy! + else + dup.delete(f.name) + end end end - end - # let's iterate through our arrays and compare them - array_fields.each do |field_name, fields| - if fields.length == dup[field_name].length && fields.map(&:value) == dup[field_name] - dup.delete(field_name) - else - fields.each(&:destroy) + # let's iterate through our arrays and compare them + array_fields.each do |field_name, fields| + if fields.length == dup[field_name].length && fields.map(&:value) == dup[field_name] + dup.delete(field_name) + else + fields.each(&:destroy!) + end end - end - dup.each do |k, v| - field_type = self.class.get_custom_field_type(k) + dup.each do |k, v| + field_type = self.class.get_custom_field_type(k) - if v.is_a?(Array) && field_type != :json - v.each { |subv| _custom_fields.create!(name: k, value: subv) } - else - _custom_fields.create!( - name: k, - value: v.is_a?(Hash) || field_type == :json ? v.to_json : v - ) + if v.is_a?(Array) && field_type != :json + v.each { |subv| _custom_fields.create!(name: k, value: subv) } + else + _custom_fields.create!( + name: k, + value: v.is_a?(Hash) || field_type == :json ? v.to_json : v + ) + end end end From 0f1afad6dab73ab9d862fcbc05f1387d98222a28 Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Thu, 18 Oct 2018 02:05:34 -0400 Subject: [PATCH 059/209] FIX: extracted theme JavaScripts for multisite (#6502) * FIX: extracted theme javascripts for multisite * onceoff to rebake all theme fields --- app/jobs/onceoff/rebake_all_html_theme_fields.rb | 11 +++++++++++ app/models/javascript_cache.rb | 2 +- spec/jobs/rebake_all_html_theme_fields_spec.rb | 15 +++++++++++++++ spec/models/javascript_cache_spec.rb | 7 +++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 app/jobs/onceoff/rebake_all_html_theme_fields.rb create mode 100644 spec/jobs/rebake_all_html_theme_fields_spec.rb diff --git a/app/jobs/onceoff/rebake_all_html_theme_fields.rb b/app/jobs/onceoff/rebake_all_html_theme_fields.rb new file mode 100644 index 0000000000..afdce8abc1 --- /dev/null +++ b/app/jobs/onceoff/rebake_all_html_theme_fields.rb @@ -0,0 +1,11 @@ +module Jobs + class RebakeAllHtmlThemeFields < Jobs::Onceoff + def execute_onceoff(args) + ThemeField.where(type_id: ThemeField.types[:html]).find_each do |theme_field| + theme_field.update(value_baked: nil) + end + + Theme.clear_cache! + end + end +end diff --git a/app/models/javascript_cache.rb b/app/models/javascript_cache.rb index e11c40feb2..cb850af6de 100644 --- a/app/models/javascript_cache.rb +++ b/app/models/javascript_cache.rb @@ -7,7 +7,7 @@ class JavascriptCache < ActiveRecord::Base before_save :update_digest def url - "#{GlobalSetting.cdn_url}#{GlobalSetting.relative_url_root}/theme-javascripts/#{digest}.js" + "#{GlobalSetting.cdn_url}#{GlobalSetting.relative_url_root}/theme-javascripts/#{digest}.js?__ws=#{Discourse.current_hostname}" end private diff --git a/spec/jobs/rebake_all_html_theme_fields_spec.rb b/spec/jobs/rebake_all_html_theme_fields_spec.rb new file mode 100644 index 0000000000..91dd4828f7 --- /dev/null +++ b/spec/jobs/rebake_all_html_theme_fields_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +describe Jobs::RebakeAllHtmlThemeFields do + let(:theme) { Fabricate(:theme) } + let(:theme_field) { ThemeField.create!(theme: theme, target_id: 0, name: "header", value: "") } + + it 'extracts inline javascripts' do + theme_field.update_attributes(value_baked: 'need to be rebaked') + + described_class.new.execute_onceoff({}) + + theme_field.reload + expect(theme_field.value_baked).to include('theme-javascripts') + end +end diff --git a/spec/models/javascript_cache_spec.rb b/spec/models/javascript_cache_spec.rb index 1599fbe92e..28b06a2ea8 100644 --- a/spec/models/javascript_cache_spec.rb +++ b/spec/models/javascript_cache_spec.rb @@ -29,4 +29,11 @@ RSpec.describe JavascriptCache, type: :model do expect(javascript_cache.errors.details[:content]).to include(error: :empty) end end + + describe 'url' do + it 'works with multisite' do + javascript_cache = JavascriptCache.create!(content: 'console.log("hello");', theme_field: theme_field) + expect(javascript_cache.url).to include("?__ws=test.localhost") + end + end end From bbf542da01229d782e46def5a37d65f4c1565b4d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 18 Oct 2018 14:17:10 +0800 Subject: [PATCH 060/209] DEV: Prefer `<<~` over `<<`. --- spec/models/theme_field_spec.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/models/theme_field_spec.rb b/spec/models/theme_field_spec.rb index 89aa19ebfb..b129c3143c 100644 --- a/spec/models/theme_field_spec.rb +++ b/spec/models/theme_field_spec.rb @@ -33,15 +33,15 @@ describe ThemeField do end it 'only extracts inline javascript to an external file' do - html = < - var a = "inline discourse plugin"; - - - -HTML + html = <<~HTML + + + + HTML theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html) From 5f2fb0fe334943f4e54afc8d1f6f72905203df7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 18 Oct 2018 10:21:12 +0200 Subject: [PATCH 061/209] Show original options when an error happens while importing an user --- script/import_scripts/base.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index 880150c11f..2546b661ea 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -283,6 +283,7 @@ class ImportScripts::Base end def create_user(opts, import_id) + original_opts = opts.dup opts.delete(:id) merge = opts.delete(:merge) post_create_action = opts.delete(:post_create_action) @@ -360,7 +361,7 @@ class ImportScripts::Base u = existing end else - puts "Error on record: #{opts.inspect}" + puts "Error on record: #{original_opts.inspect}" raise e end end From 53aa0344bff63d1807267003c0f4db2bc4786378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 18 Oct 2018 10:22:55 +0200 Subject: [PATCH 062/209] FIX: properly import vBulletin's hashed password --- script/import_scripts/vbulletin.rb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/script/import_scripts/vbulletin.rb b/script/import_scripts/vbulletin.rb index 23923d0baa..4c5216a679 100644 --- a/script/import_scripts/vbulletin.rb +++ b/script/import_scripts/vbulletin.rb @@ -126,8 +126,15 @@ EOM batches(BATCH_SIZE) do |offset| users = mysql_query(<<-SQL - SELECT userid, username, homepage, usertitle, usergroupid, joindate, email, - CONCAT(password, ':', salt) AS crypted_password + SELECT userid + , username + , homepage + , usertitle + , usergroupid + , joindate + , email + , password + , salt FROM #{TABLE_PREFIX}user WHERE userid > #{last_user_id} ORDER BY userid @@ -145,13 +152,15 @@ EOM email = user["email"].presence || fake_email email = fake_email unless email[EmailValidator.email_regex] + password = [user["password"].presence, user["salt"].presence].compact.join(":") + username = @htmlentities.decode(user["username"]).strip { id: user["userid"], name: username, username: username, - password: user["crypted_password"], + password: password, email: email, website: user["homepage"].strip, title: @htmlentities.decode(user["usertitle"]).strip, From 3973823a33f2215577e84e5b757f3c4f40b4cece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 18 Oct 2018 11:02:54 +0200 Subject: [PATCH 063/209] FIX: always update 'last_gravatar_download_attempt' when updating gravatar --- app/models/user_avatar.rb | 12 ++-- spec/models/user_avatar_spec.rb | 99 +++++++++++++++++++-------------- 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index 69d0ff7764..fd3fb8e4bb 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -13,12 +13,10 @@ class UserAvatar < ActiveRecord::Base def update_gravatar! DistributedMutex.synchronize("update_gravatar_#{user_id}") do begin - # special logic for our system user - email_hash = user_id == Discourse::SYSTEM_USER_ID ? User.email_hash("info@discourse.org") : user.email_hash - - self.last_gravatar_download_attempt = Time.new + self.update_columns(last_gravatar_download_attempt: Time.now) max = Discourse.avatar_sizes.max + email_hash = user_id == Discourse::SYSTEM_USER_ID ? User.email_hash("info@discourse.org") : user.email_hash gravatar_url = "https://www.gravatar.com/avatar/#{email_hash}.png?s=#{max}&d=404" # follow redirects in case gravatar change rules on us @@ -42,12 +40,10 @@ class UserAvatar < ActiveRecord::Base type: "avatar" ).create_for(user_id) - upload_id = upload.id - - if gravatar_upload_id != upload_id + if gravatar_upload_id != upload.id User.transaction do if gravatar_upload_id && user.uploaded_avatar_id == gravatar_upload_id - user.update!(uploaded_avatar_id: upload_id) + user.update!(uploaded_avatar_id: upload.id) end gravatar_upload&.destroy! diff --git a/spec/models/user_avatar_spec.rb b/spec/models/user_avatar_spec.rb index 5e9af7a5ea..b52303d7b7 100644 --- a/spec/models/user_avatar_spec.rb +++ b/spec/models/user_avatar_spec.rb @@ -8,62 +8,79 @@ describe UserAvatar do let(:temp) { Tempfile.new('test') } let(:upload) { Fabricate(:upload, user: user) } - before do - temp.binmode - # tiny valid png - temp.write(Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==")) - temp.rewind - FileHelper.expects(:download).returns(temp) - end + describe "when working" do - after do - temp.unlink - end + before do + temp.binmode + # tiny valid png + temp.write(Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==")) + temp.rewind + FileHelper.expects(:download).returns(temp) + end - it 'can update gravatars' do - expect do - avatar.update_gravatar! - end.to change { Upload.count }.by(1) + after do + temp.unlink + end - upload = Upload.last + it 'can update gravatars' do + freeze_time Time.now - expect(avatar.gravatar_upload).to eq(upload) - expect(user.reload.uploaded_avatar).to eq(nil) - end + expect { avatar.update_gravatar! }.to change { Upload.count }.by(1) - describe 'when user has an existing custom upload' do - it "should not change the user's uploaded avatar" do - user.update!(uploaded_avatar: upload) - - avatar.update!( - custom_upload: upload, - gravatar_upload: Fabricate(:upload, user: user) - ) - - avatar.update_gravatar! - - expect(upload.reload).to eq(upload) - expect(user.reload.uploaded_avatar).to eq(upload) - expect(avatar.reload.custom_upload).to eq(upload) expect(avatar.gravatar_upload).to eq(Upload.last) + expect(avatar.last_gravatar_download_attempt).to eq(Time.now) + expect(user.reload.uploaded_avatar).to eq(nil) + end + + describe 'when user has an existing custom upload' do + it "should not change the user's uploaded avatar" do + user.update!(uploaded_avatar: upload) + + avatar.update!( + custom_upload: upload, + gravatar_upload: Fabricate(:upload, user: user) + ) + + avatar.update_gravatar! + + expect(upload.reload).to eq(upload) + expect(user.reload.uploaded_avatar).to eq(upload) + expect(avatar.reload.custom_upload).to eq(upload) + expect(avatar.gravatar_upload).to eq(Upload.last) + end + end + + describe 'when user has an existing gravatar' do + it "should update the user's uploaded avatar correctly" do + user.update!(uploaded_avatar: upload) + avatar.update!(gravatar_upload: upload) + + avatar.update_gravatar! + + expect(Upload.find_by(id: upload.id)).to eq(nil) + + new_upload = Upload.last + + expect(user.reload.uploaded_avatar).to eq(new_upload) + expect(avatar.reload.gravatar_upload).to eq(new_upload) + end end end - describe 'when user has an existing gravatar' do - it "should update the user's uploaded avatar correctly" do - user.update!(uploaded_avatar: upload) - avatar.update!(gravatar_upload: upload) + describe "when failing" do - avatar.update_gravatar! + it "always update 'last_gravatar_download_attempt'" do + freeze_time Time.now - expect(Upload.find_by(id: upload.id)).to eq(nil) + FileHelper.expects(:download).raises(SocketError) - new_upload = Upload.last + expect { avatar.update_gravatar! }.to_not change { Upload.count } - expect(user.reload.uploaded_avatar).to eq(new_upload) - expect(avatar.reload.gravatar_upload).to eq(new_upload) + expect(avatar.last_gravatar_download_attempt).to eq(Time.now) end + end + end context '.import_url_for_user' do From 93485facaf5672f7472b2d0dd605d36617ad93be Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Thu, 18 Oct 2018 13:17:24 -0600 Subject: [PATCH 064/209] FIX: lowercase username for add/rem group members This fix searches for users based on the downcased username so that if you pass in usernames to add/remove from a group and you don't have the casing just right it will still find the correct users. I updated the tests to add a username that has a mix of upper and lowercase letters to verify this functionality. --- app/controllers/groups_controller.rb | 2 +- spec/requests/groups_controller_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 5dad97d0f4..b20d5e639a 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -484,7 +484,7 @@ class GroupsController < ApplicationController def users_from_params if params[:usernames].present? - users = User.where(username: params[:usernames].split(",")) + users = User.where(username_lower: params[:usernames].split(",").map(&:downcase)) raise Discourse::InvalidParameters.new(:usernames) if users.blank? elsif params[:user_ids].present? users = User.where(id: params[:user_ids].split(",")) diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 05e18d426b..15d6b9612d 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -856,7 +856,7 @@ describe GroupsController do context "is able to add several members to a group" do let(:user1) { Fabricate(:user) } - let(:user2) { Fabricate(:user) } + let(:user2) { Fabricate(:user, username: "UsEr2") } it "adds by username" do expect do @@ -1069,7 +1069,7 @@ describe GroupsController do context '#remove_members' do context "is able to remove several members from a group" do let(:user1) { Fabricate(:user) } - let(:user2) { Fabricate(:user) } + let(:user2) { Fabricate(:user, username: "UsEr2") } let(:group1) { Fabricate(:group, users: [user1, user2]) } it "removes by username" do From f1ba981ae93182a5e8ea7344db09773b7e02819c Mon Sep 17 00:00:00 2001 From: Blake Erickson Date: Thu, 18 Oct 2018 13:32:36 -0600 Subject: [PATCH 065/209] Improve add user to group spec for uppercase usernames Oops forgot to check for this. See previous commit for more details. --- spec/requests/groups_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 15d6b9612d..6b5cadad79 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -861,7 +861,7 @@ describe GroupsController do it "adds by username" do expect do put "/groups/#{group.id}/members.json", - params: { usernames: [user1.username, user2.username].join(",") } + params: { usernames: [user1.username, user2.username.upcase].join(",") } end.to change { group.users.count }.by(2) expect(response.status).to eq(200) @@ -1075,7 +1075,7 @@ describe GroupsController do it "removes by username" do expect do delete "/groups/#{group1.id}/members.json", - params: { usernames: [user1.username, user2.username].join(",") } + params: { usernames: [user1.username, user2.username.upcase].join(",") } end.to change { group1.users.count }.by(-2) expect(response.status).to eq(200) end From f0af61da415d0bbb4a49cb0eb461bd7bbeb7ea12 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 18 Oct 2018 15:49:34 -0400 Subject: [PATCH 066/209] FIX: User `AvatarLookup` for looking up avatar details (#6508) This allows plugins with their own avatar logic to work in the user summary sections. --- app/models/user_summary.rb | 55 +++++++++++++------------------------- 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/app/models/user_summary.rb b/app/models/user_summary.rb index e75a53cd52..cea0d05b58 100644 --- a/app/models/user_summary.rb +++ b/app/models/user_summary.rb @@ -60,19 +60,9 @@ class UserSummary .order('COUNT(*) DESC') .limit(MAX_SUMMARY_RESULTS) .pluck('acting_user_id, COUNT(*)') - .each { |l| likers[l[0].to_s] = l[1] } + .each { |l| likers[l[0]] = l[1] } - User.where(id: likers.keys) - .pluck(:id, :username, :name, :uploaded_avatar_id) - .map do |u| - UserWithCount.new( - id: u[0], - username: u[1], - name: u[2], - avatar_template: User.avatar_template(u[1], u[3]), - count: likers[u[0].to_s] - ) - end.sort_by { |u| -u[:count] } + user_counts(likers) end def most_liked_users @@ -85,19 +75,9 @@ class UserSummary .order('COUNT(*) DESC') .limit(MAX_SUMMARY_RESULTS) .pluck('user_actions.user_id, COUNT(*)') - .each { |l| liked_users[l[0].to_s] = l[1] } + .each { |l| liked_users[l[0]] = l[1] } - User.where(id: liked_users.keys) - .pluck(:id, :username, :name, :uploaded_avatar_id) - .map do |u| - UserWithCount.new( - id: u[0], - username: u[1], - name: u[2], - avatar_template: User.avatar_template(u[1], u[3]), - count: liked_users[u[0].to_s] - ) - end.sort_by { |u| -u[:count] } + user_counts(liked_users) end REPLY_ACTIONS ||= [UserAction::RESPONSE, UserAction::QUOTE, UserAction::MENTION] @@ -117,19 +97,9 @@ class UserSummary .order('COUNT(*) DESC') .limit(MAX_SUMMARY_RESULTS) .pluck('replies.user_id, COUNT(*)') - .each { |r| replied_users[r[0].to_s] = r[1] } + .each { |r| replied_users[r[0]] = r[1] } - User.where(id: replied_users.keys) - .pluck(:id, :username, :name, :uploaded_avatar_id) - .map do |u| - UserWithCount.new( - id: u[0], - username: u[1], - name: u[2], - avatar_template: User.avatar_template(u[1], u[3]), - count: replied_users[u[0].to_s] - ) - end.sort_by { |u| -u[:count] } + user_counts(replied_users) end def badges @@ -215,4 +185,17 @@ class UserSummary :time_read, to: :user_stat +protected + + def user_counts(user_hash) + user_ids = user_hash.keys + + lookup = AvatarLookup.new(user_ids) + user_ids.map do |user_id| + UserWithCount.new( + lookup[user_id].attributes.merge(count: user_hash[user_id]) + ) + end.sort_by { |u| -u[:count] } + end + end From 22ada32d4ddc172654534db8d35e61b6d84f78b7 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Fri, 19 Oct 2018 03:56:10 +0300 Subject: [PATCH 067/209] FIX: Strip @ when searching for users and groups. (#6506) --- app/assets/javascripts/discourse/lib/user-search.js.es6 | 4 ++++ test/javascripts/lib/user-search-test.js.es6 | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index 0b2c13dba8..e666a72a1b 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -109,6 +109,10 @@ function organizeResults(r, options) { } export default function userSearch(options) { + if (options.term && options.term.length > 0 && options.term[0] === "@") { + options.term = options.term.substring(1); + } + var term = options.term || "", includeGroups = options.includeGroups, includeMentionableGroups = options.includeMentionableGroups, diff --git a/test/javascripts/lib/user-search-test.js.es6 b/test/javascripts/lib/user-search-test.js.es6 index 068b648627..9d8bcbaf0a 100644 --- a/test/javascripts/lib/user-search-test.js.es6 +++ b/test/javascripts/lib/user-search-test.js.es6 @@ -66,3 +66,8 @@ QUnit.test("it places groups unconditionally for exact match", async assert => { let results = await userSearch({ term: "Team" }); assert.equal(results[results.length - 1]["name"], "team"); }); + +QUnit.test("it strips @ from the beginning", async assert => { + let results = await userSearch({ term: "@Team" }); + assert.equal(results[results.length - 1]["name"], "team"); +}); From 85ef8e5a9f33fd7a008531561c930ed45414766a Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Fri, 19 Oct 2018 12:33:45 +1100 Subject: [PATCH 068/209] auto is not a valid value for min/max height (#6509) --- app/assets/stylesheets/wizard.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/wizard.scss b/app/assets/stylesheets/wizard.scss index a35b7830b1..678fa95686 100644 --- a/app/assets/stylesheets/wizard.scss +++ b/app/assets/stylesheets/wizard.scss @@ -540,7 +540,7 @@ body.wizard { margin: auto !important; } .wizard-step-form { - max-height: auto; + max-height: none; } .wizard-step-contents { min-height: auto !important; From 9bfc939692044bca7ecf9b3f7abe07efefe031ec Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 19 Oct 2018 12:51:34 +1100 Subject: [PATCH 069/209] cleanup so gravatar download failures are consistent previously we would ignore socket error, but this would mean that there could be conditions where we would keep trying to download gravatars forever (in an hourly job) --- app/models/user_avatar.rb | 11 +++-------- spec/models/user_avatar_spec.rb | 4 +++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index fd3fb8e4bb..e271d2eab9 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -13,7 +13,7 @@ class UserAvatar < ActiveRecord::Base def update_gravatar! DistributedMutex.synchronize("update_gravatar_#{user_id}") do begin - self.update_columns(last_gravatar_download_attempt: Time.now) + self.update!(last_gravatar_download_attempt: Time.now) max = Discourse.avatar_sizes.max email_hash = user_id == Discourse::SYSTEM_USER_ID ? User.email_hash("info@discourse.org") : user.email_hash @@ -47,17 +47,12 @@ class UserAvatar < ActiveRecord::Base end gravatar_upload&.destroy! - self.gravatar_upload = upload - save! + self.update!(gravatar_upload: upload) end end end - rescue OpenURI::HTTPError - save! - rescue SocketError - # skip saving, we are not connected to the net ensure - tempfile.try(:close!) + tempfile&.close! end end end diff --git a/spec/models/user_avatar_spec.rb b/spec/models/user_avatar_spec.rb index b52303d7b7..b2aac9adeb 100644 --- a/spec/models/user_avatar_spec.rb +++ b/spec/models/user_avatar_spec.rb @@ -74,7 +74,9 @@ describe UserAvatar do FileHelper.expects(:download).raises(SocketError) - expect { avatar.update_gravatar! }.to_not change { Upload.count } + expect do + expect { avatar.update_gravatar! }.to raise_error(SocketError) + end.to_not change { Upload.count } expect(avatar.last_gravatar_download_attempt).to eq(Time.now) end From 65faff5832e7549bfad6ce0f4dba2c8c7ce70364 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 19 Oct 2018 14:31:17 +0800 Subject: [PATCH 070/209] DEV: Improve specs to provide a better error message. --- .../web_hook_topic_view_serializer_spec.rb | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/spec/serializers/web_hook_topic_view_serializer_spec.rb b/spec/serializers/web_hook_topic_view_serializer_spec.rb index d7ee100555..ff1ac0cfdb 100644 --- a/spec/serializers/web_hook_topic_view_serializer_spec.rb +++ b/spec/serializers/web_hook_topic_view_serializer_spec.rb @@ -11,20 +11,47 @@ RSpec.describe WebHookTopicViewSerializer do ) end - it 'should only include the required keys' do - count = serializer.as_json.keys.count - difference = count - 30 + before do + SiteSetting.tagging_enabled = true + end - expect(difference).to eq(0), lambda { - message = "" - - if difference < 0 - message << "#{difference * -1} key(s) have been removed from this serializer." - else - message << "#{difference} key(s) have been added to this serializer." - end - - message << "\nPlease verify if those key(s) are required as part of the web hook's payload." + it 'should only include the keys that are sent out in the webhook' do + expected_keys = %i{ + id + title + fancy_title + posts_count + created_at + views + reply_count + like_count + last_posted_at + visible + closed + archived + archetype + slug + category_id + word_count + deleted_at + user_id + featured_link + pinned_globally + pinned_at + pinned_until + unpinned + pinned + highest_post_number + deleted_by + bookmarked + participant_count + created_by + last_poster + tags } + + keys = serializer.as_json.keys + + expect(serializer.as_json.keys).to contain_exactly(*expected_keys) end end From 5f86564da1bc7c921bf1ed791b3ebc00b5917525 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 19 Oct 2018 09:54:06 +0200 Subject: [PATCH 071/209] FEATURE: adds latest to user-api-key session scope --- app/models/user_api_key.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/user_api_key.rb b/app/models/user_api_key.rb index 5ed0d47b4d..cf9bc315c3 100644 --- a/app/models/user_api_key.rb +++ b/app/models/user_api_key.rb @@ -10,7 +10,8 @@ class UserApiKey < ActiveRecord::Base [:get, 'session#current'], [:get, 'users#topic_tracking_state'], [:get, 'list#unread'], - [:get, 'list#new'] + [:get, 'list#new'], + [:get, 'list#latest'] ] } From 7166d7de9a72b8174cf253ab9b7081ae3f505d01 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 19 Oct 2018 13:44:43 +0100 Subject: [PATCH 072/209] FIX: Prevent duplicate tags in tag-choosers (#6512) * FIX: Prevent duplicate tags in tag-choosers This reverts 5685b45, which fixes the duplicate tags problem. The fix introduced by 5685b45 is re-implemented on the server. --- .../select-kit/components/mini-tag-chooser.js.es6 | 6 ------ .../select-kit/components/tag-chooser.js.es6 | 6 ------ .../select-kit/components/tag-group-chooser.js.es6 | 6 ------ app/controllers/tags_controller.rb | 7 ++++++- spec/requests/tags_controller_spec.rb | 11 +++++++++++ 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index 765aae62dd..5484dbae8e 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -192,12 +192,6 @@ export default ComboBox.extend(TagsMixin, { return { id: result.text, name: result.text, count: result.count }; }); - // if forbidden we probably have an existing tag which is not in the list of - // returned tags, so we manually add it at the top - if (json.forbidden) { - results.unshift({ id: json.forbidden, name: json.forbidden, count: 0 }); - } - return results; }, diff --git a/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 index 5b0946e692..90c291b4de 100644 --- a/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 @@ -132,12 +132,6 @@ export default MultiSelectComponent.extend(TagsMixin, { return { id: result.text, name: result.text, count: result.count }; }); - // if forbidden we probably have an existing tag which is not in the list of - // returned tags, so we manually add it at the top - if (json.forbidden) { - results.unshift({ id: json.forbidden, name: json.forbidden, count: 0 }); - } - return results; } }); diff --git a/app/assets/javascripts/select-kit/components/tag-group-chooser.js.es6 b/app/assets/javascripts/select-kit/components/tag-group-chooser.js.es6 index 2bfa1b41fa..4a563c0cbd 100644 --- a/app/assets/javascripts/select-kit/components/tag-group-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/tag-group-chooser.js.es6 @@ -88,12 +88,6 @@ export default MultiSelectComponent.extend(TagsMixin, { return { id: result.text, name: result.text, count: result.count }; }); - // if forbidden we probably have an existing tag which is not in the list of - // returned tags, so we manually add it at the top - if (json.forbidden) { - results.unshift({ id: json.forbidden, name: json.forbidden, count: 0 }); - } - return results; } }); diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index a493d9c25c..9b73d52355 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -178,8 +178,13 @@ class TagsController < ::ApplicationController def search category = params[:categoryId] ? Category.find_by_id(params[:categoryId]) : nil + # Prioritize exact matches when ordering + order_query = Tag.sanitize_sql_for_order( + ["lower(name) = lower(?) DESC, topic_count DESC", params[:q]] + ) + tags_with_counts = DiscourseTagging.filter_allowed_tags( - Tag.order('topic_count DESC').limit(params[:limit]), + Tag.order(order_query).limit(params[:limit]), guardian, for_input: params[:filterForInput], term: params[:q], diff --git a/spec/requests/tags_controller_spec.rb b/spec/requests/tags_controller_spec.rb index c24712daff..4a0633301c 100644 --- a/spec/requests/tags_controller_spec.rb +++ b/spec/requests/tags_controller_spec.rb @@ -308,6 +308,17 @@ describe TagsController do expect(json["results"].map { |j| j["id"] }.sort).to eq(['stuff', 'stumped']) end + it "returns tags ordered by topic_count, and prioritises exact matches" do + Fabricate(:tag, name: 'tag1', topic_count: 10) + Fabricate(:tag, name: 'tag2', topic_count: 100) + Fabricate(:tag, name: 'tag', topic_count: 1) + + get '/tags/filter/search.json', params: { q: 'tag', limit: 2 } + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['results'].map { |j| j['id'] }).to eq(['tag', 'tag2']) + end + it "can say if given tag is not allowed" do yup, nope = Fabricate(:tag, name: 'yup'), Fabricate(:tag, name: 'nope') category = Fabricate(:category, tags: [yup]) From 637123ff6f40cedec370bfde1f460ebabe5c463d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 19 Oct 2018 15:16:45 +0200 Subject: [PATCH 073/209] Merge users based on their email in vBulletin importer --- .../import_scripts/base/lookup_container.rb | 28 ++++++------------- script/import_scripts/vbulletin.rb | 4 +-- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/script/import_scripts/base/lookup_container.rb b/script/import_scripts/base/lookup_container.rb index 8922b0e235..2e6f2c0af2 100644 --- a/script/import_scripts/base/lookup_container.rb +++ b/script/import_scripts/base/lookup_container.rb @@ -2,28 +2,16 @@ module ImportScripts class LookupContainer def initialize puts 'Loading existing groups...' - @groups = {} - GroupCustomField.where(name: 'import_id').pluck(:group_id, :value).each do |group_id, import_id| - @groups[import_id] = group_id - end + @groups = GroupCustomField.where(name: 'import_id').pluck(:value, :group_id).to_h puts 'Loading existing users...' - @users = {} - UserCustomField.where(name: 'import_id').pluck(:user_id, :value).each do |user_id, import_id| - @users[import_id] = user_id - end + @users = UserCustomField.where(name: 'import_id').pluck(:value, :user_id).to_h puts 'Loading existing categories...' - @categories = {} - CategoryCustomField.where(name: 'import_id').pluck(:category_id, :value).each do |category_id, import_id| - @categories[import_id] = category_id - end + @categories = CategoryCustomField.where(name: 'import_id').pluck(:value, :category_id).to_h puts 'Loading existing posts...' - @posts = {} - PostCustomField.where(name: 'import_id').pluck(:post_id, :value).each do |post_id, import_id| - @posts[import_id] = post_id - end + @posts = PostCustomField.where(name: 'import_id').pluck(:value, :post_id).to_h puts 'Loading existing topics...' @topics = {} @@ -73,19 +61,19 @@ module ImportScripts end def add_group(import_id, group) - @groups[import_id] = group.id + @groups[import_id.to_s] = group.id end def add_user(import_id, user) - @users[import_id] = user.id + @users[import_id.to_s] = user.id end def add_category(import_id, category) - @categories[import_id] = category.id + @categories[import_id.to_s] = category.id end def add_post(import_id, post) - @posts[import_id] = post.id + @posts[import_id.to_s] = post.id end def add_topic(post) diff --git a/script/import_scripts/vbulletin.rb b/script/import_scripts/vbulletin.rb index 4c5216a679..40c4d5e3cf 100644 --- a/script/import_scripts/vbulletin.rb +++ b/script/import_scripts/vbulletin.rb @@ -146,7 +146,7 @@ EOM last_user_id = users[-1]["userid"] before = users.size - users.reject! { |u| @lookup.user_already_imported?(u["userid"].to_i) } + users.reject! { |u| @lookup.user_already_imported?(u["userid"]) } create_users(users, total: user_count, offset: offset) do |user| email = user["email"].presence || fake_email @@ -162,6 +162,7 @@ EOM username: username, password: password, email: email, + merge: true, website: user["homepage"].strip, title: @htmlentities.decode(user["usertitle"]).strip, primary_group_id: group_id_from_imported_group_id(user["usergroupid"].to_i), @@ -176,7 +177,6 @@ EOM end @usernames = UserCustomField.joins(:user).where(name: 'import_username').pluck('user_custom_fields.value', 'users.username').to_h - end def create_groups_membership From b69652278f760c9d35fc8682f1d430c47cfa29c0 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Fri, 19 Oct 2018 16:30:27 +0300 Subject: [PATCH 074/209] FEATURE: Add Wiki Editor badge. (#6511) --- app/models/badge.rb | 1 + config/locales/server.en.yml | 5 +++++ db/fixtures/006_badges.rb | 13 +++++++++++++ lib/badge_queries.rb | 9 +++++++++ 4 files changed, 28 insertions(+) diff --git a/app/models/badge.rb b/app/models/badge.rb index 7ab289cf13..c7c6c40b30 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -15,6 +15,7 @@ class Badge < ActiveRecord::Base GreatPost = 8 Autobiographer = 9 Editor = 10 + WikiEditor = 48 FirstLike = 11 FirstShare = 12 diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 1b841ad84c..c1ba5847b7 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3655,6 +3655,11 @@ en: description: First post edit long_description: | This badge is granted the first time you edit one of your posts. While you won’t be able to edit your posts forever, editing is encouraged — you can improve the formatting, fix small mistakes, or add anything you missed when you originally posted. Edit to make your posts even better! + wiki_editor: + name: Wiki Editor + description: First wiki edit + long_description: | + This badge is granted the first time you edit one wiki post. basic_user: name: Basic description: Granted all essential community functions diff --git a/db/fixtures/006_badges.rb b/db/fixtures/006_badges.rb index eae24fec16..6008cd93e8 100644 --- a/db/fixtures/006_badges.rb +++ b/db/fixtures/006_badges.rb @@ -233,6 +233,19 @@ Badge.seed do |b| b.system = true end +Badge.seed do |b| + b.id = Badge::WikiEditor + b.name = "Wiki Editor" + b.badge_type_id = BadgeType::Bronze + b.multiple_grant = false + b.target_posts = true + b.query = BadgeQueries::WikiEditor + b.badge_grouping_id = BadgeGrouping::GettingStarted + b.default_badge_grouping_id = BadgeGrouping::GettingStarted + b.trigger = Badge::Trigger::PostRevision + b.system = true +end + [ [Badge::NicePost, "Nice Post", BadgeType::Bronze, false], [Badge::GoodPost, "Good Post", BadgeType::Silver, false], diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index f28e98f873..dec8a45a76 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -99,6 +99,15 @@ SQL GROUP BY p.user_id SQL + WikiEditor = <<~SQL + SELECT DISTINCT ON (pr.user_id) pr.user_id, pr.post_id, pr.created_at granted_at + FROM post_revisions pr + JOIN badge_posts p on p.id = pr.post_id + WHERE p.wiki + AND NOT pr.hidden + AND (:backfill OR p.id IN (:post_ids)) +SQL + Welcome = < Date: Fri, 19 Oct 2018 15:48:48 +0200 Subject: [PATCH 075/209] Remove unnecessary line --- script/import_scripts/vbulletin.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/script/import_scripts/vbulletin.rb b/script/import_scripts/vbulletin.rb index 40c4d5e3cf..ce1af98c96 100644 --- a/script/import_scripts/vbulletin.rb +++ b/script/import_scripts/vbulletin.rb @@ -145,7 +145,6 @@ EOM break if users.empty? last_user_id = users[-1]["userid"] - before = users.size users.reject! { |u| @lookup.user_already_imported?(u["userid"]) } create_users(users, total: user_count, offset: offset) do |user| From 3d5085c0452b61cfbc98729d1f8ce9a3fbcbf2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 19 Oct 2018 16:03:22 +0200 Subject: [PATCH 076/209] Prevent warning when bundling for imports --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index ad652e5d47..1dfbdc1b11 100644 --- a/Gemfile +++ b/Gemfile @@ -187,7 +187,7 @@ if ENV["IMPORT"] == "1" gem 'mysql2' gem 'redcarpet' gem 'sqlite3', '~> 1.3.13' - gem 'ruby-bbcode-to-md', github: 'nlalonde/ruby-bbcode-to-md' + gem 'ruby-bbcode-to-md', git: 'https://github.com/nlalonde/ruby-bbcode-to-md' gem 'reverse_markdown' gem 'tiny_tds' end From fb8231077a07fe9b3bcdce150c6615b1897166c7 Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Fri, 19 Oct 2018 10:39:22 -0400 Subject: [PATCH 077/209] FEATURE: [Experimental] Content Security Policy (#6504) --- app/controllers/csp_reports_controller.rb | 35 +++++++++ config/application.rb | 3 + config/initializers/100-mime_types.rb | 1 + config/locales/server.en.yml | 6 +- config/routes.rb | 2 + config/site_settings.yml | 11 ++- lib/content_security_policy.rb | 83 ++++++++++++++++++++ spec/lib/content_security_policy_spec.rb | 61 ++++++++++++++ spec/requests/application_controller_spec.rb | 65 +++++++++++++++ spec/requests/csp_reports_controller_spec.rb | 56 +++++++++++++ spec/support/fake_logger.rb | 7 +- 11 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 app/controllers/csp_reports_controller.rb create mode 100644 config/initializers/100-mime_types.rb create mode 100644 lib/content_security_policy.rb create mode 100644 spec/lib/content_security_policy_spec.rb create mode 100644 spec/requests/csp_reports_controller_spec.rb diff --git a/app/controllers/csp_reports_controller.rb b/app/controllers/csp_reports_controller.rb new file mode 100644 index 0000000000..41dfc9502d --- /dev/null +++ b/app/controllers/csp_reports_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +class CspReportsController < ApplicationController + skip_before_action :check_xhr, :preload_json, :verify_authenticity_token, only: [:create] + + def create + raise Discourse::NotFound unless report_collection_enabled? + + Logster.add_to_env(request.env, 'CSP Report', report) + Rails.logger.warn("CSP Violation: '#{report['blocked-uri']}'") + + head :ok + end + + private + + def report + @report ||= params.require('csp-report').permit( + 'blocked-uri', + 'disposition', + 'document-uri', + 'effective-directive', + 'original-policy', + 'referrer', + 'script-sample', + 'status-code', + 'violated-directive', + 'line-number', + 'source-file' + ).to_h + end + + def report_collection_enabled? + ContentSecurityPolicy.enabled? && SiteSetting.content_security_policy_collect_reports + end +end diff --git a/config/application.rb b/config/application.rb index 640f05f5b7..a320d6dc36 100644 --- a/config/application.rb +++ b/config/application.rb @@ -190,6 +190,9 @@ module Discourse # supports etags (post 1.7) config.middleware.delete Rack::ETag + require 'content_security_policy' + config.middleware.swap ActionDispatch::ContentSecurityPolicy::Middleware, ContentSecurityPolicy::Middleware + require 'middleware/discourse_public_exceptions' config.exceptions_app = Middleware::DiscoursePublicExceptions.new(Rails.public_path) diff --git a/config/initializers/100-mime_types.rb b/config/initializers/100-mime_types.rb new file mode 100644 index 0000000000..676ee367e8 --- /dev/null +++ b/config/initializers/100-mime_types.rb @@ -0,0 +1 @@ +Mime::Type.register 'application/csp-report', :json diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index c1ba5847b7..e8df322d68 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1267,6 +1267,10 @@ en: blacklisted_crawler_user_agents: 'Unique case insensitive word in the user agent string identifying web crawlers that should not be allowed to access the site. Does not apply if whitelist is defined.' slow_down_crawler_user_agents: 'User agents of web crawlers that should be rate limited in robots.txt using the Crawl-delay directive' slow_down_crawler_rate: 'If slow_down_crawler_user_agents is specified this rate will apply to all the crawlers (number of seconds delay between requests)' + content_security_policy: EXPERIMENTAL - Turn on Content-Security-Policy + content_security_policy_report_only: EXPERIMENTAL - Turn on Content-Security-Policy-Report-Only + content_security_policy_collect_reports: Enable CSP violation report collection at /csp_reports + content_security_policy_script_src: Additional whitelisted script sources. The current host and CDN are included by default. top_menu: "Determine which items appear in the homepage navigation, and in what order. Example latest|new|unread|categories|top|read|posted|bookmarks" post_menu: "Determine which items appear on the post menu, and in what order. Example like|edit|flag|delete|share|bookmark|reply" post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on." @@ -1868,7 +1872,7 @@ en: sso_provider_secrets: key: "www.example.com" value: "SSO secret" - + search: within_post: "#%{post_number} by %{username}" types: diff --git a/config/routes.rb b/config/routes.rb index 3d072a4691..d3cebd11ee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -828,6 +828,8 @@ Discourse::Application.routes.draw do post "/push_notifications/subscribe" => "push_notification#subscribe" post "/push_notifications/unsubscribe" => "push_notification#unsubscribe" + resources :csp_reports, only: [:create] + get "*url", to: 'permalinks#show', constraints: PermalinkConstraint.new end diff --git a/config/site_settings.yml b/config/site_settings.yml index 4af22bda68..c6b5c718d8 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1179,6 +1179,15 @@ security: default: 'bingbot' list_type: compact slow_down_crawler_rate: 60 + content_security_policy: + default: false + content_security_policy_report_only: + default: false + content_security_policy_collect_reports: + default: true + content_security_policy_script_src: + type: list + default: '' onebox: enable_flash_video_onebox: false @@ -1801,4 +1810,4 @@ tags: remove_muted_tags_from_latest: default: false force_lowercase_tags: - default: true \ No newline at end of file + default: true diff --git a/lib/content_security_policy.rb b/lib/content_security_policy.rb new file mode 100644 index 0000000000..6feb85e0cd --- /dev/null +++ b/lib/content_security_policy.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true +require_dependency 'global_path' + +class ContentSecurityPolicy + include GlobalPath + + class Middleware + WHITELISTED_PATHS = %w( + /logs + ) + + def initialize(app) + @app = app + end + + def call(env) + request = Rack::Request.new(env) + _, headers, _ = response = @app.call(env) + + return response unless html_response?(headers) && ContentSecurityPolicy.enabled? + return response if whitelisted?(request.path) + + policy = ContentSecurityPolicy.new.build + headers['Content-Security-Policy'] = policy if SiteSetting.content_security_policy + headers['Content-Security-Policy-Report-Only'] = policy if SiteSetting.content_security_policy_report_only + + response + end + + private + + def html_response?(headers) + headers['Content-Type'] && headers['Content-Type'] =~ /html/ + end + + def whitelisted?(path) + if GlobalSetting.relative_url_root + path.slice!(/^#{Regexp.quote(GlobalSetting.relative_url_root)}/) + end + + WHITELISTED_PATHS.any? { |whitelisted| path.start_with?(whitelisted) } + end + end + + def self.enabled? + SiteSetting.content_security_policy || SiteSetting.content_security_policy_report_only + end + + def initialize + @directives = { + script_src: script_src, + } + + @directives[:report_uri] = path('/csp_reports') if SiteSetting.content_security_policy_collect_reports + end + + def build + policy = ActionDispatch::ContentSecurityPolicy.new + + @directives.each do |directive, sources| + if sources.is_a?(Array) + policy.public_send(directive, *sources) + else + policy.public_send(directive, sources) + end + end + + policy.build + end + + private + + def script_src + sources = [:self, :unsafe_eval] + + sources << :https if SiteSetting.force_https + sources << Discourse.asset_host if Discourse.asset_host.present? + sources << 'www.google-analytics.com' if SiteSetting.ga_universal_tracking_code.present? + sources << 'www.googletagmanager.com' if SiteSetting.gtm_container_id.present? + + sources.concat(SiteSetting.content_security_policy_script_src.split('|')) + end +end diff --git a/spec/lib/content_security_policy_spec.rb b/spec/lib/content_security_policy_spec.rb new file mode 100644 index 0000000000..dd26fb2d94 --- /dev/null +++ b/spec/lib/content_security_policy_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +describe ContentSecurityPolicy do + describe 'report-uri' do + it 'is enabled by SiteSetting' do + SiteSetting.content_security_policy_collect_reports = true + report_uri = parse(ContentSecurityPolicy.new.build)['report-uri'].first + expect(report_uri).to eq('/csp_reports') + + SiteSetting.content_security_policy_collect_reports = false + report_uri = parse(ContentSecurityPolicy.new.build)['report-uri'] + expect(report_uri).to eq(nil) + end + end + + describe 'script-src defaults' do + it 'always have self and unsafe-eval' do + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to eq(%w['self' 'unsafe-eval']) + end + + it 'enforces https when SiteSetting.force_https' do + SiteSetting.force_https = true + + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to include('https:') + end + + it 'whitelists Google Analytics and Tag Manager when integrated' do + SiteSetting.ga_universal_tracking_code = 'UA-12345678-9' + SiteSetting.gtm_container_id = 'GTM-ABCDEF' + + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to include('www.google-analytics.com') + expect(script_srcs).to include('www.googletagmanager.com') + end + + it 'whitelists CDN when integrated' do + set_cdn_url('cdn.com') + + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to include('cdn.com') + end + + it 'can be extended with more sources' do + SiteSetting.content_security_policy_script_src = 'example.com|another.com' + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to include('example.com') + expect(script_srcs).to include('another.com') + expect(script_srcs).to include("'unsafe-eval'") + expect(script_srcs).to include("'self'") + end + end + + def parse(csp_string) + csp_string.split(';').map do |policy| + directive, *sources = policy.split + [directive, sources] + end.to_h + end +end diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 03c5488f11..13338f0666 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -196,4 +196,69 @@ RSpec.describe ApplicationController do expect(controller.theme_ids).to eq([theme.id]) end end + + describe 'Content Security Policy' do + it 'is enabled by SiteSettings' do + SiteSetting.content_security_policy = false + SiteSetting.content_security_policy_report_only = false + + get '/' + + expect(response.headers).to_not include('Content-Security-Policy') + expect(response.headers).to_not include('Content-Security-Policy-Report-Only') + + SiteSetting.content_security_policy = true + SiteSetting.content_security_policy_report_only = true + + get '/' + + expect(response.headers).to include('Content-Security-Policy') + expect(response.headers).to include('Content-Security-Policy-Report-Only') + end + + it 'can be customized with SiteSetting' do + SiteSetting.content_security_policy = true + + get '/' + script_src = parse(response.headers['Content-Security-Policy'])['script-src'] + + expect(script_src).to_not include('example.com') + + SiteSetting.content_security_policy_script_src = 'example.com' + + get '/' + script_src = parse(response.headers['Content-Security-Policy'])['script-src'] + + expect(script_src).to include('example.com') + expect(script_src).to include("'self'") + expect(script_src).to include("'unsafe-eval'") + end + + it 'does not set CSP when responding to non-HTML' do + SiteSetting.content_security_policy = true + SiteSetting.content_security_policy_report_only = true + + get '/latest.json' + + expect(response.headers).to_not include('Content-Security-Policy') + expect(response.headers).to_not include('Content-Security-Policy-Report-Only') + end + + it 'does not set CSP for /logs' do + sign_in(Fabricate(:admin)) + SiteSetting.content_security_policy = true + + get '/logs' + + expect(response.status).to eq(200) + expect(response.headers).to_not include('Content-Security-Policy') + end + + def parse(csp_string) + csp_string.split(';').map do |policy| + directive, *sources = policy.split + [directive, sources] + end.to_h + end + end end diff --git a/spec/requests/csp_reports_controller_spec.rb b/spec/requests/csp_reports_controller_spec.rb new file mode 100644 index 0000000000..1fd6b23876 --- /dev/null +++ b/spec/requests/csp_reports_controller_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +describe CspReportsController do + describe '#create' do + before do + SiteSetting.content_security_policy = true + SiteSetting.content_security_policy_collect_reports = true + + @orig_logger = Rails.logger + Rails.logger = @fake_logger = FakeLogger.new + end + + after do + Rails.logger = @orig_logger + end + + def send_report + post '/csp_reports', params: { + "csp-report": { + "document-uri": "http://localhost:3000/", + "referrer": "", + "violated-directive": "script-src", + "effective-directive": "script-src", + "original-policy": "script-src 'unsafe-eval' www.google-analytics.com; report-uri /csp_reports", + "disposition": "report", + "blocked-uri": "http://suspicio.us/assets.js", + "line-number": 25, + "source-file": "http://localhost:3000/", + "status-code": 200, + "script-sample": "" + }, headers: { "Content-Type": "application/csp-report" } + } + end + + it 'is enabled by SiteSetting' do + SiteSetting.content_security_policy = false + SiteSetting.content_security_policy_report_only = false + SiteSetting.content_security_policy_collect_reports = true + send_report + expect(response.status).to eq(404) + + SiteSetting.content_security_policy = true + send_report + expect(response.status).to eq(200) + + SiteSetting.content_security_policy_collect_reports = false + send_report + expect(response.status).to eq(404) + end + + it 'logs the violation report' do + send_report + expect(Rails.logger.warnings).to include("CSP Violation: 'http://suspicio.us/assets.js'") + end + end +end diff --git a/spec/support/fake_logger.rb b/spec/support/fake_logger.rb index 416de400dc..5a4c9f2acc 100644 --- a/spec/support/fake_logger.rb +++ b/spec/support/fake_logger.rb @@ -1,9 +1,14 @@ class FakeLogger - attr_reader :warnings, :errors + attr_reader :warnings, :errors, :infos def initialize @warnings = [] @errors = [] + @infos = [] + end + + def info(message = nil) + @infos << message end def warn(message) From 18ae8de9e5f8d04f32bf2f0781bfaf6ab833e2c9 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 19 Oct 2018 15:43:31 +0100 Subject: [PATCH 078/209] FIX: Sanitize tags before creation --- .../components/mini-tag-chooser.js.es6 | 22 ------------------- .../javascripts/select-kit/mixins/tags.js.es6 | 15 +++++++++++++ config/site_settings.yml | 1 + .../components/mini-tag-chooser-test.js.es6 | 7 +++--- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index 5484dbae8e..07825a3bca 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -214,28 +214,6 @@ export default ComboBox.extend(TagsMixin, { this.destroyTags(tags); }, - _sanitizeContent(content, property) { - switch (typeof content) { - case "string": - // See lib/discourse_tagging#clean_tag. - return content - .trim() - .replace(/\s+/, "-") - .replace(/[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/, "") - .substring(0, this.siteSettings.max_tag_length); - default: - return get(content, this.get(property)); - } - }, - - valueForContentItem(content) { - return this._sanitizeContent(content, "valueAttribute"); - }, - - _nameForContent(content) { - return this._sanitizeContent(content, "nameProperty"); - }, - actions: { onSelect(tag) { this.set("tags", makeArray(this.get("tags")).concat(tag)); diff --git a/app/assets/javascripts/select-kit/mixins/tags.js.es6 b/app/assets/javascripts/select-kit/mixins/tags.js.es6 index 0f0bef09ad..f6bd407c38 100644 --- a/app/assets/javascripts/select-kit/mixins/tags.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/tags.js.es6 @@ -68,5 +68,20 @@ export default Ember.Mixin.create({ } return true; + }, + + createContentFromInput(input) { + // See lib/discourse_tagging#clean_tag. + var content = input + .trim() + .replace(/\s+/, "-") + .replace(/[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/, "") + .substring(0, this.siteSettings.max_tag_length); + + if (this.siteSettings.force_lowercase_tags) { + content = content.toLowerCase(); + } + + return content; } }); diff --git a/config/site_settings.yml b/config/site_settings.yml index c6b5c718d8..1f22e1f179 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1811,3 +1811,4 @@ tags: default: false force_lowercase_tags: default: true + client: true diff --git a/test/javascripts/components/mini-tag-chooser-test.js.es6 b/test/javascripts/components/mini-tag-chooser-test.js.es6 index d08379be6c..194f9591ae 100644 --- a/test/javascripts/components/mini-tag-chooser-test.js.es6 +++ b/test/javascripts/components/mini-tag-chooser-test.js.es6 @@ -12,6 +12,7 @@ componentTest("default", { beforeEach() { this.siteSettings.max_tag_length = 24; + this.siteSettings.force_lowercase_tags = true; this.site.set("can_create_tag", true); this.set("tags", ["jeff", "neil", "arpit"]); @@ -85,11 +86,11 @@ componentTest("default", { ); await this.get("subject").expand(); - await this.get("subject").fillInFilter("invalid'tag"); + await this.get("subject").fillInFilter("invalid' Tag"); await this.get("subject").keyboard("enter"); assert.deepEqual( this.get("tags"), - ["jeff", "neil", "arpit", "régis", "joffrey", "invalidtag"], + ["jeff", "neil", "arpit", "régis", "joffrey", "invalid-tag"], "it strips invalid characters in tag" ); @@ -98,7 +99,7 @@ componentTest("default", { await this.get("subject").keyboard("enter"); assert.deepEqual( this.get("tags"), - ["jeff", "neil", "arpit", "régis", "joffrey", "invalidtag"], + ["jeff", "neil", "arpit", "régis", "joffrey", "invalid-tag"], "it does not allow creating long tags" ); From 0dd717e641e48d97f80ca9bba29a2d22b0b6f92f Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 19 Oct 2018 15:49:05 +0100 Subject: [PATCH 079/209] Revert "FIX: Sanitize tags before creation" This reverts commit 18ae8de9e5f8d04f32bf2f0781bfaf6ab833e2c9. --- .../components/mini-tag-chooser.js.es6 | 22 +++++++++++++++++++ .../javascripts/select-kit/mixins/tags.js.es6 | 15 ------------- config/site_settings.yml | 1 - .../components/mini-tag-chooser-test.js.es6 | 7 +++--- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index 07825a3bca..5484dbae8e 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -214,6 +214,28 @@ export default ComboBox.extend(TagsMixin, { this.destroyTags(tags); }, + _sanitizeContent(content, property) { + switch (typeof content) { + case "string": + // See lib/discourse_tagging#clean_tag. + return content + .trim() + .replace(/\s+/, "-") + .replace(/[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/, "") + .substring(0, this.siteSettings.max_tag_length); + default: + return get(content, this.get(property)); + } + }, + + valueForContentItem(content) { + return this._sanitizeContent(content, "valueAttribute"); + }, + + _nameForContent(content) { + return this._sanitizeContent(content, "nameProperty"); + }, + actions: { onSelect(tag) { this.set("tags", makeArray(this.get("tags")).concat(tag)); diff --git a/app/assets/javascripts/select-kit/mixins/tags.js.es6 b/app/assets/javascripts/select-kit/mixins/tags.js.es6 index f6bd407c38..0f0bef09ad 100644 --- a/app/assets/javascripts/select-kit/mixins/tags.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/tags.js.es6 @@ -68,20 +68,5 @@ export default Ember.Mixin.create({ } return true; - }, - - createContentFromInput(input) { - // See lib/discourse_tagging#clean_tag. - var content = input - .trim() - .replace(/\s+/, "-") - .replace(/[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/, "") - .substring(0, this.siteSettings.max_tag_length); - - if (this.siteSettings.force_lowercase_tags) { - content = content.toLowerCase(); - } - - return content; } }); diff --git a/config/site_settings.yml b/config/site_settings.yml index 1f22e1f179..c6b5c718d8 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1811,4 +1811,3 @@ tags: default: false force_lowercase_tags: default: true - client: true diff --git a/test/javascripts/components/mini-tag-chooser-test.js.es6 b/test/javascripts/components/mini-tag-chooser-test.js.es6 index 194f9591ae..d08379be6c 100644 --- a/test/javascripts/components/mini-tag-chooser-test.js.es6 +++ b/test/javascripts/components/mini-tag-chooser-test.js.es6 @@ -12,7 +12,6 @@ componentTest("default", { beforeEach() { this.siteSettings.max_tag_length = 24; - this.siteSettings.force_lowercase_tags = true; this.site.set("can_create_tag", true); this.set("tags", ["jeff", "neil", "arpit"]); @@ -86,11 +85,11 @@ componentTest("default", { ); await this.get("subject").expand(); - await this.get("subject").fillInFilter("invalid' Tag"); + await this.get("subject").fillInFilter("invalid'tag"); await this.get("subject").keyboard("enter"); assert.deepEqual( this.get("tags"), - ["jeff", "neil", "arpit", "régis", "joffrey", "invalid-tag"], + ["jeff", "neil", "arpit", "régis", "joffrey", "invalidtag"], "it strips invalid characters in tag" ); @@ -99,7 +98,7 @@ componentTest("default", { await this.get("subject").keyboard("enter"); assert.deepEqual( this.get("tags"), - ["jeff", "neil", "arpit", "régis", "joffrey", "invalid-tag"], + ["jeff", "neil", "arpit", "régis", "joffrey", "invalidtag"], "it does not allow creating long tags" ); From b35c8fb336049f52d9fe341ce8cb4eeee7c1c5b0 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 19 Oct 2018 11:30:11 -0400 Subject: [PATCH 080/209] Add offset to topic footer admin menu, to avoid header overlap --- .../javascripts/discourse/widgets/topic-admin-menu.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 index 7feb7d150f..4322771a23 100644 --- a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 @@ -112,7 +112,7 @@ export default createWidget("topic-admin-menu", { if (attrs.openUpwards) { const documentHeight = $(document).height(); const mainHeight = $("#main").height(); - let bottom = documentHeight - top - $("#main").offset().top; + let bottom = documentHeight - top - 70 - $("#main").offset().top; if (documentHeight > mainHeight) { bottom = bottom - (documentHeight - mainHeight) - outerHeight; From dca830cb73371faca8e8497af13064a5bc92b52d Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Fri, 19 Oct 2018 11:53:29 -0400 Subject: [PATCH 081/209] Revert "FEATURE: [Experimental] Content Security Policy (#6504)" This reverts commit fb8231077a07fe9b3bcdce150c6615b1897166c7. --- app/controllers/csp_reports_controller.rb | 35 --------- config/application.rb | 3 - config/initializers/100-mime_types.rb | 1 - config/locales/server.en.yml | 6 +- config/routes.rb | 2 - config/site_settings.yml | 11 +-- lib/content_security_policy.rb | 83 -------------------- spec/lib/content_security_policy_spec.rb | 61 -------------- spec/requests/application_controller_spec.rb | 65 --------------- spec/requests/csp_reports_controller_spec.rb | 56 ------------- spec/support/fake_logger.rb | 7 +- 11 files changed, 3 insertions(+), 327 deletions(-) delete mode 100644 app/controllers/csp_reports_controller.rb delete mode 100644 config/initializers/100-mime_types.rb delete mode 100644 lib/content_security_policy.rb delete mode 100644 spec/lib/content_security_policy_spec.rb delete mode 100644 spec/requests/csp_reports_controller_spec.rb diff --git a/app/controllers/csp_reports_controller.rb b/app/controllers/csp_reports_controller.rb deleted file mode 100644 index 41dfc9502d..0000000000 --- a/app/controllers/csp_reports_controller.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true -class CspReportsController < ApplicationController - skip_before_action :check_xhr, :preload_json, :verify_authenticity_token, only: [:create] - - def create - raise Discourse::NotFound unless report_collection_enabled? - - Logster.add_to_env(request.env, 'CSP Report', report) - Rails.logger.warn("CSP Violation: '#{report['blocked-uri']}'") - - head :ok - end - - private - - def report - @report ||= params.require('csp-report').permit( - 'blocked-uri', - 'disposition', - 'document-uri', - 'effective-directive', - 'original-policy', - 'referrer', - 'script-sample', - 'status-code', - 'violated-directive', - 'line-number', - 'source-file' - ).to_h - end - - def report_collection_enabled? - ContentSecurityPolicy.enabled? && SiteSetting.content_security_policy_collect_reports - end -end diff --git a/config/application.rb b/config/application.rb index a320d6dc36..640f05f5b7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -190,9 +190,6 @@ module Discourse # supports etags (post 1.7) config.middleware.delete Rack::ETag - require 'content_security_policy' - config.middleware.swap ActionDispatch::ContentSecurityPolicy::Middleware, ContentSecurityPolicy::Middleware - require 'middleware/discourse_public_exceptions' config.exceptions_app = Middleware::DiscoursePublicExceptions.new(Rails.public_path) diff --git a/config/initializers/100-mime_types.rb b/config/initializers/100-mime_types.rb deleted file mode 100644 index 676ee367e8..0000000000 --- a/config/initializers/100-mime_types.rb +++ /dev/null @@ -1 +0,0 @@ -Mime::Type.register 'application/csp-report', :json diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e8df322d68..c1ba5847b7 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1267,10 +1267,6 @@ en: blacklisted_crawler_user_agents: 'Unique case insensitive word in the user agent string identifying web crawlers that should not be allowed to access the site. Does not apply if whitelist is defined.' slow_down_crawler_user_agents: 'User agents of web crawlers that should be rate limited in robots.txt using the Crawl-delay directive' slow_down_crawler_rate: 'If slow_down_crawler_user_agents is specified this rate will apply to all the crawlers (number of seconds delay between requests)' - content_security_policy: EXPERIMENTAL - Turn on Content-Security-Policy - content_security_policy_report_only: EXPERIMENTAL - Turn on Content-Security-Policy-Report-Only - content_security_policy_collect_reports: Enable CSP violation report collection at /csp_reports - content_security_policy_script_src: Additional whitelisted script sources. The current host and CDN are included by default. top_menu: "Determine which items appear in the homepage navigation, and in what order. Example latest|new|unread|categories|top|read|posted|bookmarks" post_menu: "Determine which items appear on the post menu, and in what order. Example like|edit|flag|delete|share|bookmark|reply" post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on." @@ -1872,7 +1868,7 @@ en: sso_provider_secrets: key: "www.example.com" value: "SSO secret" - + search: within_post: "#%{post_number} by %{username}" types: diff --git a/config/routes.rb b/config/routes.rb index d3cebd11ee..3d072a4691 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -828,8 +828,6 @@ Discourse::Application.routes.draw do post "/push_notifications/subscribe" => "push_notification#subscribe" post "/push_notifications/unsubscribe" => "push_notification#unsubscribe" - resources :csp_reports, only: [:create] - get "*url", to: 'permalinks#show', constraints: PermalinkConstraint.new end diff --git a/config/site_settings.yml b/config/site_settings.yml index c6b5c718d8..4af22bda68 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1179,15 +1179,6 @@ security: default: 'bingbot' list_type: compact slow_down_crawler_rate: 60 - content_security_policy: - default: false - content_security_policy_report_only: - default: false - content_security_policy_collect_reports: - default: true - content_security_policy_script_src: - type: list - default: '' onebox: enable_flash_video_onebox: false @@ -1810,4 +1801,4 @@ tags: remove_muted_tags_from_latest: default: false force_lowercase_tags: - default: true + default: true \ No newline at end of file diff --git a/lib/content_security_policy.rb b/lib/content_security_policy.rb deleted file mode 100644 index 6feb85e0cd..0000000000 --- a/lib/content_security_policy.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true -require_dependency 'global_path' - -class ContentSecurityPolicy - include GlobalPath - - class Middleware - WHITELISTED_PATHS = %w( - /logs - ) - - def initialize(app) - @app = app - end - - def call(env) - request = Rack::Request.new(env) - _, headers, _ = response = @app.call(env) - - return response unless html_response?(headers) && ContentSecurityPolicy.enabled? - return response if whitelisted?(request.path) - - policy = ContentSecurityPolicy.new.build - headers['Content-Security-Policy'] = policy if SiteSetting.content_security_policy - headers['Content-Security-Policy-Report-Only'] = policy if SiteSetting.content_security_policy_report_only - - response - end - - private - - def html_response?(headers) - headers['Content-Type'] && headers['Content-Type'] =~ /html/ - end - - def whitelisted?(path) - if GlobalSetting.relative_url_root - path.slice!(/^#{Regexp.quote(GlobalSetting.relative_url_root)}/) - end - - WHITELISTED_PATHS.any? { |whitelisted| path.start_with?(whitelisted) } - end - end - - def self.enabled? - SiteSetting.content_security_policy || SiteSetting.content_security_policy_report_only - end - - def initialize - @directives = { - script_src: script_src, - } - - @directives[:report_uri] = path('/csp_reports') if SiteSetting.content_security_policy_collect_reports - end - - def build - policy = ActionDispatch::ContentSecurityPolicy.new - - @directives.each do |directive, sources| - if sources.is_a?(Array) - policy.public_send(directive, *sources) - else - policy.public_send(directive, sources) - end - end - - policy.build - end - - private - - def script_src - sources = [:self, :unsafe_eval] - - sources << :https if SiteSetting.force_https - sources << Discourse.asset_host if Discourse.asset_host.present? - sources << 'www.google-analytics.com' if SiteSetting.ga_universal_tracking_code.present? - sources << 'www.googletagmanager.com' if SiteSetting.gtm_container_id.present? - - sources.concat(SiteSetting.content_security_policy_script_src.split('|')) - end -end diff --git a/spec/lib/content_security_policy_spec.rb b/spec/lib/content_security_policy_spec.rb deleted file mode 100644 index dd26fb2d94..0000000000 --- a/spec/lib/content_security_policy_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'rails_helper' - -describe ContentSecurityPolicy do - describe 'report-uri' do - it 'is enabled by SiteSetting' do - SiteSetting.content_security_policy_collect_reports = true - report_uri = parse(ContentSecurityPolicy.new.build)['report-uri'].first - expect(report_uri).to eq('/csp_reports') - - SiteSetting.content_security_policy_collect_reports = false - report_uri = parse(ContentSecurityPolicy.new.build)['report-uri'] - expect(report_uri).to eq(nil) - end - end - - describe 'script-src defaults' do - it 'always have self and unsafe-eval' do - script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] - expect(script_srcs).to eq(%w['self' 'unsafe-eval']) - end - - it 'enforces https when SiteSetting.force_https' do - SiteSetting.force_https = true - - script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] - expect(script_srcs).to include('https:') - end - - it 'whitelists Google Analytics and Tag Manager when integrated' do - SiteSetting.ga_universal_tracking_code = 'UA-12345678-9' - SiteSetting.gtm_container_id = 'GTM-ABCDEF' - - script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] - expect(script_srcs).to include('www.google-analytics.com') - expect(script_srcs).to include('www.googletagmanager.com') - end - - it 'whitelists CDN when integrated' do - set_cdn_url('cdn.com') - - script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] - expect(script_srcs).to include('cdn.com') - end - - it 'can be extended with more sources' do - SiteSetting.content_security_policy_script_src = 'example.com|another.com' - script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] - expect(script_srcs).to include('example.com') - expect(script_srcs).to include('another.com') - expect(script_srcs).to include("'unsafe-eval'") - expect(script_srcs).to include("'self'") - end - end - - def parse(csp_string) - csp_string.split(';').map do |policy| - directive, *sources = policy.split - [directive, sources] - end.to_h - end -end diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 13338f0666..03c5488f11 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -196,69 +196,4 @@ RSpec.describe ApplicationController do expect(controller.theme_ids).to eq([theme.id]) end end - - describe 'Content Security Policy' do - it 'is enabled by SiteSettings' do - SiteSetting.content_security_policy = false - SiteSetting.content_security_policy_report_only = false - - get '/' - - expect(response.headers).to_not include('Content-Security-Policy') - expect(response.headers).to_not include('Content-Security-Policy-Report-Only') - - SiteSetting.content_security_policy = true - SiteSetting.content_security_policy_report_only = true - - get '/' - - expect(response.headers).to include('Content-Security-Policy') - expect(response.headers).to include('Content-Security-Policy-Report-Only') - end - - it 'can be customized with SiteSetting' do - SiteSetting.content_security_policy = true - - get '/' - script_src = parse(response.headers['Content-Security-Policy'])['script-src'] - - expect(script_src).to_not include('example.com') - - SiteSetting.content_security_policy_script_src = 'example.com' - - get '/' - script_src = parse(response.headers['Content-Security-Policy'])['script-src'] - - expect(script_src).to include('example.com') - expect(script_src).to include("'self'") - expect(script_src).to include("'unsafe-eval'") - end - - it 'does not set CSP when responding to non-HTML' do - SiteSetting.content_security_policy = true - SiteSetting.content_security_policy_report_only = true - - get '/latest.json' - - expect(response.headers).to_not include('Content-Security-Policy') - expect(response.headers).to_not include('Content-Security-Policy-Report-Only') - end - - it 'does not set CSP for /logs' do - sign_in(Fabricate(:admin)) - SiteSetting.content_security_policy = true - - get '/logs' - - expect(response.status).to eq(200) - expect(response.headers).to_not include('Content-Security-Policy') - end - - def parse(csp_string) - csp_string.split(';').map do |policy| - directive, *sources = policy.split - [directive, sources] - end.to_h - end - end end diff --git a/spec/requests/csp_reports_controller_spec.rb b/spec/requests/csp_reports_controller_spec.rb deleted file mode 100644 index 1fd6b23876..0000000000 --- a/spec/requests/csp_reports_controller_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'rails_helper' - -describe CspReportsController do - describe '#create' do - before do - SiteSetting.content_security_policy = true - SiteSetting.content_security_policy_collect_reports = true - - @orig_logger = Rails.logger - Rails.logger = @fake_logger = FakeLogger.new - end - - after do - Rails.logger = @orig_logger - end - - def send_report - post '/csp_reports', params: { - "csp-report": { - "document-uri": "http://localhost:3000/", - "referrer": "", - "violated-directive": "script-src", - "effective-directive": "script-src", - "original-policy": "script-src 'unsafe-eval' www.google-analytics.com; report-uri /csp_reports", - "disposition": "report", - "blocked-uri": "http://suspicio.us/assets.js", - "line-number": 25, - "source-file": "http://localhost:3000/", - "status-code": 200, - "script-sample": "" - }, headers: { "Content-Type": "application/csp-report" } - } - end - - it 'is enabled by SiteSetting' do - SiteSetting.content_security_policy = false - SiteSetting.content_security_policy_report_only = false - SiteSetting.content_security_policy_collect_reports = true - send_report - expect(response.status).to eq(404) - - SiteSetting.content_security_policy = true - send_report - expect(response.status).to eq(200) - - SiteSetting.content_security_policy_collect_reports = false - send_report - expect(response.status).to eq(404) - end - - it 'logs the violation report' do - send_report - expect(Rails.logger.warnings).to include("CSP Violation: 'http://suspicio.us/assets.js'") - end - end -end diff --git a/spec/support/fake_logger.rb b/spec/support/fake_logger.rb index 5a4c9f2acc..416de400dc 100644 --- a/spec/support/fake_logger.rb +++ b/spec/support/fake_logger.rb @@ -1,14 +1,9 @@ class FakeLogger - attr_reader :warnings, :errors, :infos + attr_reader :warnings, :errors def initialize @warnings = [] @errors = [] - @infos = [] - end - - def info(message = nil) - @infos << message end def warn(message) From ce0a51665ecde210689eb5823e185519d89c4155 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 19 Oct 2018 20:28:35 +0530 Subject: [PATCH 082/209] FIX: count emoji shortcuts in topic title https://meta.discourse.org/t/max-emojis-in-title-set-to-0-conflicting-with-emoji-shortcuts/98368/3?u=techapj --- config/locales/server.en.yml | 3 ++- lib/validators/max_emojis_validator.rb | 5 +++-- spec/components/validators/max_emojis_validator_spec.rb | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index c1ba5847b7..651b276d7f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -139,6 +139,7 @@ en: odd: must be odd record_invalid: ! 'Validation failed: %{errors}' max_emojis: "can't have more than %{max_emojis_count} emoji" + emojis_disabled: "can't have emoji" ip_address_already_screened: "is already included in an existing rule" restrict_dependent_destroy: one: "Cannot delete record because a dependent %{record} exists" @@ -1868,7 +1869,7 @@ en: sso_provider_secrets: key: "www.example.com" value: "SSO secret" - + search: within_post: "#%{post_number} by %{username}" types: diff --git a/lib/validators/max_emojis_validator.rb b/lib/validators/max_emojis_validator.rb index 3476cd9c01..5f8e6e6c7a 100644 --- a/lib/validators/max_emojis_validator.rb +++ b/lib/validators/max_emojis_validator.rb @@ -1,9 +1,10 @@ class MaxEmojisValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - if Emoji.unicode_unescape(value).scan(/:([\w\-+]+(?::t\d)?):/).size > SiteSetting.max_emojis_in_title + unescaped_title = PrettyText.unescape_emoji(Emoji.unicode_unescape(CGI::escapeHTML(value))) + if unescaped_title.scan(/ SiteSetting.max_emojis_in_title record.errors.add( - attribute, :max_emojis, + attribute, SiteSetting.max_emojis_in_title > 0 ? :max_emojis : :emojis_disabled, max_emojis_count: SiteSetting.max_emojis_in_title ) end diff --git a/spec/components/validators/max_emojis_validator_spec.rb b/spec/components/validators/max_emojis_validator_spec.rb index 351746e44b..671caaea99 100644 --- a/spec/components/validators/max_emojis_validator_spec.rb +++ b/spec/components/validators/max_emojis_validator_spec.rb @@ -14,7 +14,7 @@ describe MaxEmojisValidator do shared_examples "validating any topic title" do it 'adds an error when emoji count is greater than SiteSetting.max_emojis_in_title' do SiteSetting.max_emojis_in_title = 3 - record.title = '🧐 Lots of emojis here 🎃 :joy: :sunglasses:' + record.title = '🧐 Lots of emojis here 🎃 :joy: :)' validate expect(record.errors[:title][0]).to eq(I18n.t("errors.messages.max_emojis", max_emojis_count: 3)) From afa22a0c6f121683e4493de13ebcecf45ebd6968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 22 Oct 2018 11:12:40 +0200 Subject: [PATCH 083/209] REFACTOR: more 'fake_email' to base importer --- script/import_scripts/base.rb | 13 +++++++------ script/import_scripts/lithium.rb | 4 ---- script/import_scripts/nodebb/nodebb.rb | 4 ---- script/import_scripts/question2answer.rb | 4 ---- script/import_scripts/vbulletin.rb | 4 ---- script/import_scripts/vbulletin5.rb | 4 ---- 6 files changed, 7 insertions(+), 26 deletions(-) diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index 2546b661ea..ead0601395 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -253,7 +253,7 @@ class ImportScripts::Base if user_id_from_imported_user_id(import_id) skipped += 1 - elsif u[:email].present? + else new_user = create_user(u, import_id) created_user(new_user) @@ -270,9 +270,6 @@ class ImportScripts::Base end end end - else - failed += 1 - puts "Skipping user id #{import_id} because email is blank" end end @@ -314,8 +311,8 @@ class ImportScripts::Base opts[:username] = UserNameSuggester.suggest(opts[:username] || opts[:name].presence || opts[:email]) end - unless opts[:email].match(EmailValidator.email_regex) - opts[:email] = "invalid#{SecureRandom.hex}@no-email.invalid" + unless opts[:email][EmailValidator.email_regex] + opts[:email] = fake_email puts "Invalid email #{original_email} for #{opts[:username]}. Using: #{opts[:email]}" end @@ -881,4 +878,8 @@ class ImportScripts::Base offset += batch_size end end + + def fake_email + SecureRandom.hex << "@domain.com" + end end diff --git a/script/import_scripts/lithium.rb b/script/import_scripts/lithium.rb index 442df14f8a..834f97cec2 100644 --- a/script/import_scripts/lithium.rb +++ b/script/import_scripts/lithium.rb @@ -1032,10 +1032,6 @@ SQL html end - def fake_email - SecureRandom.hex << "@domain.com" - end - def mysql_query(sql) @client.query(sql, cache_rows: true) end diff --git a/script/import_scripts/nodebb/nodebb.rb b/script/import_scripts/nodebb/nodebb.rb index 1c9acfef22..3e5c616a5d 100644 --- a/script/import_scripts/nodebb/nodebb.rb +++ b/script/import_scripts/nodebb/nodebb.rb @@ -518,10 +518,6 @@ class ImportScripts::NodeBB < ImportScripts::Base raw end - - def fake_email - SecureRandom.hex << "@domain.com" - end end ImportScripts::NodeBB.new.perform diff --git a/script/import_scripts/question2answer.rb b/script/import_scripts/question2answer.rb index 49eb3eddc5..f9d248791b 100644 --- a/script/import_scripts/question2answer.rb +++ b/script/import_scripts/question2answer.rb @@ -551,10 +551,6 @@ EOM Time.zone.at(@tz.utc_to_local(timestamp)) end - def fake_email - SecureRandom.hex << "@domain.com" - end - def mysql_query(sql) @client.query(sql, cache_rows: true) end diff --git a/script/import_scripts/vbulletin.rb b/script/import_scripts/vbulletin.rb index ce1af98c96..092d8d536b 100644 --- a/script/import_scripts/vbulletin.rb +++ b/script/import_scripts/vbulletin.rb @@ -931,10 +931,6 @@ EOM Time.zone.at(@tz.utc_to_local(timestamp)) end - def fake_email - SecureRandom.hex << "@domain.com" - end - def mysql_query(sql) @client.query(sql, cache_rows: true) end diff --git a/script/import_scripts/vbulletin5.rb b/script/import_scripts/vbulletin5.rb index 36a46745b5..1bab839561 100644 --- a/script/import_scripts/vbulletin5.rb +++ b/script/import_scripts/vbulletin5.rb @@ -621,10 +621,6 @@ class ImportScripts::VBulletin < ImportScripts::Base Time.zone.at(@tz.utc_to_local(timestamp)) end - def fake_email - SecureRandom.hex << "@domain.com" - end - def mysql_query(sql) @client.query(sql, cache_rows: false) end From c39a1022cc9de539ec6ac4f46a6bf39356e9bcda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 22 Oct 2018 11:14:13 +0200 Subject: [PATCH 084/209] PERF: user imports would slow down the more users were imported --- script/import_scripts/base.rb | 3 ++- script/import_scripts/base/lookup_container.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index ead0601395..367236092f 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -390,7 +390,8 @@ class ImportScripts::Base end def find_existing_user(email, username) - User.joins(:user_emails).where("user_emails.email = ? OR username = ?", email.downcase, username).first + # Force the use of the index on the 'user_emails' table + UserEmail.where("lower(email) = ?", email.downcase).first&.user || User.where(username: username).first end def created_category(category) diff --git a/script/import_scripts/base/lookup_container.rb b/script/import_scripts/base/lookup_container.rb index 2e6f2c0af2..fa324db051 100644 --- a/script/import_scripts/base/lookup_container.rb +++ b/script/import_scripts/base/lookup_container.rb @@ -37,7 +37,7 @@ module ImportScripts # Get the Discourse Group id based on the id of the source group def group_id_from_imported_group_id(import_id) - @groups[import_id] || @groups[import_id.to_s] || find_group_by_import_id(import_id).try(:id) + @groups[import_id] || @groups[import_id.to_s] end # Get the Discourse Group based on the id of the source group @@ -47,7 +47,7 @@ module ImportScripts # Get the Discourse User id based on the id of the source user def user_id_from_imported_user_id(import_id) - @users[import_id] || @users[import_id.to_s] || find_user_by_import_id(import_id).try(:id) + @users[import_id] || @users[import_id.to_s] end # Get the Discourse User based on the id of the source user From 597d4863d67665530219f66010ff40222f9634f2 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 22 Oct 2018 15:08:26 +0530 Subject: [PATCH 085/209] fix the build --- lib/validators/max_emojis_validator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/validators/max_emojis_validator.rb b/lib/validators/max_emojis_validator.rb index 5f8e6e6c7a..8db71afa57 100644 --- a/lib/validators/max_emojis_validator.rb +++ b/lib/validators/max_emojis_validator.rb @@ -2,7 +2,7 @@ class MaxEmojisValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) unescaped_title = PrettyText.unescape_emoji(Emoji.unicode_unescape(CGI::escapeHTML(value))) - if unescaped_title.scan(/ SiteSetting.max_emojis_in_title + if unescaped_title.present? && unescaped_title.scan(/ SiteSetting.max_emojis_in_title record.errors.add( attribute, SiteSetting.max_emojis_in_title > 0 ? :max_emojis : :emojis_disabled, max_emojis_count: SiteSetting.max_emojis_in_title From 37b7afa522b77dc3a3fb78507f2fe8690819e11e Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 19 Oct 2018 15:43:31 +0100 Subject: [PATCH 086/209] FIX: Sanitize tags before creation --- .../components/mini-tag-chooser.js.es6 | 22 ------------------- .../javascripts/select-kit/mixins/tags.js.es6 | 15 +++++++++++++ config/site_settings.yml | 3 ++- .../components/mini-tag-chooser-test.js.es6 | 7 +++--- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index 5484dbae8e..07825a3bca 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -214,28 +214,6 @@ export default ComboBox.extend(TagsMixin, { this.destroyTags(tags); }, - _sanitizeContent(content, property) { - switch (typeof content) { - case "string": - // See lib/discourse_tagging#clean_tag. - return content - .trim() - .replace(/\s+/, "-") - .replace(/[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/, "") - .substring(0, this.siteSettings.max_tag_length); - default: - return get(content, this.get(property)); - } - }, - - valueForContentItem(content) { - return this._sanitizeContent(content, "valueAttribute"); - }, - - _nameForContent(content) { - return this._sanitizeContent(content, "nameProperty"); - }, - actions: { onSelect(tag) { this.set("tags", makeArray(this.get("tags")).concat(tag)); diff --git a/app/assets/javascripts/select-kit/mixins/tags.js.es6 b/app/assets/javascripts/select-kit/mixins/tags.js.es6 index 0f0bef09ad..26438da9f8 100644 --- a/app/assets/javascripts/select-kit/mixins/tags.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/tags.js.es6 @@ -68,5 +68,20 @@ export default Ember.Mixin.create({ } return true; + }, + + createContentFromInput(input) { + // See lib/discourse_tagging#clean_tag. + var content = input + .trim() + .replace(/\s+/g, "-") + .replace(/[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/g, "") + .substring(0, this.siteSettings.max_tag_length); + + if (this.siteSettings.force_lowercase_tags) { + content = content.toLowerCase(); + } + + return content; } }); diff --git a/config/site_settings.yml b/config/site_settings.yml index 4af22bda68..28c616ac89 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1801,4 +1801,5 @@ tags: remove_muted_tags_from_latest: default: false force_lowercase_tags: - default: true \ No newline at end of file + default: true + client: true diff --git a/test/javascripts/components/mini-tag-chooser-test.js.es6 b/test/javascripts/components/mini-tag-chooser-test.js.es6 index d08379be6c..194f9591ae 100644 --- a/test/javascripts/components/mini-tag-chooser-test.js.es6 +++ b/test/javascripts/components/mini-tag-chooser-test.js.es6 @@ -12,6 +12,7 @@ componentTest("default", { beforeEach() { this.siteSettings.max_tag_length = 24; + this.siteSettings.force_lowercase_tags = true; this.site.set("can_create_tag", true); this.set("tags", ["jeff", "neil", "arpit"]); @@ -85,11 +86,11 @@ componentTest("default", { ); await this.get("subject").expand(); - await this.get("subject").fillInFilter("invalid'tag"); + await this.get("subject").fillInFilter("invalid' Tag"); await this.get("subject").keyboard("enter"); assert.deepEqual( this.get("tags"), - ["jeff", "neil", "arpit", "régis", "joffrey", "invalidtag"], + ["jeff", "neil", "arpit", "régis", "joffrey", "invalid-tag"], "it strips invalid characters in tag" ); @@ -98,7 +99,7 @@ componentTest("default", { await this.get("subject").keyboard("enter"); assert.deepEqual( this.get("tags"), - ["jeff", "neil", "arpit", "régis", "joffrey", "invalidtag"], + ["jeff", "neil", "arpit", "régis", "joffrey", "invalid-tag"], "it does not allow creating long tags" ); From 3377f26ebabf4143aaffd4c9bfa70eadc249f7a7 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 22 Oct 2018 11:09:06 +0100 Subject: [PATCH 087/209] FIX: Clean tag before searching for matches --- app/controllers/tags_controller.rb | 7 ++++--- spec/requests/tags_controller_spec.rb | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 9b73d52355..14549413d4 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -176,18 +176,19 @@ class TagsController < ::ApplicationController end def search + clean_name = DiscourseTagging.clean_tag(params[:q]) category = params[:categoryId] ? Category.find_by_id(params[:categoryId]) : nil # Prioritize exact matches when ordering order_query = Tag.sanitize_sql_for_order( - ["lower(name) = lower(?) DESC, topic_count DESC", params[:q]] + ["lower(name) = lower(?) DESC, topic_count DESC", clean_name] ) tags_with_counts = DiscourseTagging.filter_allowed_tags( Tag.order(order_query).limit(params[:limit]), guardian, for_input: params[:filterForInput], - term: params[:q], + term: clean_name, category: category, selected_tags: params[:selected_tags] ) @@ -196,7 +197,7 @@ class TagsController < ::ApplicationController json_response = { results: tags } - if Tag.where_name(params[:q]).exists? && !tags.find { |h| h[:id] == params[:q] } + if Tag.where_name(clean_name).exists? && !tags.find { |h| h[:id].downcase == clean_name.downcase } # filter_allowed_tags determined that the tag entered is not allowed json_response[:forbidden] = params[:q] end diff --git a/spec/requests/tags_controller_spec.rb b/spec/requests/tags_controller_spec.rb index 4a0633301c..9166a4442a 100644 --- a/spec/requests/tags_controller_spec.rb +++ b/spec/requests/tags_controller_spec.rb @@ -329,6 +329,14 @@ describe TagsController do expect(json["forbidden"]).to be_present end + it "matches tags after sanitizing input" do + yup, nope = Fabricate(:tag, name: 'yup'), Fabricate(:tag, name: 'nope') + get "/tags/filter/search.json", params: { q: 'N/ope' } + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["results"].map { |j| j["id"] }.sort).to eq(["nope"]) + end + it "can return tags that are in secured categories but are allowed to be used" do c = Fabricate(:private_category, group: Fabricate(:group)) Fabricate(:topic, category: c, tags: [Fabricate(:tag, name: "cooltag")]) From 99b43f281bd912b14ff1c3cf33dffe470dca2f07 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Mon, 22 Oct 2018 15:15:41 +0300 Subject: [PATCH 088/209] FIX: Fix browser detection for Microsoft Edge. (#6516) cool! --- config/locales/server.en.yml | 1 + lib/browser_detection.rb | 2 ++ spec/lib/browser_detection_spec.rb | 1 + 3 files changed, 4 insertions(+) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 651b276d7f..f38a823a6c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -709,6 +709,7 @@ en: firefox: "Firefox" opera: "Opera" ie: "Internet Explorer" + edge: "Microsoft Edge" unknown: "unknown browser" device: android: "Android Device" diff --git a/lib/browser_detection.rb b/lib/browser_detection.rb index cf3ea57e6a..78d5bd1161 100644 --- a/lib/browser_detection.rb +++ b/lib/browser_detection.rb @@ -2,6 +2,8 @@ module BrowserDetection def self.browser(user_agent) case user_agent + when /Edge/i + :edge when /Opera/i, /OPR/i :opera when /Firefox/i diff --git a/spec/lib/browser_detection_spec.rb b/spec/lib/browser_detection_spec.rb index 6a4722ca74..f67a7c0707 100644 --- a/spec/lib/browser_detection_spec.rb +++ b/spec/lib/browser_detection_spec.rb @@ -27,6 +27,7 @@ describe BrowserDetection do ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", :chrome, :linux, :linux], ["Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", :firefox, :linux, :linux], ["Opera/9.80 (X11; Linux zvav; U; en) Presto/2.12.423 Version/12.16", :opera, :linux, :linux], + ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", :edge, :windows, :windows], ].each do |user_agent, browser, device, os| expect(BrowserDetection.browser(user_agent)).to eq(browser) expect(BrowserDetection.device(user_agent)).to eq(device) From db26fe1527f1514d12c72db88cce5952b10302f9 Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Mon, 22 Oct 2018 13:34:01 -0300 Subject: [PATCH 089/209] FIX: Proper naming for the GNU/Linux OS --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index f38a823a6c..b3197e3b3e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -718,7 +718,7 @@ en: ipod: "iPod" mobile: "Mobile Device" mac: "Mac" - linux: "Linux Computer" + linux: "GNU/Linux Computer" windows: "Windows Computer" unknown: "unknown device" os: From 3e232412e3a70669abda43c7ca003e25df752a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 22 Oct 2018 19:00:30 +0200 Subject: [PATCH 090/209] UX: show error when hitting the rate limit on password reset --- .../controllers/password-reset.js.es6 | 11 ++++--- .../discourse/templates/password-reset.hbs | 10 ++++++- app/controllers/users_controller.rb | 28 ++++++++++-------- config/locales/client.en.yml | 1 + spec/requests/users_controller_spec.rb | 29 +++++++++++++++++++ 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 index b012e29ed0..2caf6e6b70 100644 --- a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 +++ b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 @@ -41,8 +41,7 @@ export default Ember.Controller.extend(PasswordValidation, { second_factor_token: this.get("secondFactor"), second_factor_method: this.get("secondFactorMethod") } - }) - .then(result => { + }).then(result => { if (result.success) { this.set("successMessage", result.message); this.set("redirectTo", result.redirect_to); @@ -83,8 +82,12 @@ export default Ember.Controller.extend(PasswordValidation, { } } }) - .catch(error => { - throw new Error(error); + .catch(e => { + if (e.jqXHR && e.jqXHR.status === 429) { + this.set("errorMessage", I18n.t("user.second_factor.rate_limit")); + } else { + throw new Error(e); + } }); }, diff --git a/app/assets/javascripts/discourse/templates/password-reset.hbs b/app/assets/javascripts/discourse/templates/password-reset.hbs index 660c370a94..5a762a985a 100644 --- a/app/assets/javascripts/discourse/templates/password-reset.hbs +++ b/app/assets/javascripts/discourse/templates/password-reset.hbs @@ -18,7 +18,15 @@
{{#if secondFactorRequired}} {{#second-factor-form secondFactorMethod=secondFactorMethod backupEnabled=backupEnabled}} - {{text-field value=secondFactor id="second-factor" autocorrect="off" autocapitalize="off" autofocus="autofocus" secondFactorMethod=secondFactorMethod}} + {{text-field + id="second-factor" + value=secondFactor + autocorrect="off" + autocapitalize="off" + autofocus="autofocus" + maxlength="6" + secondFactorMethod=secondFactorMethod + }} {{/second-factor-form}} {{d-button action="submit" class='btn-primary' label='submit'}} {{else}} diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f3c837bf30..2153f47b78 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -453,12 +453,11 @@ class UsersController < ApplicationController token = params[:token] if EmailToken.valid_token_format?(token) - @user = - if request.put? - EmailToken.confirm(token) - else - EmailToken.confirmable(token)&.user - end + @user = if request.put? + EmailToken.confirm(token) + else + EmailToken.confirmable(token)&.user + end if @user secure_session["password-#{token}"] = @user.id @@ -468,9 +467,15 @@ class UsersController < ApplicationController end end - totp_enabled = @user&.totp_enabled? + second_factor_token = params[:second_factor_token] + second_factor_method = params[:second_factor_method].to_i - if !totp_enabled || @user.authenticate_second_factor(params[:second_factor_token], params[:second_factor_method].to_i) + if second_factor_token.present? && second_factor_token[/\d{6}/] && UserSecondFactor.methods[second_factor_method] + RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! + second_factor_authenticated = @user&.authenticate_second_factor(second_factor_token, second_factor_method) + end + + if second_factor_authenticated || !@user&.totp_enabled? secure_session["second-factor-#{token}"] = "true" end @@ -479,13 +484,10 @@ class UsersController < ApplicationController if !@user @error = I18n.t('password_reset.no_token') elsif request.put? - @invalid_password = params[:password].blank? || params[:password].length > User.max_password_length - if !valid_second_factor - RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 3, 1.minute).performed! @user.errors.add(:user_second_factors, :invalid) @error = I18n.t('login.invalid_second_factor_code') - elsif @invalid_password + elsif @invalid_password = params[:password].blank? || params[:password].size > User.max_password_length @user.errors.add(:password, :invalid) else @user.password = params[:password] @@ -547,6 +549,8 @@ class UsersController < ApplicationController end end end + rescue RateLimiter::LimitExceeded => e + render_rate_limit_error(e) end def confirm_email_token diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 918e12d8b0..7bf0b3e45b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -771,6 +771,7 @@ en: enable: "Enable two factor authentication for enhanced account security" confirm_password_description: "Please confirm your password to continue" label: "Code" + rate_limit: "Please wait before trying another authentication code." enable_description: | Scan this QR code in a supported app (AndroidiOS and enter your authentication code. disable_description: "Please enter the authentication code from your app" diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 67c96d6bde..106cf80e93 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -250,6 +250,35 @@ describe UsersController do expect(UserAuthToken.where(id: user_token.id).count).to eq(1) end + context "rate limiting" do + + before { RateLimiter.clear_all!; RateLimiter.enable } + after { RateLimiter.disable } + + it "rate limits reset passwords" do + freeze_time + + token = user.email_tokens.create!(email: user.email).token + + 3.times do + put "/u/password-reset/#{token}", params: { + second_factor_token: 123456, + second_factor_method: 1 + } + + expect(response.status).to eq(200) + end + + put "/u/password-reset/#{token}", params: { + second_factor_token: 123456, + second_factor_method: 1 + } + + expect(response.status).to eq(429) + end + + end + context '2 factor authentication required' do let!(:second_factor) { Fabricate(:user_second_factor_totp, user: user) } From b9261588f9f6b787d815d3dc8237883f8c1b196a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 22 Oct 2018 19:07:41 +0200 Subject: [PATCH 091/209] make the code prettier --- .../javascripts/discourse/controllers/password-reset.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 index 2caf6e6b70..7e2ed248b5 100644 --- a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 +++ b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 @@ -41,7 +41,8 @@ export default Ember.Controller.extend(PasswordValidation, { second_factor_token: this.get("secondFactor"), second_factor_method: this.get("secondFactorMethod") } - }).then(result => { + }) + .then(result => { if (result.success) { this.set("successMessage", result.message); this.set("redirectTo", result.redirect_to); From ec2613699f21817457c2a8fd3f4e830a51ea9dc3 Mon Sep 17 00:00:00 2001 From: Jeff Wong Date: Wed, 17 Oct 2018 15:36:25 -0700 Subject: [PATCH 092/209] Change box category view to use flexbox --- .../templates/components/categories-boxes.hbs | 51 ++++++++++--------- .../common/base/category-list.scss | 19 ++++--- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs b/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs index 16aeaf6195..623191149b 100644 --- a/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs +++ b/app/assets/javascripts/discourse/templates/components/categories-boxes.hbs @@ -1,35 +1,38 @@ {{#each categories as |c|}}
-
+ - -
- {{{text-overflow class="overflow" text=c.description_excerpt}}} -
- - {{#if c.subcategories}} -
- {{#each c.subcategories as |sc|}} - - - {{cdn-img src=sc.uploaded_logo.url class="logo"}} - - {{sc.name}} - - {{/each}} +
+
+

+ {{#if c.read_restricted}} + {{d-icon 'lock'}} + {{/if}} + {{c.name}} +

- {{/if}} + +
+ {{{text-overflow class="overflow" text=c.description_excerpt}}} +
+ + {{#if c.subcategories}} +
+ {{#each c.subcategories as |sc|}} + + + {{cdn-img src=sc.uploaded_logo.url class="logo"}} + + {{sc.name}} + + {{/each}} +
+ {{/if}} +
{{/each}} diff --git a/app/assets/stylesheets/common/base/category-list.scss b/app/assets/stylesheets/common/base/category-list.scss index b1d0210f88..8a6a395452 100644 --- a/app/assets/stylesheets/common/base/category-list.scss +++ b/app/assets/stylesheets/common/base/category-list.scss @@ -55,6 +55,8 @@ .category-box-inner { width: 100%; padding: 0; + display: flex; + flex-direction: column; border-width: 2px; border-left-width: 0; @@ -78,6 +80,14 @@ width: 100%; padding: 0; } + .category-box-inner { + padding: 1em; + + .category-logo { + float: none; + margin: 0; + } + } } &.no-logos { @@ -86,12 +96,8 @@ } } - .category-box-heading { - padding: 1em 1em 0 1em; - } - .description { - padding: 0 1em 1em 1em; + padding-bottom: 1em; text-align: center; font-size: $font-0; color: dark-light-choose($primary-medium, $secondary-high); @@ -114,7 +120,6 @@ } .subcategories { - margin-left: 1em; display: flex; flex-flow: wrap; .subcategory { @@ -127,8 +132,6 @@ margin-bottom: 0.6em; .subcategory-image-placeholder { display: inline-block; - width: 20px; - height: 20px; margin-right: 0.6em; } .logo { From e9a971a2b6d2b29edf698cea2ee8211040a14453 Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Mon, 22 Oct 2018 13:22:23 -0400 Subject: [PATCH 093/209] FEATURE: [Experimental] Content Security Policy (#6514) do not register new MIME type, parse raw body instead --- app/controllers/csp_reports_controller.rb | 35 +++++++++ config/application.rb | 3 + config/locales/server.en.yml | 4 + config/routes.rb | 2 + config/site_settings.yml | 9 +++ lib/content_security_policy.rb | 83 ++++++++++++++++++++ spec/lib/content_security_policy_spec.rb | 61 ++++++++++++++ spec/requests/application_controller_spec.rb | 65 +++++++++++++++ spec/requests/csp_reports_controller_spec.rb | 56 +++++++++++++ spec/support/fake_logger.rb | 7 +- 10 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 app/controllers/csp_reports_controller.rb create mode 100644 lib/content_security_policy.rb create mode 100644 spec/lib/content_security_policy_spec.rb create mode 100644 spec/requests/csp_reports_controller_spec.rb diff --git a/app/controllers/csp_reports_controller.rb b/app/controllers/csp_reports_controller.rb new file mode 100644 index 0000000000..9b81840607 --- /dev/null +++ b/app/controllers/csp_reports_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +class CspReportsController < ApplicationController + skip_before_action :check_xhr, :preload_json, :verify_authenticity_token, only: [:create] + + def create + raise Discourse::NotFound unless report_collection_enabled? + + Logster.add_to_env(request.env, 'CSP Report', report) + Rails.logger.warn("CSP Violation: '#{report['blocked-uri']}'") + + head :ok + end + + private + + def report + @report ||= JSON.parse(request.body.read)['csp-report'].slice( + 'blocked-uri', + 'disposition', + 'document-uri', + 'effective-directive', + 'original-policy', + 'referrer', + 'script-sample', + 'status-code', + 'violated-directive', + 'line-number', + 'source-file' + ) + end + + def report_collection_enabled? + ContentSecurityPolicy.enabled? && SiteSetting.content_security_policy_collect_reports + end +end diff --git a/config/application.rb b/config/application.rb index 640f05f5b7..a320d6dc36 100644 --- a/config/application.rb +++ b/config/application.rb @@ -190,6 +190,9 @@ module Discourse # supports etags (post 1.7) config.middleware.delete Rack::ETag + require 'content_security_policy' + config.middleware.swap ActionDispatch::ContentSecurityPolicy::Middleware, ContentSecurityPolicy::Middleware + require 'middleware/discourse_public_exceptions' config.exceptions_app = Middleware::DiscoursePublicExceptions.new(Rails.public_path) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b3197e3b3e..aa16da5c65 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1269,6 +1269,10 @@ en: blacklisted_crawler_user_agents: 'Unique case insensitive word in the user agent string identifying web crawlers that should not be allowed to access the site. Does not apply if whitelist is defined.' slow_down_crawler_user_agents: 'User agents of web crawlers that should be rate limited in robots.txt using the Crawl-delay directive' slow_down_crawler_rate: 'If slow_down_crawler_user_agents is specified this rate will apply to all the crawlers (number of seconds delay between requests)' + content_security_policy: EXPERIMENTAL - Turn on Content-Security-Policy + content_security_policy_report_only: EXPERIMENTAL - Turn on Content-Security-Policy-Report-Only + content_security_policy_collect_reports: Enable CSP violation report collection at /csp_reports + content_security_policy_script_src: Additional whitelisted script sources. The current host and CDN are included by default. top_menu: "Determine which items appear in the homepage navigation, and in what order. Example latest|new|unread|categories|top|read|posted|bookmarks" post_menu: "Determine which items appear on the post menu, and in what order. Example like|edit|flag|delete|share|bookmark|reply" post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on." diff --git a/config/routes.rb b/config/routes.rb index 3d072a4691..d3cebd11ee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -828,6 +828,8 @@ Discourse::Application.routes.draw do post "/push_notifications/subscribe" => "push_notification#subscribe" post "/push_notifications/unsubscribe" => "push_notification#unsubscribe" + resources :csp_reports, only: [:create] + get "*url", to: 'permalinks#show', constraints: PermalinkConstraint.new end diff --git a/config/site_settings.yml b/config/site_settings.yml index 28c616ac89..1f22e1f179 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1179,6 +1179,15 @@ security: default: 'bingbot' list_type: compact slow_down_crawler_rate: 60 + content_security_policy: + default: false + content_security_policy_report_only: + default: false + content_security_policy_collect_reports: + default: true + content_security_policy_script_src: + type: list + default: '' onebox: enable_flash_video_onebox: false diff --git a/lib/content_security_policy.rb b/lib/content_security_policy.rb new file mode 100644 index 0000000000..6feb85e0cd --- /dev/null +++ b/lib/content_security_policy.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true +require_dependency 'global_path' + +class ContentSecurityPolicy + include GlobalPath + + class Middleware + WHITELISTED_PATHS = %w( + /logs + ) + + def initialize(app) + @app = app + end + + def call(env) + request = Rack::Request.new(env) + _, headers, _ = response = @app.call(env) + + return response unless html_response?(headers) && ContentSecurityPolicy.enabled? + return response if whitelisted?(request.path) + + policy = ContentSecurityPolicy.new.build + headers['Content-Security-Policy'] = policy if SiteSetting.content_security_policy + headers['Content-Security-Policy-Report-Only'] = policy if SiteSetting.content_security_policy_report_only + + response + end + + private + + def html_response?(headers) + headers['Content-Type'] && headers['Content-Type'] =~ /html/ + end + + def whitelisted?(path) + if GlobalSetting.relative_url_root + path.slice!(/^#{Regexp.quote(GlobalSetting.relative_url_root)}/) + end + + WHITELISTED_PATHS.any? { |whitelisted| path.start_with?(whitelisted) } + end + end + + def self.enabled? + SiteSetting.content_security_policy || SiteSetting.content_security_policy_report_only + end + + def initialize + @directives = { + script_src: script_src, + } + + @directives[:report_uri] = path('/csp_reports') if SiteSetting.content_security_policy_collect_reports + end + + def build + policy = ActionDispatch::ContentSecurityPolicy.new + + @directives.each do |directive, sources| + if sources.is_a?(Array) + policy.public_send(directive, *sources) + else + policy.public_send(directive, sources) + end + end + + policy.build + end + + private + + def script_src + sources = [:self, :unsafe_eval] + + sources << :https if SiteSetting.force_https + sources << Discourse.asset_host if Discourse.asset_host.present? + sources << 'www.google-analytics.com' if SiteSetting.ga_universal_tracking_code.present? + sources << 'www.googletagmanager.com' if SiteSetting.gtm_container_id.present? + + sources.concat(SiteSetting.content_security_policy_script_src.split('|')) + end +end diff --git a/spec/lib/content_security_policy_spec.rb b/spec/lib/content_security_policy_spec.rb new file mode 100644 index 0000000000..dd26fb2d94 --- /dev/null +++ b/spec/lib/content_security_policy_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +describe ContentSecurityPolicy do + describe 'report-uri' do + it 'is enabled by SiteSetting' do + SiteSetting.content_security_policy_collect_reports = true + report_uri = parse(ContentSecurityPolicy.new.build)['report-uri'].first + expect(report_uri).to eq('/csp_reports') + + SiteSetting.content_security_policy_collect_reports = false + report_uri = parse(ContentSecurityPolicy.new.build)['report-uri'] + expect(report_uri).to eq(nil) + end + end + + describe 'script-src defaults' do + it 'always have self and unsafe-eval' do + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to eq(%w['self' 'unsafe-eval']) + end + + it 'enforces https when SiteSetting.force_https' do + SiteSetting.force_https = true + + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to include('https:') + end + + it 'whitelists Google Analytics and Tag Manager when integrated' do + SiteSetting.ga_universal_tracking_code = 'UA-12345678-9' + SiteSetting.gtm_container_id = 'GTM-ABCDEF' + + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to include('www.google-analytics.com') + expect(script_srcs).to include('www.googletagmanager.com') + end + + it 'whitelists CDN when integrated' do + set_cdn_url('cdn.com') + + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to include('cdn.com') + end + + it 'can be extended with more sources' do + SiteSetting.content_security_policy_script_src = 'example.com|another.com' + script_srcs = parse(ContentSecurityPolicy.new.build)['script-src'] + expect(script_srcs).to include('example.com') + expect(script_srcs).to include('another.com') + expect(script_srcs).to include("'unsafe-eval'") + expect(script_srcs).to include("'self'") + end + end + + def parse(csp_string) + csp_string.split(';').map do |policy| + directive, *sources = policy.split + [directive, sources] + end.to_h + end +end diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb index 03c5488f11..13338f0666 100644 --- a/spec/requests/application_controller_spec.rb +++ b/spec/requests/application_controller_spec.rb @@ -196,4 +196,69 @@ RSpec.describe ApplicationController do expect(controller.theme_ids).to eq([theme.id]) end end + + describe 'Content Security Policy' do + it 'is enabled by SiteSettings' do + SiteSetting.content_security_policy = false + SiteSetting.content_security_policy_report_only = false + + get '/' + + expect(response.headers).to_not include('Content-Security-Policy') + expect(response.headers).to_not include('Content-Security-Policy-Report-Only') + + SiteSetting.content_security_policy = true + SiteSetting.content_security_policy_report_only = true + + get '/' + + expect(response.headers).to include('Content-Security-Policy') + expect(response.headers).to include('Content-Security-Policy-Report-Only') + end + + it 'can be customized with SiteSetting' do + SiteSetting.content_security_policy = true + + get '/' + script_src = parse(response.headers['Content-Security-Policy'])['script-src'] + + expect(script_src).to_not include('example.com') + + SiteSetting.content_security_policy_script_src = 'example.com' + + get '/' + script_src = parse(response.headers['Content-Security-Policy'])['script-src'] + + expect(script_src).to include('example.com') + expect(script_src).to include("'self'") + expect(script_src).to include("'unsafe-eval'") + end + + it 'does not set CSP when responding to non-HTML' do + SiteSetting.content_security_policy = true + SiteSetting.content_security_policy_report_only = true + + get '/latest.json' + + expect(response.headers).to_not include('Content-Security-Policy') + expect(response.headers).to_not include('Content-Security-Policy-Report-Only') + end + + it 'does not set CSP for /logs' do + sign_in(Fabricate(:admin)) + SiteSetting.content_security_policy = true + + get '/logs' + + expect(response.status).to eq(200) + expect(response.headers).to_not include('Content-Security-Policy') + end + + def parse(csp_string) + csp_string.split(';').map do |policy| + directive, *sources = policy.split + [directive, sources] + end.to_h + end + end end diff --git a/spec/requests/csp_reports_controller_spec.rb b/spec/requests/csp_reports_controller_spec.rb new file mode 100644 index 0000000000..e4568776dd --- /dev/null +++ b/spec/requests/csp_reports_controller_spec.rb @@ -0,0 +1,56 @@ +require 'rails_helper' + +describe CspReportsController do + describe '#create' do + before do + SiteSetting.content_security_policy = true + SiteSetting.content_security_policy_collect_reports = true + + @orig_logger = Rails.logger + Rails.logger = @fake_logger = FakeLogger.new + end + + after do + Rails.logger = @orig_logger + end + + def send_report + post '/csp_reports', params: { + "csp-report": { + "document-uri": "http://localhost:3000/", + "referrer": "", + "violated-directive": "script-src", + "effective-directive": "script-src", + "original-policy": "script-src 'unsafe-eval' www.google-analytics.com; report-uri /csp_reports", + "disposition": "report", + "blocked-uri": "http://suspicio.us/assets.js", + "line-number": 25, + "source-file": "http://localhost:3000/", + "status-code": 200, + "script-sample": "" + } + }.to_json, headers: { "Content-Type": "application/csp-report" } + end + + it 'is enabled by SiteSetting' do + SiteSetting.content_security_policy = false + SiteSetting.content_security_policy_report_only = false + SiteSetting.content_security_policy_collect_reports = true + send_report + expect(response.status).to eq(404) + + SiteSetting.content_security_policy = true + send_report + expect(response.status).to eq(200) + + SiteSetting.content_security_policy_collect_reports = false + send_report + expect(response.status).to eq(404) + end + + it 'logs the violation report' do + send_report + expect(Rails.logger.warnings).to include("CSP Violation: 'http://suspicio.us/assets.js'") + end + end +end diff --git a/spec/support/fake_logger.rb b/spec/support/fake_logger.rb index 416de400dc..5a4c9f2acc 100644 --- a/spec/support/fake_logger.rb +++ b/spec/support/fake_logger.rb @@ -1,9 +1,14 @@ class FakeLogger - attr_reader :warnings, :errors + attr_reader :warnings, :errors, :infos def initialize @warnings = [] @errors = [] + @infos = [] + end + + def info(message = nil) + @infos << message end def warn(message) From 37fa7775f15009821fefa266ec4285d329f8406b Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Mon, 22 Oct 2018 20:30:23 +0300 Subject: [PATCH 094/209] FIX: Fix order of recently connected devices. (#6517) --- .../discourse/controllers/preferences/account.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index ca3fe8e7ee..ca6947f0eb 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -107,7 +107,7 @@ export default Ember.Controller.extend( @computed("showAllAuthTokens", "model.user_auth_tokens") authTokens(showAllAuthTokens, tokens) { tokens.sort( - (a, b) => (a.is_active ? -1 : b.is_active ? 1 : a.seen_at < b.seen_at) + (a, b) => (a.is_active ? -1 : b.is_active ? 1 : b.seen_at.localeCompare(a.seen_at)) ); return showAllAuthTokens From 2cc195f3d9a5e25304ba37eb0f9fead9bf0f692f Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Mon, 22 Oct 2018 14:18:26 -0400 Subject: [PATCH 095/209] prettier linting fix --- .../discourse/controllers/preferences/account.js.es6 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 index ca6947f0eb..6a7df6986c 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 @@ -107,7 +107,12 @@ export default Ember.Controller.extend( @computed("showAllAuthTokens", "model.user_auth_tokens") authTokens(showAllAuthTokens, tokens) { tokens.sort( - (a, b) => (a.is_active ? -1 : b.is_active ? 1 : b.seen_at.localeCompare(a.seen_at)) + (a, b) => + a.is_active + ? -1 + : b.is_active + ? 1 + : b.seen_at.localeCompare(a.seen_at) ); return showAllAuthTokens From 093cab2db04675dc9f9ccc52a3ceee5f85156f45 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 22 Oct 2018 21:28:38 +0100 Subject: [PATCH 096/209] DEV: Lint official plugins in CI (#6519) --- .travis.yml | 1 + lib/tasks/docker.rake | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 29b6d0fd7a..4e2e7df725 100644 --- a/.travis.yml +++ b/.travis.yml @@ -76,6 +76,7 @@ script: if [ '$RUN_LINT' == '1' ]; then bundle exec rubocop --parallel && \ bundle exec danger && \ + yarn prettier --list-different "plugins/**/*.scss" "plugins/**/*.es6" \ yarn eslint --ext .es6 app/assets/javascripts && \ yarn eslint --ext .es6 test/javascripts && \ yarn eslint --ext .es6 plugins/**/assets/javascripts && \ diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake index 65276b759c..8689aa2173 100644 --- a/lib/tasks/docker.rake +++ b/lib/tasks/docker.rake @@ -38,6 +38,11 @@ desc 'Run all tests (JS and code in a standalone environment)' task 'docker:test' do begin @good = true + + if ENV["INSTALL_OFFICIAL_PLUGINS"] + @good &&= run_or_fail("bundle exec rake plugin:install_all_official") + end + unless ENV['SKIP_LINT'] puts "travis_fold:start:lint" if ENV["TRAVIS"] puts "Running linters/prettyfiers" @@ -91,10 +96,6 @@ task 'docker:test' do @good &&= run_or_fail("bundle exec rake db:create") - if ENV["INSTALL_OFFICIAL_PLUGINS"] - @good &&= run_or_fail("bundle exec rake plugin:install_all_official") - end - if ENV["SKIP_PLUGINS"] @good &&= run_or_fail("bundle exec rake db:migrate") else From bafe3cd99a87d67fa24800a04012980cf9ada92b Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 22 Oct 2018 22:30:33 +0100 Subject: [PATCH 097/209] Revert "DEV: Lint official plugins in CI (#6519)" This reverts commit 093cab2db04675dc9f9ccc52a3ceee5f85156f45. --- .travis.yml | 1 - lib/tasks/docker.rake | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4e2e7df725..29b6d0fd7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -76,7 +76,6 @@ script: if [ '$RUN_LINT' == '1' ]; then bundle exec rubocop --parallel && \ bundle exec danger && \ - yarn prettier --list-different "plugins/**/*.scss" "plugins/**/*.es6" \ yarn eslint --ext .es6 app/assets/javascripts && \ yarn eslint --ext .es6 test/javascripts && \ yarn eslint --ext .es6 plugins/**/assets/javascripts && \ diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake index 8689aa2173..65276b759c 100644 --- a/lib/tasks/docker.rake +++ b/lib/tasks/docker.rake @@ -38,11 +38,6 @@ desc 'Run all tests (JS and code in a standalone environment)' task 'docker:test' do begin @good = true - - if ENV["INSTALL_OFFICIAL_PLUGINS"] - @good &&= run_or_fail("bundle exec rake plugin:install_all_official") - end - unless ENV['SKIP_LINT'] puts "travis_fold:start:lint" if ENV["TRAVIS"] puts "Running linters/prettyfiers" @@ -96,6 +91,10 @@ task 'docker:test' do @good &&= run_or_fail("bundle exec rake db:create") + if ENV["INSTALL_OFFICIAL_PLUGINS"] + @good &&= run_or_fail("bundle exec rake plugin:install_all_official") + end + if ENV["SKIP_PLUGINS"] @good &&= run_or_fail("bundle exec rake db:migrate") else From bea8d337b2d295b94bb0ebb5b075f7814c46dc27 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 23 Oct 2018 08:45:06 +1100 Subject: [PATCH 098/209] DEV: ensure resizing test does not raise bad error Current resizing test was showing binary diff in terminal and failing in latest image magick 7, this fixes both issues --- spec/models/optimized_image_spec.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/models/optimized_image_spec.rb b/spec/models/optimized_image_spec.rb index 9d7db74d4c..5bc7bcfd7b 100644 --- a/spec/models/optimized_image_spec.rb +++ b/spec/models/optimized_image_spec.rb @@ -37,6 +37,8 @@ describe OptimizedImage do # we use "filename" to get the correct extension here, it is more important # then any other param + orig_size = File.size(original_path) + OptimizedImage.resize( original_path, original_path, @@ -45,9 +47,8 @@ describe OptimizedImage do filename: "test.png" ) - expect(File.read(original_path)).to eq( - File.read("#{Rails.root}/spec/fixtures/images/resized.png") - ) + expect(orig_size).to be > File.size(original_path) + ensure File.delete(original_path) if File.exists?(original_path) end From adab7a3a48460ee585f99d7ab1c41b6ae1792718 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 23 Oct 2018 08:50:07 +1100 Subject: [PATCH 099/209] improve test, also ensure no zero size is generated --- spec/models/optimized_image_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/models/optimized_image_spec.rb b/spec/models/optimized_image_spec.rb index 5bc7bcfd7b..40d12b5cbf 100644 --- a/spec/models/optimized_image_spec.rb +++ b/spec/models/optimized_image_spec.rb @@ -47,7 +47,9 @@ describe OptimizedImage do filename: "test.png" ) - expect(orig_size).to be > File.size(original_path) + new_size = File.size(original_path) + expect(orig_size).to be > new_size + expect(new_size).not_to eq(0) ensure File.delete(original_path) if File.exists?(original_path) From b74dd7d379167f5b5b41f3ef7f46ee3c5e7473b5 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 23 Oct 2018 11:43:14 +1100 Subject: [PATCH 100/209] FIX: stop logging every 404 error when searching for gravatars --- app/models/user_avatar.rb | 4 ++++ spec/models/user_avatar_spec.rb | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index e271d2eab9..d414d88525 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -51,6 +51,10 @@ class UserAvatar < ActiveRecord::Base end end end + rescue OpenURI::HTTPError => e + if e.io&.status[0].to_i != 404 + raise e + end ensure tempfile&.close! end diff --git a/spec/models/user_avatar_spec.rb b/spec/models/user_avatar_spec.rb index b2aac9adeb..e5eba4498d 100644 --- a/spec/models/user_avatar_spec.rb +++ b/spec/models/user_avatar_spec.rb @@ -83,6 +83,23 @@ describe UserAvatar do end + describe "404 should be silent, nothing to do really" do + + it "does nothing when avatar is 404" do + + freeze_time Time.now + + stub_request(:get, "https://www.gravatar.com/avatar/#{avatar.user.email_hash}.png?d=404&s=360"). + to_return(status: 404, body: "", headers: {}) + + expect do + avatar.update_gravatar! + end.to_not change { Upload.count } + + expect(avatar.last_gravatar_download_attempt).to eq(Time.now) + end + end + end context '.import_url_for_user' do From 7d2e582b28bda8b5d825836ee2c522b1c1d3c598 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Tue, 23 Oct 2018 03:09:06 +0200 Subject: [PATCH 101/209] FIX: validates import theme form (#6513) --- .../modals/admin-import-theme.js.es6 | 19 ++++++++++++++++--- .../templates/modal/admin-import-theme.hbs | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 index 4b9a5aa6dd..e69ada3b55 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-import-theme.js.es6 @@ -1,7 +1,10 @@ import ModalFunctionality from "discourse/mixins/modal-functionality"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; -import { observes } from "ember-addons/ember-computed-decorators"; +import { + default as computed, + observes +} from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend(ModalFunctionality, { local: Ember.computed.equal("selection", "local"), @@ -11,8 +14,14 @@ export default Ember.Controller.extend(ModalFunctionality, { loading: false, keyGenUrl: "/admin/themes/generate_key_pair", importUrl: "/admin/themes/import", - checkPrivate: Ember.computed.match("uploadUrl", /^git/), + localFile: null, + uploadUrl: null, + + @computed("loading", "remote", "uploadUrl", "local", "localFile") + importDisabled(isLoading, isRemote, uploadUrl, isLocal, localFile) { + return isLoading || (isRemote && !uploadUrl) || (isLocal && !localFile); + }, @observes("privateChecked") privateWasChecked() { @@ -32,6 +41,10 @@ export default Ember.Controller.extend(ModalFunctionality, { }, actions: { + uploadLocaleFile() { + this.set("localFile", $("#file-input")[0].files[0]); + }, + importTheme() { let options = { type: "POST" @@ -41,7 +54,7 @@ export default Ember.Controller.extend(ModalFunctionality, { options.processData = false; options.contentType = false; options.data = new FormData(); - options.data.append("theme", $("#file-input")[0].files[0]); + options.data.append("theme", this.get("localFile")); } else { options.data = { remote: this.get("uploadUrl"), diff --git a/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs b/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs index ed8e7ed681..9a42c1846b 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-import-theme.hbs @@ -4,7 +4,7 @@ {{#if local}}
-
+
{{i18n 'admin.customize.theme.import_file_tip'}}
{{/if}} @@ -44,6 +44,6 @@ {{/d-modal-body}} From cee51672c96d29174196717b734c7f7ba0754172 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Tue, 23 Oct 2018 03:10:33 +0200 Subject: [PATCH 102/209] FIX: Strip accents from search query 4481836 introduced accent stipping in search_indexer, but we need to strip it from the query itself as well TODO in search with diacritics: - Still need to fix excerpts on search page - need to support accent stripping in in_topic search - need to make sure that in:title works correctly - need to fix "word boldening" in titles --- app/services/search_indexer.rb | 11 +---------- lib/search.rb | 11 +++++++++++ spec/components/search_spec.rb | 27 ++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index e31bf8e491..34b132c9ae 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -167,8 +167,6 @@ class SearchIndexer class HtmlScrubber < Nokogiri::XML::SAX::Document - DIACRITICS ||= /([\u0300-\u036f]|[\u1AB0-\u1AFF]|[\u1DC0-\u1DFF]|[\u20D0-\u20FF])/ - attr_reader :scrubbed def initialize(strip_diacritics: false) @@ -196,15 +194,8 @@ class SearchIndexer end end - def strip_diacritics(str) - s = str.unicode_normalize(:nfkd) - s.gsub!(DIACRITICS, "") - s.strip! - s - end - def characters(str) - str = strip_diacritics(str) if @strip_diacritics + str = Search.strip_diacritics(str) if @strip_diacritics scrubbed << " #{str} " end end diff --git a/lib/search.rb b/lib/search.rb index d61833e999..4b2cdf7b72 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -2,11 +2,19 @@ require_dependency 'search/grouped_search_results' class Search INDEX_VERSION = 2.freeze + DIACRITICS ||= /([\u0300-\u036f]|[\u1AB0-\u1AFF]|[\u1DC0-\u1DFF]|[\u20D0-\u20FF])/ def self.per_facet 5 end + def self.strip_diacritics(str) + s = str.unicode_normalize(:nfkd) + s.gsub!(DIACRITICS, "") + s.strip! + s + end + def self.per_filter 50 end @@ -59,6 +67,9 @@ class Search end data.force_encoding("UTF-8") + if SiteSetting.search_ignore_accents + data = strip_diacritics(data) + end data end diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index d987571be3..408f1519ab 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -1020,21 +1020,42 @@ describe Search do end end - context 'diacritics' do + context 'ignore_diacritics' do + before { SiteSetting.search_ignore_accents = true } + let!(:post1) { Fabricate(:post, raw: 'สวัสดี Rágis hello') } + + it ('allows strips correctly') do + results = Search.execute('hello', type_filter: 'topic') + expect(results.posts.length).to eq(1) + + results = Search.execute('ragis', type_filter: 'topic') + expect(results.posts.length).to eq(1) + + results = Search.execute('Rágis', type_filter: 'topic', include_blurbs: true) + expect(results.posts.length).to eq(1) + + # TODO: this is a test we need to fix! + #expect(results.blurb(results.posts.first)).to include('Rágis') + + results = Search.execute('สวัสดี', type_filter: 'topic') + expect(results.posts.length).to eq(1) + end + end + + context 'include_diacritics' do + before { SiteSetting.search_ignore_accents = false } let!(:post1) { Fabricate(:post, raw: 'สวัสดี Régis hello') } it ('allows strips correctly') do results = Search.execute('hello', type_filter: 'topic') expect(results.posts.length).to eq(1) - # TODO when we add diacritic support we should return 1 here results = Search.execute('regis', type_filter: 'topic') expect(results.posts.length).to eq(0) results = Search.execute('Régis', type_filter: 'topic', include_blurbs: true) expect(results.posts.length).to eq(1) - # this is a test we got to keep working expect(results.blurb(results.posts.first)).to include('Régis') results = Search.execute('สวัสดี', type_filter: 'topic') From de6b58536888d2cee42a0e9874022b22131817f1 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 23 Oct 2018 12:20:21 +1100 Subject: [PATCH 103/209] minor, bypass gravatar update if user does not match this protects against a race condition that can happen when a user record is destroyed reasonably quickly --- app/jobs/regular/update_gravatar.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/update_gravatar.rb b/app/jobs/regular/update_gravatar.rb index bbff6a6b22..53d4f61a4f 100644 --- a/app/jobs/regular/update_gravatar.rb +++ b/app/jobs/regular/update_gravatar.rb @@ -8,7 +8,7 @@ module Jobs user = User.find_by(id: args[:user_id]) avatar = UserAvatar.find_by(id: args[:avatar_id]) - if user && avatar + if user && avatar && avatar.user&.id == user.id avatar.update_gravatar! if !user.uploaded_avatar_id && avatar.gravatar_upload_id user.update_column(:uploaded_avatar_id, avatar.gravatar_upload_id) From 541b6a8446663364ba554326b8f536dc11d44105 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 22 Oct 2018 22:16:15 -0400 Subject: [PATCH 104/209] UX: Allow vertical timeline to fit on narrower screens --- .../javascripts/discourse/components/topic-navigation.js.es6 | 2 +- app/assets/stylesheets/common/topic-timeline.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/components/topic-navigation.js.es6 b/app/assets/javascripts/discourse/components/topic-navigation.js.es6 index bc8ac812b0..b4b840cb6b 100644 --- a/app/assets/javascripts/discourse/components/topic-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-navigation.js.es6 @@ -35,7 +35,7 @@ export default Ember.Component.extend(PanEvents, { height -= $("#reply-control").height(); } - renderTimeline = width > 960 && height > 520; + renderTimeline = width > 924 && height > 520; } info.setProperties({ diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index d535a898d1..7e9b2200f5 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -180,7 +180,7 @@ .topic-timeline { margin-left: 3em; - width: 150px; + max-width: 135px; transition: opacity 0.2s ease-in; touch-action: none; From a82dfbd2dcd0f6755f4a581b5e8b4299b2e49d9e Mon Sep 17 00:00:00 2001 From: Kris Date: Tue, 23 Oct 2018 07:59:00 -0400 Subject: [PATCH 105/209] Mobile timeline fix --- app/assets/stylesheets/common/topic-timeline.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index 7e9b2200f5..253b89e2b9 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -180,7 +180,6 @@ .topic-timeline { margin-left: 3em; - max-width: 135px; transition: opacity 0.2s ease-in; touch-action: none; From 0b4edfc7d6bfef93add6eba5d5fc5ce6cf3fd79f Mon Sep 17 00:00:00 2001 From: Kris Date: Tue, 23 Oct 2018 16:37:36 -0400 Subject: [PATCH 106/209] UX: improve spacing on composer controls --- app/assets/stylesheets/common/base/compose.scss | 7 +++++-- .../stylesheets/common/select-kit/composer-actions.scss | 8 +++++--- .../common/select-kit/dropdown-select-box.scss | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index ba0e013c8b..020db3d5fd 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -144,10 +144,13 @@ } } .composer-controls { + display: flex; margin-left: auto; - margin-right: -5px; button { - padding: 0 2px; + padding: 5px 7px; + &.toggler { + order: 2; + } } } } diff --git a/app/assets/stylesheets/common/select-kit/composer-actions.scss b/app/assets/stylesheets/common/select-kit/composer-actions.scss index 7d89bbc71f..93f420fe5c 100644 --- a/app/assets/stylesheets/common/select-kit/composer-actions.scss +++ b/app/assets/stylesheets/common/select-kit/composer-actions.scss @@ -7,15 +7,17 @@ outline: none; padding: 0; margin-right: 5px; - + border: 1px solid $primary-low; + min-height: unset; + .d-icon { + padding: 5px 6px; + } &:hover, &:focus { background: $primary-low; } } .d-icon { - border: 1px solid $primary-low; - padding: 4px 5px; margin: 0 !important; } } diff --git a/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss b/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss index b240227b98..07e30b0489 100644 --- a/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss +++ b/app/assets/stylesheets/common/select-kit/dropdown-select-box.scss @@ -37,7 +37,7 @@ justify-content: center; -ms-flex-item-align: start; align-self: flex-start; - margin-right: 10px; + margin-right: 0.357em; margin-top: 2px; width: 30px; From 64aca0dc1be041459e4e8f70e031b44d3e6dbb73 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 24 Oct 2018 08:38:39 +1100 Subject: [PATCH 107/209] FIX: remove duplicate referrer policy Rails already ships with strict-origin-when-cross-origin, no need to also add no-referrer-when-downgrade see: https://meta.discourse.org/t/harden-referrer-policy-header/100172 --- config/nginx.sample.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf index b1a5944718..f22dc22f8b 100644 --- a/config/nginx.sample.conf +++ b/config/nginx.sample.conf @@ -261,7 +261,6 @@ server { } location @discourse { - add_header Referrer-Policy 'no-referrer-when-downgrade'; proxy_set_header Host $http_host; proxy_set_header X-Request-Start "t=${msec}"; proxy_set_header X-Real-IP $remote_addr; From 29fdb503387c0197f9280c8f384ce7693a3e3d48 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 24 Oct 2018 10:04:17 +1100 Subject: [PATCH 108/209] FIX: if poll has not options do not break serializer Note: we have a proper rewrite of this plugin in progress it will address this issue in a proper way --- plugins/poll/plugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb index 1f8dc37da3..21b586f98b 100644 --- a/plugins/poll/plugin.rb +++ b/plugins/poll/plugin.rb @@ -414,6 +414,7 @@ after_initialize do polls = post_custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD].dup polls.each do |_, poll| + next if !poll poll["options"].each do |option| option.delete("voter_ids") end From e605542c4e1ee8f67dffd06e7422cc17c83c243e Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 24 Oct 2018 11:53:28 +1100 Subject: [PATCH 109/209] PERF: limit unread count to 99 in the blue circle This safeguard is in place to avoid very expensive queries on the server side --- app/models/user.rb | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 06b6ba89b6..b7925553aa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -434,24 +434,31 @@ class User < ActiveRecord::Base @unread_pms ||= unread_notifications_of_type(Notification.types[:private_message]) end + # PERF: This safeguard is in place to avoid situations where + # a user with enormous amounts of unread data can issue extremely + # expensive queries + MAX_UNREAD_NOTIFICATIONS = 99 + def unread_notifications @unread_notifications ||= begin # perf critical, much more efficient than AR sql = <<~SQL - SELECT COUNT(*) - FROM notifications n - LEFT JOIN topics t ON t.id = n.topic_id - WHERE t.deleted_at IS NULL - AND n.notification_type <> :pm - AND n.user_id = :user_id - AND n.id > :seen_notification_id - AND NOT read + SELECT COUNT(*) FROM notifications n + LEFT JOIN topics t ON t.id = n.topic_id + WHERE t.deleted_at IS NULL AND + n.notification_type <> :pm AND + n.user_id = :user_id AND + n.id > :seen_notification_id AND + NOT read + LIMIT :limit + SQL DB.query_single(sql, user_id: id, seen_notification_id: seen_notification_id, - pm: Notification.types[:private_message] + pm: Notification.types[:private_message], + limit: MAX_UNREAD_NOTIFICATIONS )[0].to_i end end From 5fd94d32115299ee481a197dbb8e3e47bf8426d2 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 24 Oct 2018 12:10:27 +1100 Subject: [PATCH 110/209] PERF: limit unread count to 99 in blue circle This revises: https://github.com/discourse/discourse/commit/e605542c4e1ee8f67dffd06e7422cc17c83c243e Previous commit was faulty --- app/models/user.rb | 30 ++++++++++++++++++++---------- spec/models/user_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index b7925553aa..18553d08d5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -439,26 +439,36 @@ class User < ActiveRecord::Base # expensive queries MAX_UNREAD_NOTIFICATIONS = 99 + def self.max_unread_notifications + @max_unread_notifications ||= MAX_UNREAD_NOTIFICATIONS + end + + def self.max_unread_notifications=(val) + @max_unread_notifications = val + end + def unread_notifications @unread_notifications ||= begin # perf critical, much more efficient than AR sql = <<~SQL - SELECT COUNT(*) FROM notifications n - LEFT JOIN topics t ON t.id = n.topic_id - WHERE t.deleted_at IS NULL AND - n.notification_type <> :pm AND - n.user_id = :user_id AND - n.id > :seen_notification_id AND - NOT read - LIMIT :limit - + SELECT COUNT(*) FROM ( + SELECT 1 FROM + notifications n + LEFT JOIN topics t ON t.id = n.topic_id + WHERE t.deleted_at IS NULL AND + n.notification_type <> :pm AND + n.user_id = :user_id AND + n.id > :seen_notification_id AND + NOT read + LIMIT :limit + ) AS X SQL DB.query_single(sql, user_id: id, seen_notification_id: seen_notification_id, pm: Notification.types[:private_message], - limit: MAX_UNREAD_NOTIFICATIONS + limit: User.max_unread_notifications )[0].to_i end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 04338ffb6d..42d73ae707 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1668,6 +1668,26 @@ describe User do end end + describe "#unread_notifications" do + before do + User.max_unread_notifications = 3 + end + + after do + User.max_unread_notifications = nil + end + + it "limits to MAX_UNREAD_NOTIFICATIONS" do + user = Fabricate(:user) + + 4.times do + Notification.create!(user_id: user.id, notification_type: 1, read: false, data: '{}') + end + + expect(user.unread_notifications).to eq(3) + end + end + describe "#unstage" do let!(:staged_user) { Fabricate(:staged, email: 'staged@account.com', active: true, username: 'staged1', name: 'Stage Name') } let(:params) { { email: 'staged@account.com', active: true, username: 'unstaged1', name: 'Foo Bar' } } From 63356d883ebf6d37b8f9c6a813a240920b6e304c Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Tue, 23 Oct 2018 23:34:10 -0400 Subject: [PATCH 111/209] FIX: GlobalPath#upload_cdn_path when S3 bucket has a folder (#6523) --- lib/global_path.rb | 2 +- spec/components/global_path_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/global_path.rb b/lib/global_path.rb index fc9557210a..e7f89c0ddf 100644 --- a/lib/global_path.rb +++ b/lib/global_path.rb @@ -9,7 +9,7 @@ module GlobalPath def upload_cdn_path(p) if SiteSetting.Upload.s3_cdn_url.present? - p = p.sub(Discourse.store.absolute_base_url, SiteSetting.Upload.s3_cdn_url) + p = Discourse.store.cdn_url(p) end p =~ /^http/ ? p : cdn_path(p) end diff --git a/spec/components/global_path_spec.rb b/spec/components/global_path_spec.rb index 41b778c9af..daf8c49b0d 100644 --- a/spec/components/global_path_spec.rb +++ b/spec/components/global_path_spec.rb @@ -25,6 +25,18 @@ describe GlobalPath do GlobalSetting.expects(:cdn_url).returns("https://something.com:221/foo") expect(cdn_relative_path("/test")).to eq("/foo/test") end + end + describe '#upload_cdn_path' do + it 'generates correctly when S3 bucket has a folder' do + global_setting :s3_access_key_id, 's3_access_key_id' + global_setting :s3_secret_access_key, 's3_secret_access_key' + global_setting :s3_bucket, 'file-uploads/folder' + global_setting :s3_region, 'us-west-2' + global_setting :s3_cdn_url, 'https://cdn-aws.com/folder' + + expect(GlobalPathInstance.upload_cdn_path("#{Discourse.store.absolute_base_url}/folder/upload.jpg")) + .to eq("https://cdn-aws.com/folder/upload.jpg") + end end end From 322b27b6dcba0ec37323b3fef64fec6075c68fe6 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 24 Oct 2018 15:03:58 +1100 Subject: [PATCH 112/209] Revert "FIX: GlobalPath#upload_cdn_path when S3 bucket has a folder (#6523)" This reverts commit 63356d883ebf6d37b8f9c6a813a240920b6e304c. This caused an outage, got to revert --- lib/global_path.rb | 2 +- spec/components/global_path_spec.rb | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/global_path.rb b/lib/global_path.rb index e7f89c0ddf..fc9557210a 100644 --- a/lib/global_path.rb +++ b/lib/global_path.rb @@ -9,7 +9,7 @@ module GlobalPath def upload_cdn_path(p) if SiteSetting.Upload.s3_cdn_url.present? - p = Discourse.store.cdn_url(p) + p = p.sub(Discourse.store.absolute_base_url, SiteSetting.Upload.s3_cdn_url) end p =~ /^http/ ? p : cdn_path(p) end diff --git a/spec/components/global_path_spec.rb b/spec/components/global_path_spec.rb index daf8c49b0d..41b778c9af 100644 --- a/spec/components/global_path_spec.rb +++ b/spec/components/global_path_spec.rb @@ -25,18 +25,6 @@ describe GlobalPath do GlobalSetting.expects(:cdn_url).returns("https://something.com:221/foo") expect(cdn_relative_path("/test")).to eq("/foo/test") end - end - describe '#upload_cdn_path' do - it 'generates correctly when S3 bucket has a folder' do - global_setting :s3_access_key_id, 's3_access_key_id' - global_setting :s3_secret_access_key, 's3_secret_access_key' - global_setting :s3_bucket, 'file-uploads/folder' - global_setting :s3_region, 'us-west-2' - global_setting :s3_cdn_url, 'https://cdn-aws.com/folder' - - expect(GlobalPathInstance.upload_cdn_path("#{Discourse.store.absolute_base_url}/folder/upload.jpg")) - .to eq("https://cdn-aws.com/folder/upload.jpg") - end end end From e955a7b49de8db6b04676756ea93b4450171918f Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 24 Oct 2018 15:14:01 +1100 Subject: [PATCH 113/209] Revert "Revert "FIX: GlobalPath#upload_cdn_path when S3 bucket has a folder (#6523)"" This reverts commit 322b27b6dcba0ec37323b3fef64fec6075c68fe6. Oops rushed on the revert here... should be good --- lib/global_path.rb | 2 +- spec/components/global_path_spec.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/global_path.rb b/lib/global_path.rb index fc9557210a..e7f89c0ddf 100644 --- a/lib/global_path.rb +++ b/lib/global_path.rb @@ -9,7 +9,7 @@ module GlobalPath def upload_cdn_path(p) if SiteSetting.Upload.s3_cdn_url.present? - p = p.sub(Discourse.store.absolute_base_url, SiteSetting.Upload.s3_cdn_url) + p = Discourse.store.cdn_url(p) end p =~ /^http/ ? p : cdn_path(p) end diff --git a/spec/components/global_path_spec.rb b/spec/components/global_path_spec.rb index 41b778c9af..daf8c49b0d 100644 --- a/spec/components/global_path_spec.rb +++ b/spec/components/global_path_spec.rb @@ -25,6 +25,18 @@ describe GlobalPath do GlobalSetting.expects(:cdn_url).returns("https://something.com:221/foo") expect(cdn_relative_path("/test")).to eq("/foo/test") end + end + describe '#upload_cdn_path' do + it 'generates correctly when S3 bucket has a folder' do + global_setting :s3_access_key_id, 's3_access_key_id' + global_setting :s3_secret_access_key, 's3_secret_access_key' + global_setting :s3_bucket, 'file-uploads/folder' + global_setting :s3_region, 'us-west-2' + global_setting :s3_cdn_url, 'https://cdn-aws.com/folder' + + expect(GlobalPathInstance.upload_cdn_path("#{Discourse.store.absolute_base_url}/folder/upload.jpg")) + .to eq("https://cdn-aws.com/folder/upload.jpg") + end end end From 05438d99a85c5cb8825dbb7d481cd34237302460 Mon Sep 17 00:00:00 2001 From: Matthew Campbell Date: Wed, 24 Oct 2018 06:58:42 -0700 Subject: [PATCH 114/209] FIX: Ensure the like button always has a title, for accessibility (#6525) The like button previously didn't have a title for anonymous users, because the `canToggleLike` flag wasn't set, but the `liked` flag wasn't set either. This made the button inaccessible to blind users. --- .../javascripts/discourse/widgets/post-menu.js.es6 | 13 +++++++++---- test/javascripts/widgets/post-test.js.es6 | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index 7e65b0ac47..0a1eaf0e16 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -67,13 +67,18 @@ registerButton("like", attrs => { className }; - if (attrs.canToggleLike) { + // If the user has already liked the post and doesn't have permission + // to undo that operation, then indicate via the title that they've liked it + // and disable the button. Otherwise, set the title even if the user + // is anonymous (meaning they don't currently have permission to like); + // this is important for accessibility. + if (attrs.liked && !attrs.canToggleLike) { + button.title = "post.controls.has_liked"; + button.disabled = true; + } else { button.title = attrs.liked ? "post.controls.undo_like" : "post.controls.like"; - } else if (attrs.liked) { - button.title = "post.controls.has_liked"; - button.disabled = true; } return button; diff --git a/test/javascripts/widgets/post-test.js.es6 b/test/javascripts/widgets/post-test.js.es6 index f01f7df534..5c4ff36fd3 100644 --- a/test/javascripts/widgets/post-test.js.es6 +++ b/test/javascripts/widgets/post-test.js.es6 @@ -206,6 +206,12 @@ widgetTest("anon liking", { assert.ok(!!this.$(".actions button.like").length); assert.ok(this.$(".actions button.like-count").length === 0); + assert.equal( + this.$("button.like").attr("title"), + I18n.t("post.controls.like"), + `shows the right button title for anonymous users` + ); + await click(".actions button.like"); assert.ok(this.loginShown); } From addf6f6d1736f26fb309085061e5f9183335e01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 24 Oct 2018 21:23:18 +0200 Subject: [PATCH 115/209] FIX: support comma in 'sso_provider_secrets' site setting --- lib/single_sign_on.rb | 2 +- spec/requests/session_controller_spec.rb | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/single_sign_on.rb b/lib/single_sign_on.rb index 2758ad65f9..686591f51d 100644 --- a/lib/single_sign_on.rb +++ b/lib/single_sign_on.rb @@ -89,7 +89,7 @@ class SingleSignOn end def self.provider_secret(return_sso_url) - provider_secrets = SiteSetting.sso_provider_secrets.split(/[\|,\n]/) + provider_secrets = SiteSetting.sso_provider_secrets.split(/[|\n]/) provider_secrets_hash = Hash[*provider_secrets] return_url_host = URI.parse(return_sso_url).host # moves wildcard domains to the end of hash diff --git a/spec/requests/session_controller_spec.rb b/spec/requests/session_controller_spec.rb index a6c5e69d86..3453d5f82d 100644 --- a/spec/requests/session_controller_spec.rb +++ b/spec/requests/session_controller_spec.rb @@ -589,7 +589,12 @@ RSpec.describe SessionController do SiteSetting.enable_sso_provider = true SiteSetting.enable_sso = false SiteSetting.enable_local_logins = true - SiteSetting.sso_provider_secrets = "*|secretforAll\n*.rainbow|wrongSecretForOverRainbow\nwww.random.site|secretForRandomSite\nsomewhere.over.rainbow|secretForOverRainbow" + SiteSetting.sso_provider_secrets = [ + "*|secret,forAll", + "*.rainbow|wrongSecretForOverRainbow", + "www.random.site|secretForRandomSite", + "somewhere.over.rainbow|secretForOverRainbow", + ].join("\n") @sso = SingleSignOn.new @sso.nonce = "mynonce" From 0140844eb0457e54a8286662a0248054e772eee8 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 24 Oct 2018 16:00:22 -0400 Subject: [PATCH 116/209] Remove whitespace in template so we can use :empty psuedo --- .../javascripts/discourse/templates/components/d-editor.hbs | 4 +--- app/assets/stylesheets/common/d-editor.scss | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs index 61e771652e..fe677e8203 100644 --- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -45,9 +45,7 @@
-
- {{{preview}}} -
+
{{{preview}}}
{{plugin-outlet name="editor-preview" classNames="d-editor-plugin"}}
diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index f60937da41..329fc369c7 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -182,6 +182,9 @@ .d-editor-preview { background-color: $primary-very-low; padding: 5px; + &:empty { + padding: 0; + } } } From c219a5fb1e09bf3c91bf77e15d39d1697244a11c Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 24 Oct 2018 16:09:36 -0400 Subject: [PATCH 117/209] Add btn-default class to all default buttons (#6521) --- .../admin/components/admin-report.js.es6 | 2 +- .../javascripts/admin/templates/api-keys.hbs | 4 +- .../admin/templates/backups-index.hbs | 12 ++-- .../javascripts/admin/templates/backups.hbs | 2 +- .../admin/templates/badges-index.hbs | 2 +- .../admin/templates/badges-show.hbs | 2 +- .../javascripts/admin/templates/badges.hbs | 2 +- .../templates/components/admin-report.hbs | 2 +- .../components/admin-user-field-item.hbs | 4 +- .../templates/components/flagged-post.hbs | 7 ++- .../templates/components/permalink-form.hbs | 2 +- .../components/screened-ip-address-form.hbs | 2 +- .../templates/components/site-setting.hbs | 2 +- .../components/site-text-summary.hbs | 2 +- .../admin/templates/components/value-list.hbs | 2 +- .../components/watched-word-form.hbs | 2 +- .../components/watched-word-uploader.hbs | 2 +- .../admin/templates/customize-colors-show.hbs | 10 ++-- .../admin/templates/customize-colors.hbs | 2 +- .../admin/templates/customize-themes-show.hbs | 22 +++---- .../admin/templates/customize-themes.hbs | 2 +- .../admin/templates/dashboard-problems.hbs | 2 +- .../templates/dashboard_next_general.hbs | 2 +- .../admin/templates/email-preview-digest.hbs | 2 +- .../admin/templates/logs/screened-emails.hbs | 2 +- .../templates/logs/screened-ip-addresses.hbs | 14 ++--- .../admin/templates/logs/screened-urls.hbs | 2 +- .../templates/logs/staff-action-logs.hbs | 2 +- .../admin/templates/plugins-index.hbs | 2 +- .../javascripts/admin/templates/plugins.hbs | 2 +- .../admin/templates/site-settings.hbs | 2 +- .../admin/templates/user-index.hbs | 59 ++++++++++--------- .../admin/templates/users-list-show.hbs | 2 +- .../admin/templates/users-list.hbs | 4 +- .../javascripts/admin/templates/web-hooks.hbs | 2 +- .../discourse/components/share-button.js.es6 | 2 +- .../controllers/topic-bulk-actions.js.es6 | 37 +++++++++--- .../components/bulk-select-button.hbs | 2 +- .../components/categories-topic-list.hbs | 2 +- .../desktop-notification-config.hbs | 8 +-- .../components/edit-category-security.hbs | 2 +- .../templates/components/image-uploader.hbs | 2 +- .../templates/components/ip-lookup.hbs | 6 +- .../components/top-period-buttons.hbs | 2 +- .../components/topic-footer-buttons.hbs | 10 ++-- .../components/user-card-contents.hbs | 1 + .../templates/components/user-stream-item.hbs | 4 +- .../discourse/templates/discovery/topics.hbs | 8 +-- .../discourse/templates/full-page-search.hbs | 2 +- .../discourse/templates/groups/index.hbs | 2 +- .../mobile/components/categories-only.hbs | 2 +- .../templates/mobile/discovery/topics.hbs | 4 +- .../templates/modal/avatar-selector.hbs | 2 +- .../templates/modal/edit-category.hbs | 2 +- .../discourse/templates/modal/history.hbs | 12 ++-- .../templates/modal/reorder-categories.hbs | 4 +- .../templates/preferences/account.hbs | 14 ++--- .../discourse/templates/queued-posts.hbs | 2 +- .../discourse/templates/tag-groups-show.hbs | 2 +- .../discourse/templates/tag-groups.hbs | 2 +- .../topic-list-header-column.raw.hbs | 4 +- .../javascripts/discourse/templates/topic.hbs | 2 +- .../javascripts/discourse/templates/user.hbs | 6 +- .../discourse/templates/user/activity.hbs | 2 +- .../discourse/templates/user/messages.hbs | 8 +-- .../templates/user/notifications.hbs | 2 +- .../discourse/widgets/post-admin-menu.js.es6 | 17 +++--- .../widgets/private-message-map.js.es6 | 2 +- .../discourse/widgets/topic-admin-menu.js.es6 | 15 ++++- .../discourse/widgets/topic-timeline.js.es6 | 2 +- .../dropdown-select-box-header.js.es6 | 2 +- .../toolbar-popup-menu-options.scss | 1 - app/views/exceptions/not_found.html.erb | 4 +- .../discourse-local-dates-create-form.hbs | 4 +- vendor/assets/javascripts/bootbox.js | 2 +- 75 files changed, 219 insertions(+), 178 deletions(-) diff --git a/app/assets/javascripts/admin/components/admin-report.js.es6 b/app/assets/javascripts/admin/components/admin-report.js.es6 index 0a1947e4b4..feab25677b 100644 --- a/app/assets/javascripts/admin/components/admin-report.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report.js.es6 @@ -140,7 +140,7 @@ export default Ember.Component.extend({ const modes = forcedModes ? forcedModes.split(",") : reportModes; return Ember.makeArray(modes).map(mode => { - const base = `mode-btn ${mode}`; + const base = `btn-default mode-btn ${mode}`; const cssClass = currentMode === mode ? `${base} is-current` : base; return { diff --git a/app/assets/javascripts/admin/templates/api-keys.hbs b/app/assets/javascripts/admin/templates/api-keys.hbs index e1907da87f..e256e127cf 100644 --- a/app/assets/javascripts/admin/templates/api-keys.hbs +++ b/app/assets/javascripts/admin/templates/api-keys.hbs @@ -19,8 +19,8 @@ {{/if}} - {{d-button action="regenerateKey" actionParam=k icon="undo" label='admin.api.regenerate'}} - {{d-button action="revokeKey" actionParam=k icon="times" label='admin.api.revoke'}} + {{d-button class="btn-default" action="regenerateKey" actionParam=k icon="undo" label='admin.api.regenerate'}} + {{d-button class="btn-default" action="revokeKey" actionParam=k icon="times" label='admin.api.revoke'}} {{/each}} diff --git a/app/assets/javascripts/admin/templates/backups-index.hbs b/app/assets/javascripts/admin/templates/backups-index.hbs index bcf8dccaa4..0e9e97037f 100644 --- a/app/assets/javascripts/admin/templates/backups-index.hbs +++ b/app/assets/javascripts/admin/templates/backups-index.hbs @@ -1,14 +1,14 @@
{{#if localBackupStorage}} - {{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadLabel title="admin.backups.upload.title"}} + {{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadLabel title="admin.backups.upload.title" class="btn-default"}} {{else}} {{backup-uploader done="remoteUploadSuccess"}} {{/if}} {{#if site.isReadOnly}} - {{d-button icon="eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}} + {{d-button class="btn-default" icon="eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}} {{else}} - {{d-button icon="eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.enable.title" label="admin.backups.read_only.enable.label"}} + {{d-button class="btn-default" icon="eye" action="toggleReadOnlyMode" disabled=status.isOperationRunning title="admin.backups.read_only.enable.title" label="admin.backups.read_only.enable.label"}} {{/if}}
@@ -24,7 +24,7 @@ diff --git a/app/assets/javascripts/admin/templates/backups.hbs b/app/assets/javascripts/admin/templates/backups.hbs index fe7861839f..21b104a9b2 100644 --- a/app/assets/javascripts/admin/templates/backups.hbs +++ b/app/assets/javascripts/admin/templates/backups.hbs @@ -8,7 +8,7 @@
{{#if model.canRollback}} {{d-button action="rollback" - class="btn-rollback" + class="btn-default btn-rollback" label="admin.backups.operations.rollback.label" title="admin.backups.operations.rollback.title" icon="ambulance" diff --git a/app/assets/javascripts/admin/templates/badges-index.hbs b/app/assets/javascripts/admin/templates/badges-index.hbs index 4a71e7d69a..c0abf3f991 100644 --- a/app/assets/javascripts/admin/templates/badges-index.hbs +++ b/app/assets/javascripts/admin/templates/badges-index.hbs @@ -2,7 +2,7 @@

{{i18n 'admin.badges.none_selected'}}

- {{#link-to 'adminBadges.show' 'new' class="btn"}} + {{#link-to 'adminBadges.show' 'new' class="btn btn-default"}} {{d-icon "plus"}} {{i18n 'admin.badges.new'}} {{/link-to}}
diff --git a/app/assets/javascripts/admin/templates/badges-show.hbs b/app/assets/javascripts/admin/templates/badges-show.hbs index 7436c48898..f8995fd540 100644 --- a/app/assets/javascripts/admin/templates/badges-show.hbs +++ b/app/assets/javascripts/admin/templates/badges-show.hbs @@ -36,7 +36,7 @@ value=buffered.badge_grouping_id content=badgeGroupings nameProperty="name"}} -   +  
diff --git a/app/assets/javascripts/admin/templates/badges.hbs b/app/assets/javascripts/admin/templates/badges.hbs index b5789d4c42..04b60d10e2 100644 --- a/app/assets/javascripts/admin/templates/badges.hbs +++ b/app/assets/javascripts/admin/templates/badges.hbs @@ -14,7 +14,7 @@ {{/each}} - {{#link-to 'adminBadges.show' 'new' class="btn"}} + {{#link-to 'adminBadges.show' 'new' class="btn btn-default"}} {{d-icon "plus"}} {{i18n 'admin.badges.new'}} {{/link-to}}
diff --git a/app/assets/javascripts/admin/templates/components/admin-report.hbs b/app/assets/javascripts/admin/templates/components/admin-report.hbs index 9024f099f0..fb24af76c3 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report.hbs @@ -167,7 +167,7 @@
{{d-button - class="export-csv-btn" + class="btn-default export-csv-btn" action="exportCsv" label="admin.export_csv.button_text" icon="download"}} diff --git a/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs b/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs index 99f1de8ce2..0074c99f1a 100644 --- a/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-user-field-item.hbs @@ -48,8 +48,8 @@
{{d-button action="edit" class="btn-default" icon="pencil" label="admin.user_fields.edit"}} {{d-button action="destroy" class="btn-danger" icon="trash-o" label="admin.user_fields.delete"}} - {{d-button action="moveUp" icon="arrow-up" disabled=cantMoveUp}} - {{d-button action="moveDown" icon="arrow-down" disabled=cantMoveDown}} + {{d-button action="moveUp" class="btn-default" icon="arrow-up" disabled=cantMoveUp}} + {{d-button action="moveDown" class="btn-default" icon="arrow-down" disabled=cantMoveDown}}
{{flags}}
diff --git a/app/assets/javascripts/admin/templates/components/flagged-post.hbs b/app/assets/javascripts/admin/templates/components/flagged-post.hbs index 37631f1442..0ef0f3d8a4 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-post.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-post.hbs @@ -77,21 +77,21 @@ {{#if flaggedPost.postHidden}} {{d-button title="admin.flags.disagree_flag_unhide_post_title" - class="disagree-flag" + class="btn-default disagree-flag" action="disagree" icon="thumbs-o-down" label="admin.flags.disagree_flag_unhide_post"}} {{else}} {{d-button title="admin.flags.disagree_flag_title" - class="disagree-flag" + class="btn-default disagree-flag" action="disagree" icon="thumbs-o-down" label="admin.flags.disagree_flag"}} {{/if}} {{d-button - class="defer-flag" + class="btn-default defer-flag" title="admin.flags.ignore_flag_title" action="defer" icon="external-link" @@ -103,6 +103,7 @@ {{/if}} {{d-button + class="btn-default" icon="list" label="admin.flags.moderation_history" action=(action "showModerationHistory")}} diff --git a/app/assets/javascripts/admin/templates/components/permalink-form.hbs b/app/assets/javascripts/admin/templates/components/permalink-form.hbs index c794254331..dc37c00991 100644 --- a/app/assets/javascripts/admin/templates/components/permalink-form.hbs +++ b/app/assets/javascripts/admin/templates/components/permalink-form.hbs @@ -2,4 +2,4 @@ {{text-field value=url disabled=formSubmitted class="permalink-url" placeholderKey="admin.permalink.url" autocorrect="off" autocapitalize="off"}} {{combo-box content=permalinkTypes value=permalinkType}} {{text-field value=permalink_type_value disabled=formSubmitted class="external-url" placeholderKey=permalinkTypePlaceholder autocorrect="off" autocapitalize="off"}} -{{d-button action="submit" disabled=formSubmitted label="admin.permalink.form.add"}} +{{d-button class="btn-default" action="submit" disabled=formSubmitted label="admin.permalink.form.add"}} diff --git a/app/assets/javascripts/admin/templates/components/screened-ip-address-form.hbs b/app/assets/javascripts/admin/templates/components/screened-ip-address-form.hbs index e846b1c652..82fb3162e2 100644 --- a/app/assets/javascripts/admin/templates/components/screened-ip-address-form.hbs +++ b/app/assets/javascripts/admin/templates/components/screened-ip-address-form.hbs @@ -1,4 +1,4 @@ {{i18n 'admin.logs.screened_ips.form.label'}} {{text-field value=ip_address disabled=formSubmitted class="ip-address-input" placeholderKey="admin.logs.screened_ips.form.ip_address" autocorrect="off" autocapitalize="off"}} {{combo-box content=actionNames value=actionName}} -{{d-button action="submit" disabled=formSubmitted label="admin.logs.screened_ips.form.add"}} +{{d-button class="btn-default" action="submit" disabled=formSubmitted label="admin.logs.screened_ips.form.add"}} diff --git a/app/assets/javascripts/admin/templates/components/site-setting.hbs b/app/assets/javascripts/admin/templates/components/site-setting.hbs index 558781c33d..a832080ea7 100644 --- a/app/assets/javascripts/admin/templates/components/site-setting.hbs +++ b/app/assets/javascripts/admin/templates/components/site-setting.hbs @@ -13,5 +13,5 @@ {{#if setting.secret}} {{d-button action="toggleSecret" icon="eye-slash"}} {{/if}} - {{d-button class="undo" action="resetDefault" icon="undo" label="admin.settings.reset"}} + {{d-button class="btn-default undo" action="resetDefault" icon="undo" label="admin.settings.reset"}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/components/site-text-summary.hbs b/app/assets/javascripts/admin/templates/components/site-text-summary.hbs index bf5ee8c7f0..60cfc9843d 100644 --- a/app/assets/javascripts/admin/templates/components/site-text-summary.hbs +++ b/app/assets/javascripts/admin/templates/components/site-text-summary.hbs @@ -1,4 +1,4 @@ -{{d-button label="admin.site_text.edit" class='edit' action="edit"}} +{{d-button label="admin.site_text.edit" class='btn-default edit' action="edit"}}

{{siteText.id}}

{{siteText.value}}
diff --git a/app/assets/javascripts/admin/templates/components/value-list.hbs b/app/assets/javascripts/admin/templates/components/value-list.hbs index 94a63000f8..d74b600ca4 100644 --- a/app/assets/javascripts/admin/templates/components/value-list.hbs +++ b/app/assets/javascripts/admin/templates/components/value-list.hbs @@ -5,7 +5,7 @@ {{d-button action="removeValue" actionParam=value icon="times" - class="remove-value-btn btn-small"}} + class="btn-default remove-value-btn btn-small"}} {{input title=value value=value class="value-input" focus-out=(action "changeValue" index)}}
diff --git a/app/assets/javascripts/admin/templates/components/watched-word-form.hbs b/app/assets/javascripts/admin/templates/components/watched-word-form.hbs index e4b2fab801..2b109e7385 100644 --- a/app/assets/javascripts/admin/templates/components/watched-word-form.hbs +++ b/app/assets/javascripts/admin/templates/components/watched-word-form.hbs @@ -1,6 +1,6 @@ {{i18n 'admin.watched_words.form.label'}} {{text-field value=word disabled=formSubmitted class="watched-word-input" autocorrect="off" autocapitalize="off" placeholderKey=placeholderKey}} -{{d-button action="submit" disabled=formSubmitted label="admin.watched_words.form.add"}} +{{d-button class="btn-default" action="submit" disabled=formSubmitted label="admin.watched_words.form.add"}} {{#if showMessage}} {{message}} diff --git a/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs b/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs index e69083494f..d7c7f2a572 100644 --- a/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs +++ b/app/assets/javascripts/admin/templates/components/watched-word-uploader.hbs @@ -1,4 +1,4 @@ - diff --git a/app/assets/javascripts/admin/templates/customize-colors.hbs b/app/assets/javascripts/admin/templates/customize-colors.hbs index 761996b6cb..b09fc1855a 100644 --- a/app/assets/javascripts/admin/templates/customize-colors.hbs +++ b/app/assets/javascripts/admin/templates/customize-colors.hbs @@ -9,7 +9,7 @@ {{/unless}} {{/each}} - + {{outlet}} diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs index c957a44c47..fd540a694e 100644 --- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs @@ -50,10 +50,10 @@ icon="paint-brush"}} {{#if colorSchemeChanged}} {{d-button action="changeScheme" class="btn-primary btn-small submit-edit" icon="check"}} - {{d-button action="cancelChangeScheme" class="btn-small cancel-edit" icon="times"}} + {{d-button action="cancelChangeScheme" class="btn-default btn-small cancel-edit" icon="times"}} {{/if}} - {{#link-to 'adminCustomize.colors' class="btn edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}} + {{#link-to 'adminCustomize.colors' class="btn btn-default edit"}}{{i18n 'admin.customize.colors.edit'}}{{/link-to}} {{/unless}} @@ -76,11 +76,11 @@ {{#if model.remote_theme.commits_behind}} {{#d-button action="updateToLatest" icon="download" class='btn-primary'}}{{i18n "admin.customize.theme.update_to_latest"}}{{/d-button}} {{else}} - {{#d-button action="checkForThemeUpdates" icon="refresh"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}} + {{#d-button action="checkForThemeUpdates" icon="refresh" class="btn-default"}}{{i18n "admin.customize.theme.check_for_updates"}}{{/d-button}} {{/if}} {{/if}} - {{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}} + {{#d-button action="editTheme" class="btn btn-default edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}} {{#if model.remote_theme}} {{#if updatingRemote}} @@ -119,7 +119,7 @@
  • ${{upload.name}}: {{upload.filename}} - {{d-button action="removeUpload" actionParam=upload class="second btn-small cancel-edit" icon="times"}} + {{d-button action="removeUpload" actionParam=upload class="second btn-default btn-small cancel-edit" icon="times"}}
  • {{/each}} @@ -127,7 +127,7 @@ {{else}}
    {{i18n "admin.customize.theme.no_uploads"}}
    {{/if}} - {{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}} + {{#d-button action="addUploadModal" class="btn-default" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}} {{#if hasSettings}} @@ -147,22 +147,22 @@ {{#if model.childThemes.length}}
      {{#each model.childThemes as |child|}} -
    • {{#link-to 'adminCustomizeThemes.show' child replace=true class='col'}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-small cancel-edit col" icon="times"}}
    • +
    • {{#link-to 'adminCustomizeThemes.show' child replace=true class='col'}}{{child.name}}{{/link-to}} {{d-button action="removeChildTheme" actionParam=child class="btn-default btn-small cancel-edit col" icon="times"}}
    • {{/each}}
    {{/if}} {{#if selectableChildThemes}}
    {{combo-box forceEscape=true filterable=true content=selectableChildThemes value=selectedChildThemeId none="admin.customize.theme.select_component"}} - {{#d-button action="addChildTheme" icon="plus" disabled=addButtonDisabled class="add-component-button"}}{{i18n "admin.customize.theme.add"}}{{/d-button}} + {{#d-button action="addChildTheme" icon="plus" disabled=addButtonDisabled class="btn-default add-component-button"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
    {{/if}} {{/if}} - {{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}} - {{d-icon "download"}} {{i18n 'admin.export_json.button_text'}} + {{d-icon 'desktop'}}{{i18n 'admin.customize.theme.preview'}} + {{d-icon "download"}} {{i18n 'admin.export_json.button_text'}} - {{d-button action="switchType" label="admin.customize.theme.convert" icon=convertIcon class="btn-normal" title=convertTooltip}} + {{d-button action="switchType" label="admin.customize.theme.convert" icon=convertIcon class="btn-default btn-normal" title=convertTooltip}} {{d-button action="destroy" label="admin.customize.delete" icon="trash" class="btn-danger"}} diff --git a/app/assets/javascripts/admin/templates/customize-themes.hbs b/app/assets/javascripts/admin/templates/customize-themes.hbs index 6d3e0648cf..82985568c2 100644 --- a/app/assets/javascripts/admin/templates/customize-themes.hbs +++ b/app/assets/javascripts/admin/templates/customize-themes.hbs @@ -4,7 +4,7 @@
    {{d-button label="admin.customize.new" icon="plus" action="showCreateModal" class="btn-primary"}} - {{d-button action="importModal" icon="upload" label="admin.customize.import"}} + {{d-button action="importModal" icon="upload" label="admin.customize.import" class="btn-default"}}
    {{themes-list themes=fullThemes components=childThemes currentTab=currentTab}} diff --git a/app/assets/javascripts/admin/templates/dashboard-problems.hbs b/app/assets/javascripts/admin/templates/dashboard-problems.hbs index 81bee62b28..c0c6b9ca0b 100644 --- a/app/assets/javascripts/admin/templates/dashboard-problems.hbs +++ b/app/assets/javascripts/admin/templates/dashboard-problems.hbs @@ -19,7 +19,7 @@

    {{i18n 'admin.dashboard.last_checked'}}: {{problemsTimestamp}} - {{d-button action="refreshProblems" class="btn-small" icon="refresh" label="admin.dashboard.refresh_problems"}} + {{d-button action="refreshProblems" class="btn-default btn-small" icon="refresh" label="admin.dashboard.refresh_problems"}}

    {{/conditional-loading-section}} diff --git a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs index 88f36a5d59..bb6b5270e7 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next_general.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next_general.hbs @@ -137,7 +137,7 @@

    {{i18n "admin.dashboard.last_updated"}}

    {{updatedTimestamp}}

    - + {{i18n "admin.dashboard.whats_new_in_discourse"}}
    diff --git a/app/assets/javascripts/admin/templates/email-preview-digest.hbs b/app/assets/javascripts/admin/templates/email-preview-digest.hbs index 87372268c7..70fe22afe6 100644 --- a/app/assets/javascripts/admin/templates/email-preview-digest.hbs +++ b/app/assets/javascripts/admin/templates/email-preview-digest.hbs @@ -29,7 +29,7 @@ {{else}} {{text-field value=email placeholderKey="admin.email.test_email_address"}} - + {{#if sentEmail}} {{i18n 'admin.email.sent_test'}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/logs/screened-emails.hbs b/app/assets/javascripts/admin/templates/logs/screened-emails.hbs index ce0dd4e355..2d96b2a38e 100644 --- a/app/assets/javascripts/admin/templates/logs/screened-emails.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened-emails.hbs @@ -1,7 +1,7 @@

    {{i18n 'admin.logs.screened_emails.description'}}

    - +
    diff --git a/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs b/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs index 4c1654c0f2..ea7bc1da7d 100644 --- a/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs @@ -3,8 +3,8 @@
    {{text-field value=filter class="ip-address-input" placeholderKey="admin.logs.screened_ips.form.filter" autocorrect="off" autocapitalize="off"}} - {{d-button action="rollUp" title="admin.logs.screened_ips.roll_up.title" label="admin.logs.screened_ips.roll_up.text"}} - {{d-button action="exportScreenedIpList" icon="download" title="admin.export_csv.button_title.screened_ip" label="admin.export_csv.button_text"}} + {{d-button class="btn-default" action="rollUp" title="admin.logs.screened_ips.roll_up.title" label="admin.logs.screened_ips.roll_up.text"}} + {{d-button class="btn-default" action="exportScreenedIpList" icon="download" title="admin.export_csv.button_title.screened_ip" label="admin.export_csv.button_text"}}
    {{screened-ip-address-form action="recordAdded"}}
    @@ -57,15 +57,15 @@
    diff --git a/app/assets/javascripts/admin/templates/logs/screened-urls.hbs b/app/assets/javascripts/admin/templates/logs/screened-urls.hbs index 3a5a69c955..c2a526e5f7 100644 --- a/app/assets/javascripts/admin/templates/logs/screened-urls.hbs +++ b/app/assets/javascripts/admin/templates/logs/screened-urls.hbs @@ -1,7 +1,7 @@

    {{i18n 'admin.logs.screened_urls.description'}}

    - +
    {{#conditional-loading-spinner condition=loading}} diff --git a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs index 399a824d62..63ce3d41ab 100644 --- a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs +++ b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs @@ -33,7 +33,7 @@ {{i18n "admin.logs.staff_actions.filter"}} {{combo-box content=userHistoryActions value=filterActionId none="admin.logs.staff_actions.all"}} {{/if}} - {{d-button action="exportStaffActionLogs" label="admin.export_csv.button_text" icon="download"}} + {{d-button class="btn-default" action="exportStaffActionLogs" label="admin.export_csv.button_text" icon="download"}}
    diff --git a/app/assets/javascripts/admin/templates/plugins-index.hbs b/app/assets/javascripts/admin/templates/plugins-index.hbs index cc121e2940..5704ca76e8 100644 --- a/app/assets/javascripts/admin/templates/plugins-index.hbs +++ b/app/assets/javascripts/admin/templates/plugins-index.hbs @@ -49,7 +49,7 @@ diff --git a/app/assets/javascripts/admin/templates/plugins.hbs b/app/assets/javascripts/admin/templates/plugins.hbs index d37e8bcaa5..12d9c8c8f6 100644 --- a/app/assets/javascripts/admin/templates/plugins.hbs +++ b/app/assets/javascripts/admin/templates/plugins.hbs @@ -6,7 +6,7 @@ {{#if currentUser.admin}} {{d-button label="admin.plugins.change_settings" icon="gear" - class='settings-button' + class="btn-default settings-button" action="showSettings"}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/site-settings.hbs b/app/assets/javascripts/admin/templates/site-settings.hbs index 8a1b8afcf3..1d1e579d7a 100644 --- a/app/assets/javascripts/admin/templates/site-settings.hbs +++ b/app/assets/javascripts/admin/templates/site-settings.hbs @@ -3,7 +3,7 @@
    {{d-button action="toggleMenu" class="menu-toggle" icon="bars"}} {{text-field id="setting-filter" value=filter placeholderKey="type_to_filter" class="no-blur"}} - {{d-button id="clear-filter" action="clearFilter" label="admin.site_settings.clear_filter"}} + {{d-button class="btn-default" id="clear-filter" action="clearFilter" label="admin.site_settings.clear_filter"}}
    @@ -48,10 +48,10 @@
    {{#if editingName}} - {{d-button action="saveName" label="admin.user_fields.save"}} + {{d-button class="btn-default" action="saveName" label="admin.user_fields.save"}} {{i18n 'cancel'}} {{else}} - {{d-button action="toggleNameEdit" icon="pencil"}} + {{d-button class="btn-default" action="toggleNameEdit" icon="pencil"}} {{/if}}
    @@ -68,7 +68,7 @@ {{#if model.email}} {{model.email}} {{else}} - {{d-button action="checkEmail" actionParam=model icon="envelope-o" label="admin.users.check_email.text" title="admin.users.check_email.title"}} + {{d-button class="btn-default" action="checkEmail" actionParam=model icon="envelope-o" label="admin.users.check_email.text" title="admin.users.check_email.title"}} {{/if}} @@ -89,6 +89,7 @@ {{/if}} {{else}} {{d-button action="checkEmail" + class="btn-default" actionParam=model icon="envelope-o" label="admin.users.check_email.text" @@ -102,7 +103,7 @@
    {{model.bounceScore}}
    {{#if model.canResetBounceScore}} - {{d-button action="resetBounceScore" label="admin.user.reset_bounce_score.label" title="admin.user.reset_bounce_score.title"}} + {{d-button class="btn-default" action="resetBounceScore" label="admin.user.reset_bounce_score.label" title="admin.user.reset_bounce_score.title"}} {{/if}} {{model.bounceScoreExplanation}}
    @@ -114,7 +115,7 @@ {{#if associatedAccountsLoaded}} {{associatedAccounts}} {{else}} - {{d-button action="checkEmail" actionParam=model icon="envelope-o" label="admin.users.check_email.text" title="admin.users.check_email.title"}} + {{d-button class="btn-default" action="checkEmail" actionParam=model icon="envelope-o" label="admin.users.check_email.text" title="admin.users.check_email.title"}} {{/if}} @@ -139,10 +140,10 @@
    {{#if editingTitle}} - {{d-button action="saveTitle" label="admin.user_fields.save"}} + {{d-button class="btn-default" action="saveTitle" label="admin.user_fields.save"}} {{i18n 'cancel'}} {{else}} - {{d-button action="toggleTitleEdit" icon="pencil"}} + {{d-button class="btn-default" action="toggleTitleEdit" icon="pencil"}} {{/if}}
    @@ -152,7 +153,7 @@
    {{model.ip_address}}
    {{#if currentUser.staff}} - {{d-button action="refreshBrowsers" label="admin.user.refresh_browsers"}} + {{d-button class="btn-default" action="refreshBrowsers" label="admin.user.refresh_browsers"}} {{ip-lookup ip=model.ip_address userId=model.id}} {{/if}}
    @@ -191,7 +192,7 @@
    {{#if canDisableSecondFactor}} - {{d-button action="disableSecondFactor" icon="unlock-alt" label="user.second_factor.disable"}} + {{d-button class="btn-default" action="disableSecondFactor" icon="unlock-alt" label="user.second_factor.disable"}} {{/if}}
    @@ -236,7 +237,7 @@ {{i18n 'admin.user.approve_success'}} {{else}} {{#if model.can_approve}} - {{d-button action="approve" icon="check" label="admin.user.approve"}} + {{d-button class="btn-default" action="approve" icon="check" label="admin.user.approve"}} {{/if}} {{/if}} @@ -249,15 +250,15 @@
    {{#if model.active}} {{#if model.can_deactivate}} - {{d-button action="deactivate" label="admin.user.deactivate_account"}} + {{d-button class="btn-default" action="deactivate" label="admin.user.deactivate_account"}} {{i18n 'admin.user.deactivate_explanation'}} {{/if}} {{else}} {{#if model.can_send_activation_email}} - {{d-button action="sendActivationEmail" icon="envelope" label="admin.user.send_activation_email"}} + {{d-button class="btn-default" action="sendActivationEmail" icon="envelope" label="admin.user.send_activation_email"}} {{/if}} {{#if model.can_activate}} - {{d-button action="activate" icon="check" label="admin.user.activate"}} + {{d-button class="btn-default" action="activate" icon="check" label="admin.user.activate"}} {{/if}} {{/if}}
    @@ -275,15 +276,15 @@ {{#if model.api_key}}
    {{model.api_key.key}} - {{d-button action="regenerateApiKey" icon="undo" label="admin.api.regenerate"}} - {{d-button action="revokeApiKey" icon="times" label="admin.api.revoke"}} + {{d-button class="btn-default" action="regenerateApiKey" icon="undo" label="admin.api.regenerate"}} + {{d-button class="btn-default" action="revokeApiKey" icon="times" label="admin.api.revoke"}}
    {{else}}
    - {{d-button action="generateApiKey" icon="key" label="admin.api.generate"}} + {{d-button class="btn-default" action="generateApiKey" icon="key" label="admin.api.generate"}}
    {{/if}} @@ -294,10 +295,10 @@
    {{i18n-yes-no model.admin}}
    {{#if model.can_revoke_admin}} - {{d-button action="revokeAdmin" icon="shield" label="admin.user.revoke_admin"}} + {{d-button class="btn-default" action="revokeAdmin" icon="shield" label="admin.user.revoke_admin"}} {{/if}} {{#if model.can_grant_admin}} - {{d-button action="grantAdmin" icon="shield" label="admin.user.grant_admin"}} + {{d-button class="btn-default" action="grantAdmin" icon="shield" label="admin.user.grant_admin"}} {{/if}}
    @@ -307,10 +308,10 @@
    {{i18n-yes-no model.moderator}}
    {{#if model.can_revoke_moderation}} - {{d-button action="revokeModeration" icon="shield" label="admin.user.revoke_moderation"}} + {{d-button class="btn-default" action="revokeModeration" icon="shield" label="admin.user.revoke_moderation"}} {{/if}} {{#if model.can_grant_moderation}} - {{d-button action="grantModeration" icon="shield" label="admin.user.grant_moderation"}} + {{d-button class="btn-default" action="grantModeration" icon="shield" label="admin.user.grant_moderation"}} {{/if}}
    @@ -330,14 +331,14 @@ {{#if model.canLockTrustLevel}} {{#if hasLockedTrustLevel}} {{d-icon "lock" title="admin.user.trust_level_locked_tip"}} - {{d-button action="lockTrustLevel" actionParam=false label="admin.user.unlock_trust_level"}} + {{d-button class="btn-default" action="lockTrustLevel" actionParam=false label="admin.user.unlock_trust_level"}} {{else}} {{d-icon "unlock" title="admin.user.trust_level_unlocked_tip"}} - {{d-button action="lockTrustLevel" actionParam=true label="admin.user.lock_trust_level"}} + {{d-button class="btn-default" action="lockTrustLevel" actionParam=true label="admin.user.lock_trust_level"}} {{/if}} {{/if}} {{#if model.tl3Requirements}} - {{#link-to 'adminUser.tl3Requirements' model class="btn"}}{{i18n 'admin.user.trust_level_3_requirements'}}{{/link-to}} + {{#link-to 'adminUser.tl3Requirements' model class="btn btn-default"}}{{i18n 'admin.user.trust_level_3_requirements'}}{{/link-to}} {{/if}} @@ -439,6 +440,7 @@ {{#if currentUser.admin}}
    {{d-button label="admin.user.clear_penalty_history.title" + class="btn-default" icon="times" action=(action "clearPenaltyHistory")}} {{i18n "admin.user.clear_penalty_history.description"}} @@ -537,6 +539,7 @@
    {{#if model.flags_received_count}} {{d-button + class="btn-default" action=(action "showFlagsReceived") label="admin.user.show_flags_received" icon="flag" diff --git a/app/assets/javascripts/admin/templates/users-list-show.hbs b/app/assets/javascripts/admin/templates/users-list-show.hbs index b54d8214da..e0acb5fc43 100644 --- a/app/assets/javascripts/admin/templates/users-list-show.hbs +++ b/app/assets/javascripts/admin/templates/users-list-show.hbs @@ -8,7 +8,7 @@

    {{title}}

    {{#unless showEmails}} - + {{/unless}}
    diff --git a/app/assets/javascripts/admin/templates/users-list.hbs b/app/assets/javascripts/admin/templates/users-list.hbs index bddc514eff..92c5c689ad 100644 --- a/app/assets/javascripts/admin/templates/users-list.hbs +++ b/app/assets/javascripts/admin/templates/users-list.hbs @@ -14,10 +14,10 @@ {{nav-item route='groups' label='groups.index.title'}}
    {{#unless siteSettings.enable_sso}} - {{d-button action="sendInvites" title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}} + {{d-button class="btn-default" action="sendInvites" title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}} {{/unless}} {{#if currentUser.admin}} - {{d-button action="exportUsers" title="admin.export_csv.button_title.user" icon="download" label="admin.export_csv.button_text"}} + {{d-button class="btn-default" action="exportUsers" title="admin.export_csv.button_title.user" icon="download" label="admin.export_csv.button_text"}} {{/if}}
    diff --git a/app/assets/javascripts/admin/templates/web-hooks.hbs b/app/assets/javascripts/admin/templates/web-hooks.hbs index 6a5c57f7dc..a9d30ac4b9 100644 --- a/app/assets/javascripts/admin/templates/web-hooks.hbs +++ b/app/assets/javascripts/admin/templates/web-hooks.hbs @@ -2,7 +2,7 @@

    {{i18n 'admin.web_hooks.instruction'}}

    - {{#link-to 'adminWebHooks.show' 'new' tagName='button' classNames='btn'}} + {{#link-to 'adminWebHooks.show' 'new' tagName='button' classNames='btn btn-default'}} {{d-icon 'plus'}} {{i18n 'admin.web_hooks.new'}} {{/link-to}}
    diff --git a/app/assets/javascripts/discourse/components/share-button.js.es6 b/app/assets/javascripts/discourse/components/share-button.js.es6 index 888d8a0c2c..958f821ce6 100644 --- a/app/assets/javascripts/discourse/components/share-button.js.es6 +++ b/app/assets/javascripts/discourse/components/share-button.js.es6 @@ -1,7 +1,7 @@ import Button from "discourse/components/d-button"; export default Button.extend({ - classNames: ["share"], + classNames: ["btn-default", "share"], icon: "link", title: "topic.share.help", label: "topic.share.title", diff --git a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 index 651c9a2e74..8703739cdb 100644 --- a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 @@ -21,24 +21,45 @@ function addBulkButton(action, key, opts) { } // Default buttons -addBulkButton("showChangeCategory", "change_category", { icon: "pencil" }); -addBulkButton("closeTopics", "close_topics", { icon: "lock" }); -addBulkButton("archiveTopics", "archive_topics", { icon: "folder" }); -addBulkButton("showNotificationLevel", "notification_level", { - icon: "d-regular" +addBulkButton("showChangeCategory", "change_category", { + icon: "pencil", + class: "btn-default" +}); +addBulkButton("closeTopics", "close_topics", { + icon: "lock", + class: "btn-default" +}); +addBulkButton("archiveTopics", "archive_topics", { + icon: "folder", + class: "btn-default" +}); +addBulkButton("showNotificationLevel", "notification_level", { + icon: "d-regular", + class: "btn-default" +}); +addBulkButton("resetRead", "reset_read", { + icon: "backward", + class: "btn-default" }); -addBulkButton("resetRead", "reset_read", { icon: "backward" }); addBulkButton("unlistTopics", "unlist_topics", { icon: "eye-slash", + class: "btn-default", buttonVisible: topics => topics.some(t => t.visible) }); addBulkButton("relistTopics", "relist_topics", { icon: "eye", + class: "btn-default", buttonVisible: topics => topics.some(t => !t.visible) }); if (Discourse.SiteSettings.tagging_enabled) { - addBulkButton("showTagTopics", "change_tags", { icon: "tag" }); - addBulkButton("showAppendTagTopics", "append_tags", { icon: "tag" }); + addBulkButton("showTagTopics", "change_tags", { + icon: "tag", + class: "btn-default" + }); + addBulkButton("showAppendTagTopics", "append_tags", { + icon: "tag", + class: "btn-default" + }); } addBulkButton("deleteTopics", "delete", { icon: "trash", class: "btn-danger" }); diff --git a/app/assets/javascripts/discourse/templates/components/bulk-select-button.hbs b/app/assets/javascripts/discourse/templates/components/bulk-select-button.hbs index 4dad7efb0c..822c97b544 100644 --- a/app/assets/javascripts/discourse/templates/components/bulk-select-button.hbs +++ b/app/assets/javascripts/discourse/templates/components/bulk-select-button.hbs @@ -1,5 +1,5 @@ {{#if selected}}
    - {{d-button action="showBulkActions" icon="wrench"}} + {{d-button class="btn-default" action="showBulkActions" icon="wrench"}}
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/categories-topic-list.hbs b/app/assets/javascripts/discourse/templates/components/categories-topic-list.hbs index 7761050a38..1a4314acce 100644 --- a/app/assets/javascripts/discourse/templates/components/categories-topic-list.hbs +++ b/app/assets/javascripts/discourse/templates/components/categories-topic-list.hbs @@ -7,7 +7,7 @@ {{latest-topic-list-item topic=t}} {{/each}} {{else}}
    diff --git a/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs b/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs index d1ffffbdee..bb94b11c99 100644 --- a/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs +++ b/app/assets/javascripts/discourse/templates/components/desktop-notification-config.hbs @@ -1,15 +1,15 @@ {{#if isNotSupported}} - {{d-button icon="bell-slash" label="user.desktop_notifications.not_supported" disabled="true"}} + {{d-button icon="bell-slash" class="btn-default" label="user.desktop_notifications.not_supported" disabled="true"}} {{/if}} {{#if isDeniedPermission}} - {{d-button icon="bell-slash" label="user.desktop_notifications.perm_denied_btn" action="recheckPermission" disabled='true'}} + {{d-button icon="bell-slash" class="btn-default" label="user.desktop_notifications.perm_denied_btn" action="recheckPermission" disabled='true'}} {{i18n "user.desktop_notifications.perm_denied_expl"}} {{else}} {{#if isEnabled}} - {{d-button icon="bell-slash-o" label="user.desktop_notifications.disable" action="turnoff"}} + {{d-button icon="bell-slash-o" class="btn-default" label="user.desktop_notifications.disable" action="turnoff"}} {{i18n "user.desktop_notifications.currently_enabled"}} {{else}} - {{d-button icon="bell-o" label="user.desktop_notifications.enable" action="turnon"}} + {{d-button icon="bell-o" class="btn-default" label="user.desktop_notifications.enable" action="turnon"}} {{i18n "user.desktop_notifications.currently_disabled"}} {{/if}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs index ad078cda2f..dec825ae6a 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs @@ -32,7 +32,7 @@ {{/if}} {{else}} {{#unless category.is_special}} - + {{/unless}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs index 8eb03a0145..938be5c503 100644 --- a/app/assets/javascripts/discourse/templates/components/image-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/image-uploader.hbs @@ -1,6 +1,6 @@
    -
    {{group-index-toggle order=order desc=desc field='username_lower' i18nKey='username'}} + {{group-index-toggle order=order desc=desc field='added_at' i18nKey='groups.member_added'}} {{group-index-toggle order=order desc=desc field='last_posted_at' i18nKey='last_post'}} {{group-index-toggle order=order desc=desc field='last_seen_at' i18nKey='last_seen'}} @@ -44,7 +45,9 @@ {{/if}} - + diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b20d5e639a..2a191d76a7 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -216,6 +216,8 @@ class GroupsController < ApplicationController if params[:order] && %w{last_posted_at last_seen_at}.include?(params[:order]) order = "#{params[:order]} #{dir} NULLS LAST" + elsif params[:order] == 'added_at' + order = "group_users.created_at #{dir}" end users = group.users.human_users @@ -231,6 +233,8 @@ class GroupsController < ApplicationController end end + users = users.select('users.*, group_users.created_at as added_at') + members = users .order('NOT group_users.owner') .order(order) diff --git a/app/serializers/group_user_serializer.rb b/app/serializers/group_user_serializer.rb index e72dc2bb8e..c254088381 100644 --- a/app/serializers/group_user_serializer.rb +++ b/app/serializers/group_user_serializer.rb @@ -1,3 +1,7 @@ class GroupUserSerializer < BasicUserSerializer - attributes :name, :title, :last_posted_at, :last_seen_at + attributes :name, :title, :last_posted_at, :last_seen_at, :added_at + + def include_added_at + object.respond_to? :added_at + end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0b7f3f86a2..28492063f0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -413,6 +413,7 @@ en: remove_user_as_group_owner: "Revoke owner" groups: + member_added: "Added" add_members: title: "Add Members" description: "Manage the membership of this group" diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 6b5cadad79..9a21289d67 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -360,7 +360,15 @@ describe GroupsController do end it "ensures that membership can be paginated" do - 5.times { group.add(Fabricate(:user)) } + + freeze_time + + first_user = Fabricate(:user) + group.add(first_user) + + freeze_time 1.day.from_now + + 4.times { group.add(Fabricate(:user)) } usernames = group.users.map { |m| m.username }.sort get "/groups/#{group.name}/members.json", params: { limit: 3 } @@ -378,6 +386,11 @@ describe GroupsController do members = JSON.parse(response.body)["members"] expect(members.map { |m| m['username'] }).to eq(usernames[3..5]) + + get "/groups/#{group.name}/members.json", params: { order: 'added_at', desc: true } + members = JSON.parse(response.body)["members"] + + expect(members.last['added_at']).to eq(first_user.created_at.as_json) end end From ec91450aae3c73252cb718033d12c1a8e2578a36 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 31 Oct 2018 15:35:07 -0400 Subject: [PATCH 188/209] FEATURE: Track how many user flags are agreed/disagreed/ignored Display the percentage when reviewing flags. --- .../components/user-flag-percentage.js.es6 | 53 +++++++++++++++++++ .../templates/components/flag-user-lists.hbs | 1 + .../components/user-flag-percentage.hbs | 6 +++ .../stylesheets/common/admin/flagging.scss | 24 ++++++++- app/models/post_action.rb | 9 +++- app/models/report.rb | 22 ++++---- app/serializers/flagged_user_serializer.rb | 17 +++++- config/locales/client.en.yml | 15 ++++++ .../20181031165343_add_flag_stats_to_user.rb | 34 ++++++++++++ lib/flag_query.rb | 2 +- spec/models/post_action_spec.rb | 17 ++++-- spec/models/web_hook_spec.rb | 1 + spec/requests/admin/flags_controller_spec.rb | 2 + 13 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 app/assets/javascripts/admin/components/user-flag-percentage.js.es6 create mode 100644 app/assets/javascripts/admin/templates/components/user-flag-percentage.hbs create mode 100644 db/migrate/20181031165343_add_flag_stats_to_user.rb diff --git a/app/assets/javascripts/admin/components/user-flag-percentage.js.es6 b/app/assets/javascripts/admin/components/user-flag-percentage.js.es6 new file mode 100644 index 0000000000..f51ef0a378 --- /dev/null +++ b/app/assets/javascripts/admin/components/user-flag-percentage.js.es6 @@ -0,0 +1,53 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + tagName: "", + + @computed("percentage") + showPercentage(percentage) { + return percentage.total >= 3; + }, + + // We do a little logic to choose which icon to display and which text + @computed("user.flags_agreed", "user.flags_disagreed", "user.flags_ignored") + percentage(agreed, disagreed, ignored) { + let total = agreed + disagreed + ignored; + let result = { total }; + + if (total > 0) { + result.agreed = Math.round((agreed / total) * 100); + result.disagreed = Math.round((disagreed / total) * 100); + result.ignored = Math.round((ignored / total) * 100); + } + + let highest = Math.max(agreed, disagreed, ignored); + if (highest === agreed) { + result.icon = "thumbs-up"; + result.className = "agreed"; + result.label = `${result.agreed}%`; + } else if (highest === disagreed) { + result.icon = "thumbs-down"; + result.className = "disagreed"; + result.label = `${result.disagreed}%`; + } else { + result.icon = "external-link"; + result.className = "ignored"; + result.label = `${result.ignored}%`; + } + + result.title = I18n.t("admin.flags.user_percentage.summary", { + agreed: I18n.t("admin.flags.user_percentage.agreed", { + count: result.agreed + }), + disagreed: I18n.t("admin.flags.user_percentage.disagreed", { + count: result.disagreed + }), + ignored: I18n.t("admin.flags.user_percentage.disagreed", { + count: result.ignored + }), + count: total + }); + + return result; + } +}); diff --git a/app/assets/javascripts/admin/templates/components/flag-user-lists.hbs b/app/assets/javascripts/admin/templates/components/flag-user-lists.hbs index 7c793b98bc..2bf86a7bf6 100644 --- a/app/assets/javascripts/admin/templates/components/flag-user-lists.hbs +++ b/app/assets/javascripts/admin/templates/components/flag-user-lists.hbs @@ -8,6 +8,7 @@
    {{post-action-title postAction.post_action_type_id postAction.name_key}}
    + {{user-flag-percentage user=postAction.user}} {{/flag-user}} {{/each}} diff --git a/app/assets/javascripts/admin/templates/components/user-flag-percentage.hbs b/app/assets/javascripts/admin/templates/components/user-flag-percentage.hbs new file mode 100644 index 0000000000..b358f961e7 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/user-flag-percentage.hbs @@ -0,0 +1,6 @@ +{{#if showPercentage}} +
    + {{percentage.label}} + {{d-icon percentage.icon}} +
    +{{/if}} diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index 4c17bdf795..c42b85f44b 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -107,10 +107,32 @@ .flag-user-date { color: $primary-medium; } - .flag-user-avatar { margin-right: 0.5em; } + .flag-user-extra { + display: flex; + align-items: center; + + .user-flag-percentage { + display: flex; + align-items: center; + margin-left: 0.5em; + + .percentage-label { + margin-right: 0.25em; + &.agreed { + color: $success; + } + &.disagreed { + color: $danger; + } + &.ignored { + color: $primary-medium; + } + } + } + } } .flag-conversation { diff --git a/app/models/post_action.rb b/app/models/post_action.rb index 2fc39f6f2a..bf233bb4db 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -164,6 +164,9 @@ class PostAction < ActiveRecord::Base trigger_spam = true if action.post_action_type_id == PostActionType.types[:spam] end + # Update the flags_agreed user stat + UserStat.where(user_id: actions.map(&:user_id)).update_all("flags_agreed = flags_agreed + 1") + DiscourseEvent.trigger(:confirmed_spam_post, post) if trigger_spam if actions.first.present? @@ -183,8 +186,7 @@ class PostAction < ActiveRecord::Base PostActionType.notify_flag_type_ids end - actions = PostAction.where(post_id: post.id) - .where(post_action_type_id: action_type_ids) + actions = PostAction.active.where(post_id: post.id).where(post_action_type_id: action_type_ids) actions.each do |action| action.disagreed_at = Time.zone.now @@ -194,6 +196,9 @@ class PostAction < ActiveRecord::Base action.add_moderator_post_if_needed(moderator, :disagreed) end + # Update the flags_disagreed user stat + UserStat.where(user_id: actions.map(&:user_id)).update_all("flags_disagreed = flags_disagreed + 1") + # reset all cached counters cached = {} action_type_ids.each do |atid| diff --git a/app/models/report.rb b/app/models/report.rb index d645520fe0..3b2fecdf22 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -1207,18 +1207,16 @@ class Report u.username, u.uploaded_avatar_id as avatar_id, CASE WHEN u.silenced_till IS NOT NULL THEN 't' ELSE 'f' END as silenced, - SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) as disagreed_flags, - SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) as agreed_flags, - ROUND(SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric / SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric, 2) as ratio, - SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) - SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) spread, - ROUND((1-(SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric / SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric)) * - (SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) - SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)), 2) as score - FROM post_actions AS pa - INNER JOIN users AS u ON u.id = pa.user_id - WHERE pa.post_action_type_id IN (#{PostActionType.flag_types.values.join(', ')}) - AND pa.user_id <> -1 - GROUP BY u.id, u.username, u.silenced_till - HAVING SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) > SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) + us.flags_disagreed AS disagreed_flags, + us.flags_agreed AS agreed_flags, + ROUND(us.flags_agreed::numeric / us.flags_disagreed::numeric, 2) as ratio, + us.flags_disagreed - us.flags_agreed AS spread, + ROUND((1-(us.flags_agreed::numeric / us.flags_disagreed::numeric)) * + (us.flags_disagreed - us.flags_agreed)) AS score + FROM users AS u + INNER JOIN user_stats AS us ON us.user_id = u.id + WHERE u.id <> -1 + AND flags_disagreed > flags_agreed ORDER BY score DESC LIMIT 20 SQL diff --git a/app/serializers/flagged_user_serializer.rb b/app/serializers/flagged_user_serializer.rb index 9ada8a7296..d7c703e0fe 100644 --- a/app/serializers/flagged_user_serializer.rb +++ b/app/serializers/flagged_user_serializer.rb @@ -4,7 +4,10 @@ class FlaggedUserSerializer < BasicUserSerializer :post_count, :topic_count, :ip_address, - :custom_fields + :custom_fields, + :flags_agreed, + :flags_disagreed, + :flags_ignored def can_delete_all_posts scope.can_delete_all_posts?(object) @@ -18,6 +21,18 @@ class FlaggedUserSerializer < BasicUserSerializer object.ip_address.try(:to_s) end + def flags_agreed + object.user_stat.flags_agreed + end + + def flags_disagreed + object.user_stat.flags_disagreed + end + + def flags_ignored + object.user_stat.flags_ignored + end + def custom_fields fields = User.whitelisted_user_custom_fields(scope) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 28492063f0..a12320dbc6 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2931,6 +2931,21 @@ en: was_edited: "Post was edited after the first flag" previous_flags_count: "This post has already been flagged {{count}} times." show_details: "Show flag details" + + user_percentage: + summary: + one: "{{agreed}}, {{disagreed}}, {{ignored}} ({{count}} total flag)" + other: "{{agreed}}, {{disagreed}}, {{ignored}} ({{count}} total flags)" + agreed: + one: "{{count}}% agree" + other: "{{count}}% agree" + disagreed: + one: "{{count}}% disagree" + other: "{{count}}% disagree" + ignored: + one: "{{count}}% ignore" + other: "{{count}}% ignore" + details: "details" flagged_topics: diff --git a/db/migrate/20181031165343_add_flag_stats_to_user.rb b/db/migrate/20181031165343_add_flag_stats_to_user.rb new file mode 100644 index 0000000000..847aed08f1 --- /dev/null +++ b/db/migrate/20181031165343_add_flag_stats_to_user.rb @@ -0,0 +1,34 @@ +class AddFlagStatsToUser < ActiveRecord::Migration[5.2] + def up + add_column :user_stats, :flags_agreed, :integer, default: 0, null: false + add_column :user_stats, :flags_disagreed, :integer, default: 0, null: false + add_column :user_stats, :flags_ignored, :integer, default: 0, null: false + + sql = <<~SQL + UPDATE user_stats + SET flags_agreed = x.flags_agreed, + flags_disagreed = x.flags_disagreed, + flags_ignored = x.flags_ignored + FROM ( + SELECT u.id AS user_id, + SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) as flags_disagreed, + SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) as flags_agreed, + SUM(CASE WHEN pa.deferred_at IS NOT NULL THEN 1 ELSE 0 END) as flags_ignored + FROM post_actions AS pa + INNER JOIN users AS u ON u.id = pa.user_id + WHERE pa.post_action_type_id IN (#{PostActionType.notify_flag_types.values.join(', ')}) + AND pa.user_id > 0 + GROUP BY u.id + ) AS x + WHERE x.user_id = user_stats.user_id + SQL + + execute sql + end + + def down + remove_column :user_stats, :flags_agreed + remove_column :user_stats, :flags_disagreed + remove_column :user_stats, :flags_ignored + end +end diff --git a/lib/flag_query.rb b/lib/flag_query.rb index 34d523bdbc..2b2a8806ec 100644 --- a/lib/flag_query.rb +++ b/lib/flag_query.rb @@ -206,7 +206,7 @@ module FlagQuery results = PostAction .flags .active - .includes(post: [:user, :topic]) + .includes(post: [{ user: :user_stat }, :topic]) .references(:post) .where("posts.user_id > 0") .order('post_actions.created_at DESC') diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index adaa78f122..a99ecc795e 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -79,12 +79,17 @@ describe PostAction do # Acting on the flag should not post an automated status message (since a moderator already replied) expect(topic.posts.count).to eq(2) PostAction.agree_flags!(post, admin) + expect(action.user.user_stat.flags_agreed).to eq(1) + expect(action.user.user_stat.flags_disagreed).to eq(0) + topic.reload expect(topic.posts.count).to eq(2) # Clearing the flags should not post an automated status message - PostAction.act(mod, post, PostActionType.types[:notify_moderators], message: "another special message") + new_action = PostAction.act(mod, post, PostActionType.types[:notify_moderators], message: "another special message") PostAction.clear_flags!(post, admin) + expect(new_action.user.user_stat.flags_agreed).to eq(0) + expect(new_action.user.user_stat.flags_disagreed).to eq(1) topic.reload expect(topic.posts.count).to eq(2) @@ -95,6 +100,9 @@ describe PostAction do expect(topic.posts.count).to eq(1) PostAction.agree_flags!(another_post, admin) + expect(action.user.user_stat.flags_agreed).to eq(2) + expect(action.user.user_stat.flags_disagreed).to eq(0) + topic.reload expect(topic.posts.count).to eq(2) expect(topic.posts.last.post_type).to eq(Post.types[:moderator_action]) @@ -361,7 +369,7 @@ describe PostAction do # If a flag is dismissed PostAction.clear_flags!(post, admin) - expect(PostAction.flag_counts_for(post.id)).to eq([8, 0]) + expect(PostAction.flag_counts_for(post.id)).to eq([0, 8]) end end @@ -689,6 +697,7 @@ describe PostAction do SiteSetting.auto_respond_to_flag_actions = false PostAction.agree_flags!(post, admin) + expect(action.user.user_stat.flags_agreed).to eq(1) topic.reload expect(topic.posts.count).to eq(1) @@ -704,6 +713,7 @@ describe PostAction do SiteSetting.auto_respond_to_flag_actions = true PostAction.agree_flags!(post, admin) + expect(action.user.user_stat.flags_agreed).to eq(1) user_notifications = user.notifications expect(user_notifications.count).to eq(1) @@ -715,11 +725,12 @@ describe PostAction do post = Fabricate(:post) user = Fabricate(:user) action = PostAction.act(user, post, PostActionType.types[:notify_user], message: "WAT") - topic = action.reload.related_post.topic + action.reload.related_post.topic expect(user.notifications.count).to eq(0) SiteSetting.auto_respond_to_flag_actions = true PostAction.agree_flags!(post, admin) + expect(action.user.user_stat.flags_agreed).to eq(0) user_notifications = user.notifications expect(user_notifications.count).to eq(0) diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb index 4a95ea8cab..67e0f914a0 100644 --- a/spec/models/web_hook_spec.rb +++ b/spec/models/web_hook_spec.rb @@ -404,6 +404,7 @@ describe WebHook do payload = JSON.parse(job_args["payload"]) expect(payload["id"]).to eq(post_action.id) + post_action = PostAction.act(Fabricate(:user), post, PostActionType.types[:spam]) PostAction.clear_flags!(post, moderator) job_args = Jobs::EmitWebHookEvent.jobs.last["args"].first diff --git a/spec/requests/admin/flags_controller_spec.rb b/spec/requests/admin/flags_controller_spec.rb index 13ed77dfe7..325cb83099 100644 --- a/spec/requests/admin/flags_controller_spec.rb +++ b/spec/requests/admin/flags_controller_spec.rb @@ -59,6 +59,7 @@ RSpec.describe Admin::FlagsController do post_action.reload expect(post_action.agreed_by_id).to eq(admin.id) + expect(user.user_stat.reload.flags_agreed).to eq(1) post_1.reload expect(post_1.deleted_at).to eq(nil) @@ -77,6 +78,7 @@ RSpec.describe Admin::FlagsController do post_action.reload expect(post_action.agreed_by_id).to eq(admin.id) + expect(user.user_stat.reload.flags_agreed).to eq(1) agree_post = Topic.joins(:topic_allowed_users).where('topic_allowed_users.user_id = ?', user.id).order(:id).last.posts.last expect(agree_post.raw).to eq(I18n.with_locale(:en) { I18n.t('flags_dispositions.agreed') }) From 2feadcdafb5095024b1403246e6ae5e5fd017830 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 1 Nov 2018 14:47:06 -0400 Subject: [PATCH 189/209] FIX: We shouldn't include topics when mobile view is enabled This setting was set to be the opposite of what we want --- app/controllers/categories_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 9fa1c0d12e..4edc32c560 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -307,11 +307,11 @@ class CategoriesController < ApplicationController def include_topics(parent_category = nil) style = SiteSetting.desktop_category_page_style - view_context.mobile_view? || - params[:include_topics] || + !view_context.mobile_view? && + (params[:include_topics] || (parent_category && parent_category.subcategory_list_includes_topics?) || style == "categories_with_featured_topics".freeze || style == "categories_boxes_with_topics".freeze || - style == "categories_with_top_topics".freeze + style == "categories_with_top_topics".freeze) end end From f9b36820ef862e3438f30365a3f3d4ab4079f34b Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Thu, 1 Nov 2018 16:01:46 -0400 Subject: [PATCH 190/209] FIX: only extract script tags with certain types (#6553) `script` tags with custom types (e.g. `text/template`) are not executed by the browser, and should not be extracted into an external theme JavaScript --- app/models/theme_field.rb | 23 +++++++++++++++++++++-- spec/models/theme_field_spec.rb | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 84a6b9b561..01b5c4c9fc 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -123,9 +123,10 @@ COMPILED end doc.css('script').each do |node| - next if node['src'].present? + next unless inline_javascript?(node) - javascript_cache.content << "(function() { #{node.inner_html} })();" + javascript_cache.content << node.inner_html + javascript_cache.content << "\n" node.remove end @@ -253,6 +254,24 @@ COMPILED MessageBus.publish "/header-change/#{theme.id}", self.value if theme && self.name == "header" MessageBus.publish "/footer-change/#{theme.id}", self.value if theme && self.name == "footer" end + + private + + JAVASCRIPT_TYPES = %w( + text/javascript + application/javascript + application/ecmascript + ) + + def inline_javascript?(node) + if node['src'].present? + false + elsif node['type'].present? + JAVASCRIPT_TYPES.include?(node['type'].downcase) + else + true + end + end end # == Schema Information diff --git a/spec/models/theme_field_spec.rb b/spec/models/theme_field_spec.rb index b129c3143c..5052c883b6 100644 --- a/spec/models/theme_field_spec.rb +++ b/spec/models/theme_field_spec.rb @@ -37,9 +37,18 @@ describe ThemeField do + + + HTML @@ -47,8 +56,27 @@ describe ThemeField do expect(theme_field.value_baked).to include("") expect(theme_field.value_baked).to include("external-script.js") - expect(theme_field.javascript_cache.content).to include('inline discourse plugin') - expect(theme_field.javascript_cache.content).to include('inline raw script') + expect(theme_field.value_baked).to include(' + + HTML + + extracted = <<~JavaScript + var a = 10 + var b = 10 + JavaScript + + theme_field = ThemeField.create!(theme_id: 1, target_id: 0, name: "header", value: html) + + expect(theme_field.javascript_cache.content).to eq(extracted) end it "correctly extracts and generates errors for transpiled js" do From c4ca5ed50be4504679fc57a601fb7866cc6e7150 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 1 Nov 2018 17:44:44 -0400 Subject: [PATCH 191/209] FIX: Translation error --- .../javascripts/admin/components/user-flag-percentage.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/components/user-flag-percentage.js.es6 b/app/assets/javascripts/admin/components/user-flag-percentage.js.es6 index f51ef0a378..22713a1aa6 100644 --- a/app/assets/javascripts/admin/components/user-flag-percentage.js.es6 +++ b/app/assets/javascripts/admin/components/user-flag-percentage.js.es6 @@ -42,7 +42,7 @@ export default Ember.Component.extend({ disagreed: I18n.t("admin.flags.user_percentage.disagreed", { count: result.disagreed }), - ignored: I18n.t("admin.flags.user_percentage.disagreed", { + ignored: I18n.t("admin.flags.user_percentage.ignored", { count: result.ignored }), count: total From ab02b9a5d82a143d378acfeea667496804a57b13 Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Fri, 2 Nov 2018 00:16:45 +0200 Subject: [PATCH 192/209] FIX: Use 'require' for dependencies. (#6552) --- lib/discourse_ip_info.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/discourse_ip_info.rb b/lib/discourse_ip_info.rb index ad6b2a5755..e2eb43252b 100644 --- a/lib/discourse_ip_info.rb +++ b/lib/discourse_ip_info.rb @@ -1,5 +1,5 @@ -require_dependency 'maxminddb' -require_dependency 'resolv' +require 'maxminddb' +require 'resolv' class DiscourseIpInfo include Singleton From 42340583582c11d4acc3008f36ad91cbfec279c6 Mon Sep 17 00:00:00 2001 From: Joe <33972521+hnb-ku@users.noreply.github.com> Date: Fri, 2 Nov 2018 06:18:07 +0800 Subject: [PATCH 193/209] UX: don't show crawler navigation in print view (#6551) * UX: adds CSS classes to crawler navigation links * UX: hide crawler navigation in print view --- app/views/layouts/crawler.html.erb | 4 ++-- app/views/topics/show.html.erb | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/views/layouts/crawler.html.erb b/app/views/layouts/crawler.html.erb index 85495538c0..8a485633cc 100644 --- a/app/views/layouts/crawler.html.erb +++ b/app/views/layouts/crawler.html.erb @@ -28,14 +28,14 @@ <%= yield %>
    -
    <%= theme_lookup("body_tag") %> diff --git a/app/views/topics/show.html.erb b/app/views/topics/show.html.erb index f0d02c9d8d..15b72d539f 100644 --- a/app/views/topics/show.html.erb +++ b/app/views/topics/show.html.erb @@ -106,9 +106,15 @@ <% content_for :after_body do %> <%= preload_script('print-page') %> <% end %> From d84256a876a9fa4fc7bcb4b8ac8c5865f8c10701 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 2 Nov 2018 16:39:47 +1100 Subject: [PATCH 194/209] FEATURE: add Noindex to robots.txt for disallowed routes This strips pages out of indexes that should not exist see: https://meta.discourse.org/t/pages-listed-in-the-robots-txt-are-crawled-and-indexed-by-google/100309/11?u=sam --- app/views/robots_txt/index.erb | 1 + spec/requests/robots_txt_controller_spec.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/views/robots_txt/index.erb b/app/views/robots_txt/index.erb index 71ca94baa7..d86f72edd7 100644 --- a/app/views/robots_txt/index.erb +++ b/app/views/robots_txt/index.erb @@ -10,6 +10,7 @@ Crawl-delay: <%= agent[:delay] %> <%- end -%> <% agent[:disallow].each do |path| %> Disallow: <%= path %> +Noindex: <%= path %> <% end %> diff --git a/spec/requests/robots_txt_controller_spec.rb b/spec/requests/robots_txt_controller_spec.rb index c5627c0f19..34f46e0131 100644 --- a/spec/requests/robots_txt_controller_spec.rb +++ b/spec/requests/robots_txt_controller_spec.rb @@ -18,6 +18,7 @@ RSpec.describe RobotsTxtController do Discourse.stubs(:base_uri).returns('/forum') get '/robots.txt' expect(response.body).to include("\nDisallow: /forum/admin") + expect(response.body).to include("\nNoindex: /forum/admin") end end From 4e0f033faed7758aefd3216be9429247f2826cd7 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 2 Nov 2018 11:08:00 +0100 Subject: [PATCH 195/209] FEATURE: adds ignored flags to most_disagreed_flags report (#6554) --- app/models/report.rb | 9 +++++++-- config/locales/server.en.yml | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/models/report.rb b/app/models/report.rb index 3b2fecdf22..caa6724df2 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -1195,6 +1195,11 @@ class Report property: :agreed_flags, title: I18n.t("reports.most_disagreed_flaggers.labels.agreed_flags") }, + { + type: :number, + property: :ignored_flags, + title: I18n.t("reports.most_disagreed_flaggers.labels.ignored_flags") + }, { type: :number, property: :score, @@ -1209,8 +1214,7 @@ class Report CASE WHEN u.silenced_till IS NOT NULL THEN 't' ELSE 'f' END as silenced, us.flags_disagreed AS disagreed_flags, us.flags_agreed AS agreed_flags, - ROUND(us.flags_agreed::numeric / us.flags_disagreed::numeric, 2) as ratio, - us.flags_disagreed - us.flags_agreed AS spread, + us.flags_ignored AS ignored_flags, ROUND((1-(us.flags_agreed::numeric / us.flags_disagreed::numeric)) * (us.flags_disagreed - us.flags_agreed)) AS score FROM users AS u @@ -1227,6 +1231,7 @@ class Report flagger[:username] = row.username flagger[:avatar_template] = User.avatar_template(row.username, row.avatar_id) flagger[:disagreed_flags] = row.disagreed_flags + flagger[:ignored_flags] = row.ignored_flags flagger[:agreed_flags] = row.agreed_flags flagger[:score] = row.score diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 409cac0077..bc5cb03021 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -904,6 +904,7 @@ en: user: User agreed_flags: Agreed flags disagreed_flags: Disagreed flags + ignored_flags: Ignored flags score: Score moderators_activity: title: "Moderators activity" From 5cd055fd306389eb7014b0c7c82821b0b42a7750 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 2 Nov 2018 13:09:58 +0100 Subject: [PATCH 196/209] FIX: uses more semantically correct spans in post map (#6555) --- .../discourse/widgets/topic-map.js.es6 | 29 ++++++++++++++----- .../stylesheets/common/base/topic-post.scss | 3 +- app/assets/stylesheets/mobile/topic-post.scss | 4 +-- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index d938f0c58f..8addf7d24c 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -86,7 +86,7 @@ createWidget("topic-map-summary", { const contents = []; contents.push( h("li", [ - h("h4", I18n.t("created_lowercase")), + h("span.topic-map-post-heading", I18n.t("created_lowercase")), h("div.topic-map-post.created-at", [ avatarFor("tiny", { username: attrs.createdByUsername, @@ -101,7 +101,7 @@ createWidget("topic-map-summary", { h( "li", h("a", { attributes: { href: attrs.lastPostUrl } }, [ - h("h4", I18n.t("last_reply_lowercase")), + h("span.topic-map-post-heading", I18n.t("last_reply_lowercase")), h("div.topic-map-post.last-reply", [ avatarFor("tiny", { username: attrs.lastPostUsername, @@ -116,13 +116,19 @@ createWidget("topic-map-summary", { contents.push( h("li", [ numberNode(attrs.topicReplyCount), - h("h4", I18n.t("replies_lowercase", { count: attrs.topicReplyCount })) + h( + "span.topic-map-post-heading", + I18n.t("replies_lowercase", { count: attrs.topicReplyCount }) + ) ]) ); contents.push( h("li.secondary", [ numberNode(attrs.topicViews, { className: attrs.topicViewsHeat }), - h("h4", I18n.t("views_lowercase", { count: attrs.topicViews })) + h( + "span.topic-map-post-heading", + I18n.t("views_lowercase", { count: attrs.topicViews }) + ) ]) ); @@ -130,7 +136,10 @@ createWidget("topic-map-summary", { contents.push( h("li.secondary", [ numberNode(attrs.participantCount), - h("h4", I18n.t("users_lowercase", { count: attrs.participantCount })) + h( + "span.topic-map-post-heading", + I18n.t("users_lowercase", { count: attrs.participantCount }) + ) ]) ); } @@ -139,7 +148,10 @@ createWidget("topic-map-summary", { contents.push( h("li.secondary", [ numberNode(attrs.topicLikeCount), - h("h4", I18n.t("likes_lowercase", { count: attrs.topicLikeCount })) + h( + "span.topic-map-post-heading", + I18n.t("likes_lowercase", { count: attrs.topicLikeCount }) + ) ]) ); } @@ -148,7 +160,10 @@ createWidget("topic-map-summary", { contents.push( h("li.secondary", [ numberNode(attrs.topicLinkLength), - h("h4", I18n.t("links_lowercase", { count: attrs.topicLinkLength })) + h( + "span.topic-map-post-heading", + I18n.t("links_lowercase", { count: attrs.topicLinkLength }) + ) ]) ); } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 2fb3e97c13..3d155b81a2 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -302,7 +302,8 @@ aside.quote { font-size: $font-0; } - h4 { + .topic-map-post-heading { + display: block; margin: 1px 0 2px 0; color: dark-light-choose($primary-medium, $secondary-medium); font-weight: normal; diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 9f3414436f..ff10ad982c 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -164,10 +164,10 @@ a.reply-to-tab { } .topic-map { - margin: 10px 0; - h4 { + .topic-map-post-heading { line-height: $line-height-medium; } + .user { float: left; margin-right: 10px; From 8067f8a32cc8b77540d0f4671a54a662ba609071 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 2 Nov 2018 14:42:52 +0100 Subject: [PATCH 197/209] FIX: disables dates filtering on most_disagreed_flags report (#6556) --- app/models/report.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/report.rb b/app/models/report.rb index caa6724df2..da941bb594 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -1175,6 +1175,8 @@ class Report report.modes = [:table] + report.dates_filtering = false + report.labels = [ { type: :user, From 4417faa7e52b21a8df73a11396e9a79f0100db41 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 2 Nov 2018 15:07:22 +0100 Subject: [PATCH 198/209] Revert "FIX: uses more semantically correct spans in post map (#6555)" This reverts commit 5cd055fd306389eb7014b0c7c82821b0b42a7750. --- .../discourse/widgets/topic-map.js.es6 | 29 +++++-------------- .../stylesheets/common/base/topic-post.scss | 3 +- app/assets/stylesheets/mobile/topic-post.scss | 4 +-- 3 files changed, 10 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index 8addf7d24c..d938f0c58f 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -86,7 +86,7 @@ createWidget("topic-map-summary", { const contents = []; contents.push( h("li", [ - h("span.topic-map-post-heading", I18n.t("created_lowercase")), + h("h4", I18n.t("created_lowercase")), h("div.topic-map-post.created-at", [ avatarFor("tiny", { username: attrs.createdByUsername, @@ -101,7 +101,7 @@ createWidget("topic-map-summary", { h( "li", h("a", { attributes: { href: attrs.lastPostUrl } }, [ - h("span.topic-map-post-heading", I18n.t("last_reply_lowercase")), + h("h4", I18n.t("last_reply_lowercase")), h("div.topic-map-post.last-reply", [ avatarFor("tiny", { username: attrs.lastPostUsername, @@ -116,19 +116,13 @@ createWidget("topic-map-summary", { contents.push( h("li", [ numberNode(attrs.topicReplyCount), - h( - "span.topic-map-post-heading", - I18n.t("replies_lowercase", { count: attrs.topicReplyCount }) - ) + h("h4", I18n.t("replies_lowercase", { count: attrs.topicReplyCount })) ]) ); contents.push( h("li.secondary", [ numberNode(attrs.topicViews, { className: attrs.topicViewsHeat }), - h( - "span.topic-map-post-heading", - I18n.t("views_lowercase", { count: attrs.topicViews }) - ) + h("h4", I18n.t("views_lowercase", { count: attrs.topicViews })) ]) ); @@ -136,10 +130,7 @@ createWidget("topic-map-summary", { contents.push( h("li.secondary", [ numberNode(attrs.participantCount), - h( - "span.topic-map-post-heading", - I18n.t("users_lowercase", { count: attrs.participantCount }) - ) + h("h4", I18n.t("users_lowercase", { count: attrs.participantCount })) ]) ); } @@ -148,10 +139,7 @@ createWidget("topic-map-summary", { contents.push( h("li.secondary", [ numberNode(attrs.topicLikeCount), - h( - "span.topic-map-post-heading", - I18n.t("likes_lowercase", { count: attrs.topicLikeCount }) - ) + h("h4", I18n.t("likes_lowercase", { count: attrs.topicLikeCount })) ]) ); } @@ -160,10 +148,7 @@ createWidget("topic-map-summary", { contents.push( h("li.secondary", [ numberNode(attrs.topicLinkLength), - h( - "span.topic-map-post-heading", - I18n.t("links_lowercase", { count: attrs.topicLinkLength }) - ) + h("h4", I18n.t("links_lowercase", { count: attrs.topicLinkLength })) ]) ); } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 3d155b81a2..2fb3e97c13 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -302,8 +302,7 @@ aside.quote { font-size: $font-0; } - .topic-map-post-heading { - display: block; + h4 { margin: 1px 0 2px 0; color: dark-light-choose($primary-medium, $secondary-medium); font-weight: normal; diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index ff10ad982c..9f3414436f 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -164,10 +164,10 @@ a.reply-to-tab { } .topic-map { - .topic-map-post-heading { + margin: 10px 0; + h4 { line-height: $line-height-medium; } - .user { float: left; margin-right: 10px; From 931c3d165b927d9cecc2e91cfbb94b70886286bc Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Nov 2018 10:29:44 -0400 Subject: [PATCH 199/209] Revert "FIX: We shouldn't include topics when mobile view is enabled" This reverts commit 2feadcdafb5095024b1403246e6ae5e5fd017830. --- app/controllers/categories_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 4edc32c560..9fa1c0d12e 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -307,11 +307,11 @@ class CategoriesController < ApplicationController def include_topics(parent_category = nil) style = SiteSetting.desktop_category_page_style - !view_context.mobile_view? && - (params[:include_topics] || + view_context.mobile_view? || + params[:include_topics] || (parent_category && parent_category.subcategory_list_includes_topics?) || style == "categories_with_featured_topics".freeze || style == "categories_boxes_with_topics".freeze || - style == "categories_with_top_topics".freeze) + style == "categories_with_top_topics".freeze end end From 5194313133c7b1bc1a8451c12ec67114b323216e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Nov 2018 10:58:28 -0400 Subject: [PATCH 200/209] Revert "Add base_url to config locales (#6510)" This reverts commit 8a443e051b447646136529f6589e3d553145f811. --- config/locales/server.en.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index bc5cb03021..189919bbfa 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -772,8 +772,8 @@ en: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Inappropriate' - description: 'This post contains content that a reasonable person would consider offensive, abusive, or a violation of our community guidelines.' - short_description: 'A violation of our community guidelines' + description: 'This post contains content that a reasonable person would consider offensive, abusive, or a violation of our community guidelines.' + short_description: 'A violation of our community guidelines' long_form: 'flagged this as inappropriate' notify_user: title: 'Send @{{username}} a message' @@ -824,12 +824,12 @@ en: short_description: 'This is an advertisement' inappropriate: title: 'Inappropriate' - description: 'This topic contains content that a reasonable person would consider offensive, abusive, or a violation of our community guidelines.' + description: 'This topic contains content that a reasonable person would consider offensive, abusive, or a violation of our community guidelines.' long_form: 'flagged this as inappropriate' - short_description: 'A violation of our community guidelines' + short_description: 'A violation of our community guidelines' notify_moderators: title: "Something Else" - description: 'This topic requires general staff attention based on the guidelines, TOS, or for another reason not listed above.' + description: 'This topic requires general staff attention based on the guidelines, TOS, or for another reason not listed above.' long_form: 'flagged this for moderator attention' short_description: 'Requires staff attention for another reason' email_title: 'The topic "%{title}" requires moderator attention' @@ -1137,14 +1137,14 @@ en: sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by sidekiq. Please ensure at least one sidekiq process is running. Learn about Sidekiq here.' queue_size_warning: 'The number of queued jobs is %{queue_size}, which is high. This could indicate a problem with the Sidekiq process(es), or you may need to add more Sidekiq workers.' memory_warning: 'Your server is running with less than 1 GB of total memory. At least 1 GB of memory is recommended.' - google_oauth2_config_warning: 'The server is configured to allow signup and log in with Google OAuth2 (enable_google_oauth2_logins), but the client id and client secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - facebook_config_warning: 'The server is configured to allow signup and log in with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - s3_config_warning: 'The server is configured to upload files to s3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_upload_bucket. Go to the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.' - s3_backup_config_warning: 'The server is configured to upload backups to s3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_backup_bucket. Go to the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.' + google_oauth2_config_warning: 'The server is configured to allow signup and log in with Google OAuth2 (enable_google_oauth2_logins), but the client id and client secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' + facebook_config_warning: 'The server is configured to allow signup and log in with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' + twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' + github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' + s3_config_warning: 'The server is configured to upload files to s3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_upload_bucket. Go to the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.' + s3_backup_config_warning: 'The server is configured to upload backups to s3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_backup_bucket. Go to the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.' image_magick_warning: 'The server is configured to create thumbnails of large images, but ImageMagick is not installed. Install ImageMagick using your favorite package manager or download the latest release.' - failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your app.yml and ensure that the mail server settings are correct. See the failed jobs in Sidekiq.' + failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your app.yml and ensure that the mail server settings are correct. See the failed jobs in Sidekiq.' subfolder_ends_in_slash: "Your subfolder setup is incorrect; the DISCOURSE_RELATIVE_URL_ROOT ends in a slash." email_polling_errored_recently: one: "Email polling has generated an error in the past 24 hours. Look at the logs for more details." @@ -3280,7 +3280,7 @@ en: terms_of_service: title: "Terms of Service" - signup_form_message: 'I have read and accept the Terms of Service.' + signup_form_message: 'I have read and accept the Terms of Service.' deleted: 'deleted' From 939d5ede91d3d6b4fb01b0bf940f9ba173032180 Mon Sep 17 00:00:00 2001 From: scossar Date: Fri, 2 Nov 2018 11:52:49 -0700 Subject: [PATCH 201/209] Fix sso overrides avatar description --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 189919bbfa..86a49a0e15 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1350,7 +1350,7 @@ en: sso_overrides_email: "Overrides local email with external site email from SSO payload on every login, and prevent local changes. (WARNING: discrepancies can occur due to normalization of local emails)" sso_overrides_username: "Overrides local username with external site username from SSO payload on every login, and prevent local changes. (WARNING: discrepancies can occur due to differences in username length/requirements)" sso_overrides_name: "Overrides local full name with external site full name from SSO payload on every login, and prevent local changes." - sso_overrides_avatar: "Overrides user avatar with external site avatar from SSO payload. If enabled, disabling allow_uploaded_avatars is highly recommended" + sso_overrides_avatar: "Overrides user avatar with external site avatar from SSO payload. If enabled, users will not be allowed to upload avatars on Discourse." sso_overrides_profile_background: "Overrides user profile background with external site avatar from SSO payload." sso_overrides_card_background: "Overrides user card background with external site avatar from SSO payload." sso_not_approved_url: "Redirect unapproved SSO accounts to this URL" From 48501b0d45f3144fc7063e16f508e29b22ef6ee0 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sat, 3 Nov 2018 15:36:29 -0700 Subject: [PATCH 202/209] minor wizard copyedit --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 86a49a0e15..6de00ac1d9 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -4117,7 +4117,7 @@ en: finished: title: "Your Discourse is Ready!" description: | -

    If you ever feel like changing these settings, visit your admin section; find it next to the wrench icon in the site menu.

    +

    If you ever feel like changing these settings, re-run this wizard any time, or visit your admin section; find it next to the wrench icon in the site menu.

    Have fun, and good luck building your new community!

    search_logs: From 1ac3e5473a5c7193138fedd507e5d2ae63d07d21 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Thu, 1 Nov 2018 08:41:13 +0100 Subject: [PATCH 203/209] FIX: don't strip eml attachments from received emails --- lib/email/receiver.rb | 2 +- spec/components/email/receiver_spec.rb | 19 ++++++++++--- spec/fixtures/emails/attached_eml_file.eml | 31 ++++++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 spec/fixtures/emails/attached_eml_file.eml diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index a601b27eb7..c45eddd735 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -882,7 +882,7 @@ module Email def attachments # strip blacklisted attachments (mostly signatures) @attachments ||= begin - attachments = @mail.attachments.select { |attachment| is_whitelisted_attachment?(attachment) } + attachments = @mail.parts.select { |part| part.attachment? && is_whitelisted_attachment?(part) } attachments << @mail if @mail.attachment? && is_whitelisted_attachment?(@mail) attachments end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index a13a825108..a3503301ff 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -470,31 +470,43 @@ describe Email::Receiver do it "supports attachments" do SiteSetting.authorized_extensions = "txt" expect { process(:attached_txt_file) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to match(/text\.txt/) + expect(topic.posts.last.raw).to match(/]*>text\.txt<\/a>/) + expect(topic.posts.last.uploads.length).to eq 1 + end + + it "supports eml attachments" do + SiteSetting.authorized_extensions = "eml" + expect { process(:attached_eml_file) }.to change { topic.posts.count } + expect(topic.posts.last.raw).to match(/]*>sample\.eml<\/a>/) + expect(topic.posts.last.uploads.length).to eq 1 end context "when attachment is rejected" do it "sends out the warning email" do expect { process(:attached_txt_file) }.to change { EmailLog.count }.by(1) expect(EmailLog.last.email_type).to eq("email_reject_attachment") + expect(topic.posts.last.uploads.length).to eq 0 end it "doesn't send out the warning email if sender is staged user" do user.update_columns(staged: true) expect { process(:attached_txt_file) }.not_to change { EmailLog.count } + expect(topic.posts.last.uploads.length).to eq 0 end it "creates the post with attachment missing message" do missing_attachment_regex = Regexp.escape(I18n.t('emails.incoming.missing_attachment', filename: "text.txt")) expect { process(:attached_txt_file) }.to change { topic.posts.count } expect(topic.posts.last.raw).to match(/#{missing_attachment_regex}/) + expect(topic.posts.last.uploads.length).to eq 0 end end it "supports emails with just an attachment" do SiteSetting.authorized_extensions = "pdf" expect { process(:attached_pdf_file) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to match(/discourse\.pdf/) + expect(topic.posts.last.raw).to match(/]*>discourse\.pdf<\/a>/) + expect(topic.posts.last.uploads.length).to eq 1 end it "supports liking via email" do @@ -638,7 +650,8 @@ describe Email::Receiver do it "supports any kind of attachments when 'allow_all_attachments_for_group_messages' is enabled" do SiteSetting.allow_all_attachments_for_group_messages = true expect { process(:attached_rb_file) }.to change(Topic, :count) - expect(Post.last.raw).to match(/discourse\.rb/) + expect(Post.last.raw).to match(/]*>discourse\.rb<\/a>/) + expect(Post.last.uploads.length).to eq 1 end it "enables user's email_private_messages setting when user emails new topic to group" do diff --git a/spec/fixtures/emails/attached_eml_file.eml b/spec/fixtures/emails/attached_eml_file.eml new file mode 100644 index 0000000000..6ad4c8ff10 --- /dev/null +++ b/spec/fixtures/emails/attached_eml_file.eml @@ -0,0 +1,31 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Sat, 30 Jan 2016 01:10:11 +0100 +Message-ID: <38@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="--==_mimepart_56abff5d49749_ddf83fca6d033a28548ad"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + + +----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Please find the eml file attached. + +----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad +Content-Type: message/rfc822; name="sample.eml" +Content-Disposition: attachment; filename="sample.eml" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_jnxo58s31 +Content-ID: + +RGF0ZTogU3VuLCAxIEFwciAyMDEyIDE0OjI1OjI1IC0wNjAwDQpGcm9tOiBmaWxlQGZ5aWNlbnRl +ci5jb20NClN1YmplY3Q6IFdlbGNvbWUNClRvOiBzb21lb25lQHNvbWV3aGVyZS5jb20NCg0KRGVh +ciBGcmllbmQsDQoNCldlbGNvbWUgdG8gZmlsZS5meWljZW50ZXIuY29tIQ0KDQpTaW5jZXJlbHks +DQpGWUljZW50ZXIuY29tIFRlYW0NCg== +----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad-- From 4d74688b50efd5d9afb3b7649a4ec583b4f27879 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 5 Nov 2018 09:45:32 +0100 Subject: [PATCH 204/209] UX: uses presentation role for accessibility in topic map (#6561) Co-Authored-By: mwcampbell --- .../discourse/widgets/topic-map.js.es6 | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index d938f0c58f..26cceb8772 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -86,7 +86,11 @@ createWidget("topic-map-summary", { const contents = []; contents.push( h("li", [ - h("h4", I18n.t("created_lowercase")), + h( + "h4", + { attributes: { role: "presentation" } }, + I18n.t("created_lowercase") + ), h("div.topic-map-post.created-at", [ avatarFor("tiny", { username: attrs.createdByUsername, @@ -101,7 +105,11 @@ createWidget("topic-map-summary", { h( "li", h("a", { attributes: { href: attrs.lastPostUrl } }, [ - h("h4", I18n.t("last_reply_lowercase")), + h( + "h4", + { attributes: { role: "presentation" } }, + I18n.t("last_reply_lowercase") + ), h("div.topic-map-post.last-reply", [ avatarFor("tiny", { username: attrs.lastPostUsername, @@ -116,13 +124,21 @@ createWidget("topic-map-summary", { contents.push( h("li", [ numberNode(attrs.topicReplyCount), - h("h4", I18n.t("replies_lowercase", { count: attrs.topicReplyCount })) + h( + "h4", + { attributes: { role: "presentation" } }, + I18n.t("replies_lowercase", { count: attrs.topicReplyCount }) + ) ]) ); contents.push( h("li.secondary", [ numberNode(attrs.topicViews, { className: attrs.topicViewsHeat }), - h("h4", I18n.t("views_lowercase", { count: attrs.topicViews })) + h( + "h4", + { attributes: { role: "presentation" } }, + I18n.t("views_lowercase", { count: attrs.topicViews }) + ) ]) ); @@ -130,7 +146,11 @@ createWidget("topic-map-summary", { contents.push( h("li.secondary", [ numberNode(attrs.participantCount), - h("h4", I18n.t("users_lowercase", { count: attrs.participantCount })) + h( + "h4", + { attributes: { role: "presentation" } }, + I18n.t("users_lowercase", { count: attrs.participantCount }) + ) ]) ); } @@ -139,7 +159,11 @@ createWidget("topic-map-summary", { contents.push( h("li.secondary", [ numberNode(attrs.topicLikeCount), - h("h4", I18n.t("likes_lowercase", { count: attrs.topicLikeCount })) + h( + "h4", + { attributes: { role: "presentation" } }, + I18n.t("likes_lowercase", { count: attrs.topicLikeCount }) + ) ]) ); } @@ -148,7 +172,11 @@ createWidget("topic-map-summary", { contents.push( h("li.secondary", [ numberNode(attrs.topicLinkLength), - h("h4", I18n.t("links_lowercase", { count: attrs.topicLinkLength })) + h( + "h4", + { attributes: { role: "presentation" } }, + I18n.t("links_lowercase", { count: attrs.topicLinkLength }) + ) ]) ); } From cc9869a61b3c6e398bd59ee600f3a0a8147cbed5 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 5 Nov 2018 12:02:18 +0100 Subject: [PATCH 205/209] FIX: topic-map spec with VDOM and i18n plural (#6564) It appears that in vdom nodes, pluralized i18n strings are not compiled into a string before widget is compiled and result in an error as VDOM is expecting a string and not an object. --- .../discourse/widgets/topic-map.js.es6 | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index 26cceb8772..99d3d3cee7 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -88,7 +88,9 @@ createWidget("topic-map-summary", { h("li", [ h( "h4", - { attributes: { role: "presentation" } }, + { + attributes: { role: "presentation" } + }, I18n.t("created_lowercase") ), h("div.topic-map-post.created-at", [ @@ -107,7 +109,9 @@ createWidget("topic-map-summary", { h("a", { attributes: { href: attrs.lastPostUrl } }, [ h( "h4", - { attributes: { role: "presentation" } }, + { + attributes: { role: "presentation" } + }, I18n.t("last_reply_lowercase") ), h("div.topic-map-post.last-reply", [ @@ -126,8 +130,12 @@ createWidget("topic-map-summary", { numberNode(attrs.topicReplyCount), h( "h4", - { attributes: { role: "presentation" } }, - I18n.t("replies_lowercase", { count: attrs.topicReplyCount }) + { + attributes: { role: "presentation" } + }, + I18n.t("replies_lowercase", { + count: attrs.topicReplyCount + }).toString() ) ]) ); @@ -136,8 +144,10 @@ createWidget("topic-map-summary", { numberNode(attrs.topicViews, { className: attrs.topicViewsHeat }), h( "h4", - { attributes: { role: "presentation" } }, - I18n.t("views_lowercase", { count: attrs.topicViews }) + { + attributes: { role: "presentation" } + }, + I18n.t("views_lowercase", { count: attrs.topicViews }).toString() ) ]) ); @@ -148,8 +158,12 @@ createWidget("topic-map-summary", { numberNode(attrs.participantCount), h( "h4", - { attributes: { role: "presentation" } }, - I18n.t("users_lowercase", { count: attrs.participantCount }) + { + attributes: { role: "presentation" } + }, + I18n.t("users_lowercase", { + count: attrs.participantCount + }).toString() ) ]) ); @@ -161,8 +175,12 @@ createWidget("topic-map-summary", { numberNode(attrs.topicLikeCount), h( "h4", - { attributes: { role: "presentation" } }, - I18n.t("likes_lowercase", { count: attrs.topicLikeCount }) + { + attributes: { role: "presentation" } + }, + I18n.t("likes_lowercase", { + count: attrs.topicLikeCount + }).toString() ) ]) ); @@ -174,8 +192,12 @@ createWidget("topic-map-summary", { numberNode(attrs.topicLinkLength), h( "h4", - { attributes: { role: "presentation" } }, - I18n.t("links_lowercase", { count: attrs.topicLinkLength }) + { + attributes: { role: "presentation" } + }, + I18n.t("links_lowercase", { + count: attrs.topicLinkLength + }).toString() ) ]) ); From ae9eddb002b21e514cfdd41dc11df05bd476eec6 Mon Sep 17 00:00:00 2001 From: Maja Komel Date: Sun, 4 Nov 2018 21:18:58 +0100 Subject: [PATCH 206/209] FIX: don't allow adding a value containing vertical bar char to the secret list --- .../admin/components/secret-value-list.js.es6 | 21 ++++++++++++-- .../components/secret-value-list.hbs | 2 ++ .../stylesheets/common/admin/admin_base.scss | 23 +++++++++++---- config/locales/client.en.yml | 2 ++ .../components/secret-value-list-test.js.es6 | 28 +++++++++++++++++++ 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/components/secret-value-list.js.es6 b/app/assets/javascripts/admin/components/secret-value-list.js.es6 index 939ba45c9e..26e9f93e28 100644 --- a/app/assets/javascripts/admin/components/secret-value-list.js.es6 +++ b/app/assets/javascripts/admin/components/secret-value-list.js.es6 @@ -2,11 +2,10 @@ import { on } from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ classNameBindings: [":value-list", ":secret-value-list"], - inputInvalidKey: Ember.computed.empty("newKey"), - inputInvalidSecret: Ember.computed.empty("newSecret"), inputDelimiter: null, collection: null, values: null, + validationMessage: null, @on("didReceiveAttrs") _setupCollection() { @@ -20,15 +19,18 @@ export default Ember.Component.extend({ actions: { changeKey(index, newValue) { + if (this._checkInvalidInput(newValue)) return; this._replaceValue(index, newValue, "key"); }, changeSecret(index, newValue) { + if (this._checkInvalidInput(newValue)) return; this._replaceValue(index, newValue, "secret"); }, addValue() { - if (this.get("inputInvalidKey") || this.get("inputInvalidSecret")) return; + if (this._checkInvalidInput([this.get("newKey"), this.get("newSecret")])) + return; this._addValue(this.get("newKey"), this.get("newSecret")); this.setProperties({ newKey: "", newSecret: "" }); }, @@ -38,6 +40,19 @@ export default Ember.Component.extend({ } }, + _checkInvalidInput(inputs) { + this.set("validationMessage", null); + for (let input of inputs) { + if (Ember.isEmpty(input) || input.includes("|")) { + this.set( + "validationMessage", + I18n.t("admin.site_settings.secret_list.invalid_input") + ); + return true; + } + } + }, + _addValue(value, secret) { this.get("collection").addObject({ key: value, secret: secret }); this._saveValues(); diff --git a/app/assets/javascripts/admin/templates/components/secret-value-list.hbs b/app/assets/javascripts/admin/templates/components/secret-value-list.hbs index 6bdc972ddc..a2f44f1553 100644 --- a/app/assets/javascripts/admin/templates/components/secret-value-list.hbs +++ b/app/assets/javascripts/admin/templates/components/secret-value-list.hbs @@ -20,3 +20,5 @@ icon="plus" class="add-value-btn btn-small"}} + +{{setting-validation-message message=validationMessage}} diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 24721a26be..e8df33fb5f 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -935,12 +935,23 @@ table#user-badges { margin-left: 0.25em; margin-top: 0.125em; } - &:last-of-type { - .new-value-input { - &:first-of-type { - margin-left: 0.25em; - } - } + .new-value-input { + margin-left: 0.25em; + } + } +} + +.mobile-view .secret-value-list { + .add-value-btn { + margin-bottom: 9px; + } + .value { + .value-input:last-of-type { + margin-left: 2.35em; + } + .new-value-input:first-of-type { + margin-right: 2.15em; + margin-left: 0.25em; } } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a12320dbc6..b7829bb98a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3948,6 +3948,8 @@ en: tags: "Tags" search: "Search" groups: "Groups" + secret_list: + invalid_input: "Input fields cannot be empty or contain vertical bar character." badges: title: Badges diff --git a/test/javascripts/components/secret-value-list-test.js.es6 b/test/javascripts/components/secret-value-list-test.js.es6 index 1f602cbdbd..a55d289306 100644 --- a/test/javascripts/components/secret-value-list-test.js.es6 +++ b/test/javascripts/components/secret-value-list-test.js.es6 @@ -41,6 +41,34 @@ componentTest("adding a value", { } }); +componentTest("adding an invalid value", { + template: "{{secret-value-list values=values}}", + + async test(assert) { + await fillIn(".new-value-input.key", "someString"); + await fillIn(".new-value-input.secret", "keyWithAPipe|Hidden"); + await click(".add-value-btn"); + + assert.ok( + find(".values .value").length === 0, + "it doesn't add the value to the list of values" + ); + + assert.deepEqual( + this.get("values"), + undefined, + "it doesn't add the value to the list of values" + ); + + assert.ok( + find(".validation-error") + .html() + .indexOf(I18n.t("admin.site_settings.secret_list.invalid_input")) > -1, + "it shows validation error" + ); + } +}); + componentTest("removing a value", { template: "{{secret-value-list values=values}}", From a84b6b6b0c3dbb4e3b3e4325e4b7bc0942f9f3de Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 2 Nov 2018 23:49:00 +0000 Subject: [PATCH 207/209] SECURITY: Add CSRF protections to OpenID callback --- lib/auth/open_id_authenticator.rb | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/auth/open_id_authenticator.rb b/lib/auth/open_id_authenticator.rb index 849ca6977a..b85930b79d 100644 --- a/lib/auth/open_id_authenticator.rb +++ b/lib/auth/open_id_authenticator.rb @@ -82,12 +82,25 @@ class Auth::OpenIdAuthenticator < Auth::Authenticator def register_middleware(omniauth) omniauth.provider :open_id, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:store] = OpenID::Store::Redis.new($redis) - }, - name: name, - identifier: identifier, - require: "omniauth-openid" + setup: lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:store] = OpenID::Store::Redis.new($redis) + + # Add CSRF protection in addition to OpenID Specification + def strategy.query_string + session["omniauth.state"] = state = SecureRandom.hex(24) + "?state=#{state}" + end + + def strategy.callback_phase + stored_state = session.delete("omniauth.state") + provided_state = request.params["state"] + return fail!(:invalid_credentials) unless provided_state == stored_state + super + end + }, + name: name, + identifier: identifier, + require: "omniauth-openid" end end From d963f96fa4f36879c1a19ac2d9ba54bc7ae9953d Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 5 Nov 2018 10:58:41 +0000 Subject: [PATCH 208/209] Update translations --- config/locales/client.ar.yml | 4 - config/locales/client.bg.yml | 2 - config/locales/client.bs_BA.yml | 10 - config/locales/client.ca.yml | 3 - config/locales/client.cs.yml | 177 ++++++++- config/locales/client.da.yml | 3 - config/locales/client.de.yml | 31 +- config/locales/client.el.yml | 5 - config/locales/client.es.yml | 101 ++++- config/locales/client.et.yml | 193 +++++++++- config/locales/client.fa_IR.yml | 4 - config/locales/client.fi.yml | 63 +++- config/locales/client.fr.yml | 11 - config/locales/client.gl.yml | 1 - config/locales/client.he.yml | 3 - config/locales/client.hu.yml | 7 - config/locales/client.id.yml | 5 +- config/locales/client.it.yml | 42 ++- config/locales/client.ko.yml | 240 ++++++++++-- config/locales/client.lt.yml | 3 - config/locales/client.lv.yml | 3 - config/locales/client.nb_NO.yml | 10 - config/locales/client.nl.yml | 4 - config/locales/client.pl_PL.yml | 8 - config/locales/client.pt.yml | 4 - config/locales/client.pt_BR.yml | 10 - config/locales/client.ro.yml | 5 - config/locales/client.ru.yml | 4 - config/locales/client.sk.yml | 7 - config/locales/client.sl.yml | 30 +- config/locales/client.sq.yml | 3 - config/locales/client.sv.yml | 3 - config/locales/client.sw.yml | 10 - config/locales/client.th.yml | 1 - config/locales/client.tr_TR.yml | 42 ++- config/locales/client.ur.yml | 9 - config/locales/client.vi.yml | 2 - config/locales/client.zh_CN.yml | 353 ++++++++++++++++-- config/locales/client.zh_TW.yml | 62 +-- config/locales/server.ar.yml | 13 - config/locales/server.bg.yml | 10 - config/locales/server.bs_BA.yml | 9 - config/locales/server.ca.yml | 15 - config/locales/server.cs.yml | 5 - config/locales/server.da.yml | 9 - config/locales/server.de.yml | 295 +++++++++++++-- config/locales/server.el.yml | 16 - config/locales/server.es.yml | 91 +++-- config/locales/server.et.yml | 111 +++++- config/locales/server.fa_IR.yml | 16 - config/locales/server.fi.yml | 44 +-- config/locales/server.fr.yml | 25 -- config/locales/server.he.yml | 16 - config/locales/server.it.yml | 16 - config/locales/server.ko.yml | 51 ++- config/locales/server.nb_NO.yml | 11 - config/locales/server.nl.yml | 16 - config/locales/server.pl_PL.yml | 16 - config/locales/server.pt.yml | 16 - config/locales/server.pt_BR.yml | 25 -- config/locales/server.ro.yml | 15 - config/locales/server.ru.yml | 13 - config/locales/server.sk.yml | 12 - config/locales/server.sl.yml | 5 +- config/locales/server.sq.yml | 10 - config/locales/server.sv.yml | 12 - config/locales/server.sw.yml | 20 - config/locales/server.te.yml | 3 - config/locales/server.tr_TR.yml | 12 - config/locales/server.uk.yml | 4 - config/locales/server.ur.yml | 24 -- config/locales/server.vi.yml | 12 - config/locales/server.zh_CN.yml | 62 +-- config/locales/server.zh_TW.yml | 15 - .../config/locales/client.ko.yml | 3 + .../config/locales/client.es.yml | 4 + .../config/locales/client.ko.yml | 10 +- .../config/locales/client.zh_CN.yml | 11 + .../config/locales/server.zh_CN.yml | 6 +- .../config/locales/server.es.yml | 28 ++ .../config/locales/server.it.yml | 3 + .../config/locales/server.sv.yml | 12 +- .../config/locales/server.zh_CN.yml | 28 ++ plugins/poll/config/locales/client.zh_CN.yml | 5 + plugins/poll/config/locales/server.zh_CN.yml | 3 +- 85 files changed, 1786 insertions(+), 850 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 6a2115124b..e822d192c3 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -581,7 +581,6 @@ ar: topics_entered: " مواضيع فُتحت" post_count: "# المنشورات" confirm_delete_other_accounts: "أمتأكد من حذف هذه الحسابات؟" - powered_by: "مقدم من قبل ipinfo.io" user_fields: none: "(إختر خيار )" user: @@ -662,7 +661,6 @@ ar: watched_first_post_tags: "مراقبة أول منشور" watched_first_post_tags_instructions: "سيصلك إشعار بأول منشور في كل موضوع يستخدم هذة الأوسمة." muted_categories: "مكتوم" - muted_categories_instructions: "لن يتم إشعارك بأي جديد عن الموضوعات الجديدة في هذا القسم، ولن تظهر موضوعات هذا القسم في قائمة الموضوعات المنشورة مؤخراً." no_category_access: "كمشرف لديك صلاحيات وصول محدودة للأقسام, الحفظ معطل" delete_account: "أحذف الحسابي" delete_account_confirm: "أمتأكّد من حذف حسابك للأبد؟ هذا إجراء لا عودة فيه!" @@ -811,7 +809,6 @@ ar: always: "دائما" never: "أبدا" email_digests: - title: "إن لم أزر الموقع، أرسل إلي بريدا إلكترونيا يلخص الموضوعات والردود الشائعة" every_30_minutes: "كل 30 دقيقة" every_hour: "كل ساعة" daily: "يوميا" @@ -1867,7 +1864,6 @@ ar: upload: "عذرا، حدثت مشكلة اثناء رفع الملف. من فضلك حاول مجددا." file_too_large: "عذرا، هذا الملف كبير جدا (أقصى حجم هو {{max_size_kb}}ك.بايت). ما رأيك برفع الملف على خدمة سحابية، ثم مشاركة رابط الملف؟" too_many_uploads: "عذرا، يمكنك فقط رفع ملف واحد كل مرة." - too_many_dragged_and_dropped_files: "عذرا، يمكنك فقط رفع 10 ملفات كل مرة." upload_not_authorized: "عذرا, نوع الملف الذي تحاول رفعة محذور ( الانواع المسموح بها: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "عذرا، لا يمكن للأعضاء الجدد رفع الصور." attachment_upload_not_allowed_for_new_user: "عذرا، لا يمكن للأعضاء الجدد رفع المرفقات." diff --git a/config/locales/client.bg.yml b/config/locales/client.bg.yml index 06cdf8e762..b46c1463e1 100644 --- a/config/locales/client.bg.yml +++ b/config/locales/client.bg.yml @@ -528,7 +528,6 @@ bg: watched_first_post_tags: "Наблюдавайки Първа Публикация" watched_first_post_tags_instructions: "Ще бъдете уведомени за първата публикация във всяка нова тема с тези етикети." muted_categories: "Заглушен" - muted_categories_instructions: "Вие няма да бъдете уведомен за нови теми в тези категории, и те няма да се показват в \"последни\"." no_category_access: "Като модератор имате ограничен достъп до категориите, запазването е изключено." delete_account: "Изтрий моя профил" delete_account_confirm: "Сигурни ли сте, че искате да изтриете вашия акаунт? Акаунтът не може да бъде възстановен !" @@ -666,7 +665,6 @@ bg: always: "винаги" never: "никога" email_digests: - title: "Когато не посещавам страницата, да се изпраща имейл дайджест с новости:" every_30_minutes: "на всеки 30 минути" every_hour: "почасово" daily: "дневно" diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index e919abd46b..9e93f733f6 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -551,7 +551,6 @@ bs_BA: topics_entered: "pogledano tema" post_count: "# postova" confirm_delete_other_accounts: "Jeste sigurni da želite da izbrišete ove račune?" - powered_by: "powered by ipinfo.io" copied: "koprian" user_fields: none: "(odaberi opciju)" @@ -636,7 +635,6 @@ bs_BA: watched_first_post_tags: "Prva objava" watched_first_post_tags_instructions: "Bit će te obavješteni o prvoj objavi u svakoj novoj temi sa ovim tagovima." muted_categories: "Utišan" - muted_categories_instructions: "Nećete biti obavješteni o bilo čemu što se odnosi na nove teme u ovim kategorijama, i neće biti prikazani u listi Novije." no_category_access: "Kao moderator imate ograničen pristup kategoriji, sačuvati je isključeno." delete_account: "Izbriši moj račun" delete_account_confirm: "Da li ste sigurni da želite zauvijek izbrisati vas račun? Ova radnja je kasnije nepovratna!" @@ -696,15 +694,10 @@ bs_BA: second_factor: title: "Two Factor Authentication" disable: "Isključi two factor authentication" - enable: "Uključi two factor authentication za pojačanu sigurnost korisničkih računa" confirm_password_description: "Molimo vas da potvrdite šifru kako bi nastavili" label: "Šifra" - enable_description: | - Skeniraj ovaj QR kod u podržanoj aplikaciji (AndroidiOSWindows Phone) a potom unesite kod za ovjeru. disable_description: "Molimo da uneste kod za ovjeru autentičnosti sa vaše aplikacije" show_key_description: "Unesi manuelno" - extended_description: | - Two factor authentication (dvofaktorska ovjera autentičnosti) dodaje ekstra nivo sigurnosti vašem korisničkom računu tako što zahtjeva i jednokratni "token" (bon) pored vaše šifre. Bonovi se mogu generisati na Android, iOS, i Windows Phone 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." change_about: title: "Promjeni O meni" @@ -723,7 +716,6 @@ bs_BA: title: "Promjeni sliku" gravatar: "Gravatar, baziran na" gravatar_title: "Promjenite vaš avatar na Gravatar web stranici." - gravatar_failed: "Neuspješno učitavanje Gravatar-a. Postoji li uopšte Gravatar povezan na vašu email adresu?" refresh_gravatar_title: "Osvježi Gravatar" letter_based: "Avatar dodjeljen od sistema" uploaded_avatar: "Vaša slika" @@ -792,7 +784,6 @@ bs_BA: always: "uvijek" never: "nikad" email_digests: - title: "Kada forum ne posjećujem, pošaljite mi e-mail sa Sažetkom popularnih tema i odgovora" every_30_minutes: "svakih 30. min" every_hour: "po satu" daily: "dnevno" @@ -1879,7 +1870,6 @@ bs_BA: upload: "Sorry, there was an error uploading that file. Please try again." file_too_large: "Nažalost, taj fajl je prevelik (maksimalna veličina je {{max_size_kb}} kilobajta). Alternativno možete vaš preveliki fajl spremiti na neki od cloud sharing servisa, a potom ovdje objaviti link do tog fajla?" too_many_uploads: "Sorry, you can only upload one file at a time." - too_many_dragged_and_dropped_files: "Žao nam je, možete dizati samo 10 slika odjednom." upload_not_authorized: "Nažalost, fajl koji pokušavate da učitate nije dozvoljen za učitavanje (dozvoljene ekstenzije su: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Sorry, new users can not upload images." attachment_upload_not_allowed_for_new_user: "Sorry, new users can not upload attachments." diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index 8629ebf908..7ac13a9a1e 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -509,7 +509,6 @@ ca: watched_first_post_tags: "Mirant la primera publicació" watched_first_post_tags_instructions: "Et notificarem la primera publicació per cada nou tema a aquestes etqieuetes." muted_categories: "Silenciades" - muted_categories_instructions: "No et notificarem res sobre nous temes amb aquestes categories i no apareixeran a les darreres.." no_category_access: "Com a moderador/a tens accés limitat a la categoria. Desar està inhabilitat." delete_account: "Esborra el meu compte" delete_account_confirm: "Segur que vols esborrar el teu compte permanentment? Aquesta acció no es pot desfer!" @@ -644,7 +643,6 @@ ca: always: "sempre" never: "mai" email_digests: - title: "Quan no ho visiti, envieu-me un correu electrònic amb un resum dels temes i respostes populars " every_30_minutes: "cada 30 minuts" every_hour: "cada hora" daily: "cada dia" @@ -1470,7 +1468,6 @@ ca: upload: "Disculpa, s'ha produït una errada en carregar aquest fitxer. Si us plau, torna-ho a provar." file_too_large: "Disculpa, el fitxer és massa gran (la mida màxima és de {{max_size_kb}}kb). Per què no ho carregues a un servei de núvol, per compartir-ne l'enllaç?" too_many_uploads: "Disculpa, només pots carregar un fitxer d'un cop." - too_many_dragged_and_dropped_files: "Disculpa, només pots carregar 10 fitxers d'un cop." upload_not_authorized: "Disculpa, el fitxer que estàs provant de carregar no està autoritzat (les extensions autoritzades són: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Disculpa, les noves persones usuàries no poden carregar imatges." attachment_upload_not_allowed_for_new_user: "Disculpa, les noves persones usuàries no poden adjuntar fitxers." diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index 338fa03f04..27ae348b7f 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -204,6 +204,7 @@ cs: eu_central_1: "EU (Frankfurt)" eu_west_1: "EU (Irsko)" eu_west_2: "EU (Londýn)" + eu_west_3: "EU (Paříž)" sa_east_1: "Jižní Amerika (Sao Paulo)" us_east_1: "Východ USA (S. Virginie)" us_east_2: "Východ USA (Ohio)" @@ -236,6 +237,7 @@ cs: privacy_policy: "Ochrana soukromí" privacy: "Soukromí" tos: "Podmínky používání" + rules: "Pravidla" mobile_view: "Mobilní verze" desktop_view: "Plná verze" you: "Vy" @@ -286,7 +288,15 @@ cs: unbookmark: "Kliknutím odstraníte všechny záložky v tématu" bookmarks: created: "Záložka byla přidána." + not_bookmarked: "přidat záložku k příspěvku" remove: "Odstranit záložku" + confirm_clear: "Opravdu chcete odstranit všechny své záložky z tohoto tématu?" + drafts: + resume: "Pokračovat" + remove: "Odstranit" + new_topic: "Nový koncept tématu" + new_private_message: "Nový koncept soukromé zprávy" + topic_reply: "Koncept odpovědi" topic_count_latest: one: "Zobrazit {{count}} nové nebo upravené téma" few: "Zobrazit {{count}} nová nebo upravená témata" @@ -309,6 +319,8 @@ cs: saved: "Uloženo!" upload: "Obrázek" uploading: "Nahrávám..." + uploading_filename: "Nahrává se {{filename}}..." + clipboard: "schránka" uploaded: "Nahráno!" pasting: "Vkládám..." enable: "Zapnout" @@ -409,6 +421,9 @@ cs: delete_member_confirm: "Odstranit '%{username}' ze '%{group}' skupiny?" profile: title: Profil + interaction: + posting: Přispívání + notification: Notifikace membership: title: Členství access: Přístup @@ -416,9 +431,12 @@ cs: title: "Logy" when: "Když" action: "Akce" + acting_user: "Vystupující uživatel" target_user: "Cílový uživatel" subject: "Předmět" + details: "Podrobnosti" from: "Od" + to: "Komu" public_admission: "Povolit uživatelům volný přístup do skupiny (skupina musí být veřejně viditelná)" public_exit: "Povolit uživatelům volně opustit skupinu" empty: @@ -451,6 +469,7 @@ cs: all: "Všechny skupiny" empty: "Žádné viditelné skupiny." filter: "Filtrovat podle typu skupiny" + owner_groups: "Moje skupiny" close_groups: "Uzavřené skupiny" automatic_groups: "Automatické skupiny" automatic: "Automatické" @@ -477,6 +496,9 @@ cs: remove_member: "Odstranit člena" remove_member_description: "Odstranit %{username} z této skupiny" make_owner: "Udělej vlastníkem" + make_owner_description: "Udělat z %{username} vlastníka této skupiny" + remove_owner: "Odstranit vlastníka" + remove_owner_description: "Odstranit %{username} z role vlastníka této skupiny" owner: "Vlastník" topics: "Témata" posts: "Odpovědi" @@ -531,6 +553,7 @@ cs: "15": "Koncepty" categories: all: "všechny kategorie" + all_subcategories: "vše" no_subcategory: "žádné" category: "Kategorie" category_list: "Zobrazit seznam kategorií" @@ -556,6 +579,7 @@ cs: few: "%{count} nová témata za posledních %{unit}." many: "%{count} nových témat za posledních %{unit}." other: "%{count} nových témat za posledních %{unit}." + n_more: "Kategorie (a další %{count})..." ip_lookup: title: Vyhledávání podle IP adresy hostname: Hostname @@ -571,7 +595,7 @@ cs: topics_entered: "témat zadáno" post_count: "počet příspěvků" confirm_delete_other_accounts: "Určitě chcete smazat tyto účty?" - powered_by: "běží na ipinfo.io" + copied: "zkopírováno" user_fields: none: "(zvolit možnost)" user: @@ -589,6 +613,7 @@ cs: private_messages: "Zprávy" activity_stream: "Aktivita" preferences: "Nastavení" + profile_hidden: "Tento uživatelský profil je skrytý." expand_profile: "Rozšířit" collapse_profile: "Sbalit" bookmarks: "Záložky" @@ -598,6 +623,7 @@ cs: notifications: "Oznámení" statistics: "Statistiky" desktop_notifications: + label: "Upozornění v reálném čase" not_supported: "Tento prohlížeč nepodporuje upozornění. Omlouváme se." perm_default: "Vypnout upozornění." perm_denied_btn: "Povolení zamítnuto" @@ -605,6 +631,7 @@ cs: disable: "Vypnout upozornění" enable: "Povolit upozornění" each_browser_note: "Poznámka: Musíš změnit tuto volbu v každém prohlížeči, který používáš." + consent_prompt: "Chcete dostávat upozornění v reálném čase, když někdo odpoví na vaše příspěvky?" dismiss: 'Označit jako přečtené' dismiss_notifications: "Označit vše jako přečtené" dismiss_notifications_tooltip: "Označit všechny nepřečtené notifikace jako přečtené" @@ -636,6 +663,7 @@ cs: individual_no_echo: "Upozornit emailem na každý nový příspěvek kromě mých vlastních" many_per_day: "Upozornit emailem na každý nový příspěvek (asi {{dailyEmailEstimate}} každý den)" few_per_day: "Upozornit emailem na každý nový příspěvek (asi 2 každý den)" + warning: "Režim emailové konference povolen. Nastavení emailových notifikací neplatí." tag_settings: "Štítky" watched_tags: "Hlídané" watched_tags_instructions: "Budete automaticky hlídat všechna nová témata s těmito štítky. Na všechny nové příspěvky a témata budete upozorněni. Počet nových příspěvků se zobrazí vedle tématu." @@ -652,11 +680,11 @@ cs: watched_first_post_tags: "Hlídané první příspěvky" watched_first_post_tags_instructions: "Budete informováni o prvním novém příspěvku v každém novém tématu s těmito štítky." muted_categories: "Ztišené" - muted_categories_instructions: "Budeš přijímat upornění na nová témata v těchto kategoriích a ty se neobjeví v aktuálních." no_category_access: "Jako moderátor máte omezený přístup ke kategorii bez možnosti ukládat změny." delete_account: "Smazat můj účet" delete_account_confirm: "Jste si jisti, že chcete trvale odstranit svůj účet? Tuto akci nelze vrátit zpět!" deleted_yourself: "Váš účet byl úspěšně odstraněn." + delete_yourself_not_allowed: "Prosíme kontaktujte správce, pokud chcete smazat svůj účet." unread_message_count: "Zprávy" admin_delete: "Smazat" users: "Uživatelé" @@ -670,6 +698,7 @@ cs: revoke_access: "Odebrat přístup" undo_revoke_access: "Zrušit odebrání přístupu" api_approved: "Schváleno:" + api_last_used_at: "Poslední použití:" theme: "Motiv" home: "Výchozí domovská stránka" staged: "Staged" @@ -708,16 +737,37 @@ cs: set_password: "Nastavit heslo" choose_new: "Zvolte si nové heslo" choose: "Vyber si heslo" + second_factor_backup: + title: "Kódy dvoufaktorové autentizace" + regenerate: "Vytvořit znovu" + disable: "Vypnout" + enable: "Zapnout" + enable_long: "Povolit záložní kódy" + manage: "Spravovat záložní kódy" + copied_to_clipboard: "Zkopírováno do schránky" + copy_to_clipboard_error: "Chyba při kopírování dat do schránky" + remaining_codes: "Zbývá vám {{count}} záložní kód." + codes: + title: "Záložní kódy vytvořeny" + description: "Každý z těchto kódů může být použit jen jednou. Uložte si je někde v bezpečí, ale dostupné." second_factor: title: "Dvoufázové přihlašování" + disable: "Vypnout dvoufaktorovou autentizaci" + confirm_password_description: "Prosíme před dalším krokem potvrďte své heslo" label: "Kód" + enable_description: | + Vyfoťte tento QR kód v podporované aplikaci (AndroidiOS) a zadejte svůj autentikační kód. disable_description: "Zadejte prosím ověřovací kód z vaší aplikace" show_key_description: "Vložit ručně" + extended_description: | + Dvoufaktorová autentizace přidává další bezpečnostní vrstvu k vašemu účtu, protože vedle hesla vyžaduje ještě i zadání jednorázového kódu, vytvořeného na zařízeních Android nebo iOS. + oauth_enabled_warning: "Upozorňujeme, že možnost přihlásit se pomocí účtu ze sociální sítě bude vypnuta, jakmila bude na vašem účtu povolena dvoufaktorová autentizace." change_about: title: "Změna o mně" error: "Došlo k chybě při pokusu změnit tuto hodnotu." change_username: title: "Změnit uživatelské jméno" + confirm: "Jste si naprosto jisti, že chcete změnit své uživatelské jméno?" taken: "Toto uživatelské jméno je již zabrané." invalid: "Uživatelské jméno je neplatné. Musí obsahovat pouze písmena a číslice." change_email: @@ -730,7 +780,6 @@ cs: title: "Změňte si svůj profilový obrázek" gravatar: "Založeno na Gravataru" gravatar_title: "Změňte si avatar na webových stránkách Gravatar" - gravatar_failed: "Nelze načíst Gravatar. Je nějaký Gravatar přiřazen k této e-mailové adrese?" refresh_gravatar_title: "Obnovit Gravatar" letter_based: "Systémem přidělený profilový obrázek" uploaded_avatar: "Vlastní obrázek" @@ -746,6 +795,9 @@ cs: instructions: "Obrázky pozadí jsou zarovnány a mají výchozí šířku 590px. " email: title: "Emailová adresa" + primary: "Hlavní email" + secondary: "Záložní emailové adresy" + no_secondary: "Žádné záložní emailové adresy" instructions: "není veřejný" ok: "Pro potvrzení vám pošleme email." invalid: "Zadejte prosím správnou emailovou adresu" @@ -756,6 +808,11 @@ cs: few: "Email vám zašleme pouze pokud jste se neukázali během posledních {{count}} minut." many: "Email vám zašleme pouze pokud jste se neukázali během posledních {{count}} minut." other: "Email vám zašleme pouze pokud jste se neukázali během posledních {{count}} minut." + associated_accounts: + title: "Propojené účty" + connect: "Připojit" + revoke: "Odpojit" + not_connected: "(nepřipojeno)" name: title: "Jméno" instructions: "vaše celé jméno (nepovinné)" @@ -780,6 +837,17 @@ cs: any: "jakýkoliv" password_confirmation: title: "Heslo znovu" + auth_tokens: + title: "Nedávno použitá zařízení" + ip: "IP" + details: "Podrobnosti" + log_out_all: "Odhlásit všechny" + active: "právě aktivní" + not_you: "Nejste to vy?" + show_all: "Ukázat vše ({{count}})" + show_few: "Ukázat méně" + was_this_you: "Byla to vaše návštěva?" + browser_and_device: "{{browser}} na {{device}}" last_posted: "Poslední příspěvek" last_emailed: "Email naposledy zaslán" last_seen: "Naposledy viděn" @@ -788,6 +856,7 @@ cs: location: "Lokace" website: "Webová stránka" email_settings: "Emailová upozornění" + hide_profile_and_presence: "Skrýt můj veřejný profil a informace o mé přítomnosti" like_notification_frequency: title: "Upozornit mě na \"to se mi líbí\"" always: "Vždy" @@ -800,7 +869,6 @@ cs: always: "vždy" never: "nikdy" email_digests: - title: "Pokud tady dlouho nebudu, chci zasílat souhrnné emaily s populárními tématy a reakcemi" every_30_minutes: "každých 30 minut" every_hour: "každou hodinu" daily: "denně" @@ -945,6 +1013,9 @@ cs: most_liked_users: "Nejvíce \"to se mi líbí\"" most_replied_to_users: "Nejvíce odpovědí" no_likes: "Zatím žádné \"to se mi líbí\"" + top_categories: "Nej kategorie" + topics: "Témata" + replies: "Odpovědi" ip_address: title: "Poslední IP adresa" registration_ip_address: @@ -990,6 +1061,9 @@ cs: enabled: "Tato stránka je v režimu jen pro čtení. Prosim pokračujte v prohlížení, ale odpovídání a ostatní operace jsou momentálně vypnuté." login_disabled: "Přihlášení je zakázáno jelikož je stránka v režimu jen pro čtení." logout_disabled: "Odhlášení je zakázáno zatímco je stránka v režimu jen pro čtení." + too_few_topics_and_posts_notice: "Pojďme rozjet diskuzi! V tuto chvíli je tu %{currentTopics} / %{requiredTopics} témat a %{currentPosts} / %{requiredPosts} příspěvků. Noví návštěvníci potřebují mít co číst a na co odpovídat." + too_few_topics_notice: "Pojďme rozjet diskuzi! V tuto chvíli je tu %{currentTopics} / %{requiredTopics} témat. Noví návštěvníci potřebují mít co číst a na co odpovídat." + too_few_posts_notice: "Pojďme rozjet diskuzi! V tuto chvíli je tu %{currentPosts} / %{requiredPosts} příspěvků. Noví návštěvníci potřebují mít co číst a na co odpovídat." logs_error_rate_notice: reached: "%{relativeAge}%{rate} dosáhlo limitu stránky, který je %{siteSettingRate}." exceeded: "%{relativeAge}%{rate} přesahuje limit stránky, který je %{siteSettingRate}." @@ -1027,6 +1101,8 @@ cs: hide_session: "Připomenout mi zítra" hide_forever: "děkuji, ne" hidden_for_session: "Dobrá, zeptám se tě zítra. Pro založení účtu můžeš také vždy použít 'Přihlásit se'." + intro: "Zdravíčko! Vypadá to, že si užíváte naše diskuze, ale ještě nemáte vlastní účet." + value_prop: "Pokud si založíš účet, budeme si přesně pamatovat, co jsi četl, takže se vždycky vrátíš do bodu, odkud jsi odešel. Také budeš dostávat upozornění zde a přes e-mail, kdykoli ti někdo odpoví. A můžeš přidávat 'to se mi líbí' a šířit tak lásku. :heartpulse:" summary: enabled_description: "Čtete shrnutí tohoto tématu: nejzajímavější příspěvky podle komunity." description: "Je tu {{replyCount}} odpovědí." @@ -1040,6 +1116,8 @@ cs: disable: "Zobrazit smazané příspěvky" private_message_info: title: "Zpráva" + invite: "Pozvat další..." + edit: "Přidat nebo odebrat..." leave_message: "Opravdu chcete opustit tuto zprávu?" remove_allowed_user: "Určitě chcete odstranit {{name}} z této zprávy?" remove_allowed_group: "Opravdu chcete odstranit {{name}} z této zprávy?" @@ -1069,7 +1147,12 @@ cs: button_ok: "OK" button_help: "Nápověda" email_login: + link_label: "Chci odkaz na login do emailu" button_label: "přes email" + complete_username: "Pokud je tu účet, který používá uživatelské jméno %{username}, měl by vám za chvilku přijít email s přihlašovacím odkazem." + complete_email: "Pokud je tu účet s emailovou adresou %{email}, měl by vám za chvilku přijít email s přihlašovacím odkazem." + complete_username_found: "Našli jsme účet s uživatelským jménem %{username}, za chvilku by měl přijít email s přihlašovacím odkazem." + complete_email_found: "Našli jsme účet s emailovou adresou %{email}, za chvilku by měl přijít email s přihlašovacím odkazem." complete_username_not_found: "Nebyl nalezen účet s uživatelským jménem %{username}" complete_email_not_found: "Nebyl nalezen účet s emailem %{email}" login: @@ -1077,9 +1160,15 @@ cs: username: "Uživatel" password: "Heslo" second_factor_title: "Dvoufázové přihlašování" + second_factor_description: "Prosím zadejte autentikační kód z vaší aplikace:" + second_factor_backup: "Přihlásit se záložním kódem" + second_factor_backup_title: "Dvoufaktorová autentizace" + second_factor_backup_description: "Prosíme zadejte jeden ze svých záložních kódů:" + second_factor: "Přihlásit se aplikací Authenticator" email_placeholder: "emailová adresa nebo uživatelské jméno" caps_lock_warning: "zapnutý Caps Lock" error: "Neznámá chyba" + cookies_error: "Vypadá to, že váš prohlížeč má zakázané cookies. Bez jejich povolení se nebude možné přihlásit." rate_limit: "Počkejte před dalším pokusem se přihlásit." blank_username: "Zadejte vaši emailovou adresu a uživatelské jméno." blank_username_or_password: "Vyplňte prosím email nebo uživatelské jméno, a heslo." @@ -1094,6 +1183,7 @@ cs: not_allowed_from_ip_address: "Z této IP adresy se nemůžete přihlásit." admin_not_allowed_from_ip_address: "Z této IP adresy se nemůžete přihlásit jako administrátor." resend_activation_email: "Klikněte sem pro zaslání aktivačního emailu." + omniauth_disallow_totp: "Váš účet má zapnutou dvoufaktorovou autentizaci. Prosíme přihlašte se napřed svým heslem." resend_title: "Znovu odeslat aktivační email" change_email: "Změnit emailovou adresu" provide_new_email: "Zadejte novou adresu na kterou chcete zaslat potvrzovací email." @@ -1153,15 +1243,30 @@ cs: categories_with_featured_topics: "Kategorie a vybrané příspěvky" categories_and_latest_topics: "Kategorie a nejnovější témata" categories_and_top_topics: "Kategorie a populární témata" + categories_boxes: "Boxy s podkategoriemi" + categories_boxes_with_topics: "Boxy s vybranými tématy" shortcut_modifier_key: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + conditional_loading_section: + loading: Načítá se... select_kit: default_header_text: Vybrat… no_content: Nebyly nalezeny žádné výsledky filter_placeholder: Hledat... + filter_placeholder_with_any: Vyhledat nebo vytvořit... create: "Vytvořit: '{{content}}'" + max_content_reached: + one: "Můžete vybrat jen {{count}} položku." + few: "Můžete vybrat jen {{count}} položky." + many: "Můžete vybrat pouze {{count}} položek." + other: "Můžete vybrat jen {{count}} položek." + min_content_not_reached: + one: "Vyberte nejméně {{count}} položku." + few: "Vyberte nejméně {{count}} položky." + many: "Vyberte nejméně {{count}} položek." + other: "Vyberte nejméně {{count}} položek." emoji_picker: filter_placeholder: Hledat emoji people: Lidé @@ -1220,7 +1325,9 @@ cs: post_length: "Příspěvek musí být alespoň {{min}} znaků dlouhý" try_like: 'Zkusili jste tlačítko ?' category_missing: "Musíte vybrat kategorii" + tags_missing: "Vybraných tagů musí být nejméně {{count}}" save_edit: "Uložit změnu" + overwrite_edit: "Přepsat úpravu" reply_original: "Odpovědět na původní téma" reply_here: "Odpovědět sem" reply: "Odpovědět" @@ -1229,6 +1336,7 @@ cs: create_pm: "Zpráva" create_whisper: "Šeptat" create_shared_draft: "Vytvořit sdílený koncept" + edit_shared_draft: "Upravit sdílený koncept" title: "Nebo zmáčkněte Ctrl+Enter" users_placeholder: "Přidat uživatele" title_placeholder: "O čem je ve zkratce tato diskuze?" @@ -1239,6 +1347,7 @@ cs: remove_featured_link: "Odstranit odkaz z tématu." reply_placeholder: "Pište sem. Můžete použít Markdown, BBCode nebo HTML. Obrázky nahrajte přetáhnutím nebo vložením ze schránky." reply_placeholder_no_images: "Pište sem. Můžete použít Markdown, BBCode nebo HTML." + reply_placeholder_choose_category: "Musíte vybrat kategorii, než sem začnete psát." view_new_post: "Zobrazit váš nový příspěvek." saving: "Ukládám" saved: "Uloženo!" @@ -1257,6 +1366,7 @@ cs: link_description: "sem vložte popis odkazu" link_dialog_title: "Vložit odkaz" link_optional_text: "volitelný popis" + link_url_placeholder: "https://example.com" quote_title: "Bloková citace" quote_text: "Bloková citace" code_title: "Ukázka kódu" @@ -1271,6 +1381,8 @@ cs: help: "Nápověda pro Markdown" collapse: "minimalizovat panel editoru" abandon: "zavřít editor a zahodit koncept" + enter_fullscreen: "otevřít editor na celou obrazovku" + exit_fullscreen: "opustit editor na celou obrazovku" modal_ok: "OK" modal_cancel: "Zrušit" cant_send_pm: "Bohužel, nemůžete poslat zprávu uživateli %{username}." @@ -1279,6 +1391,8 @@ cs: body: "Nyní tuto zprávu posíláte pouze sám/sama sobě!" admin_options_title: "Volitelné redakční nastavení tématu" composer_actions: + reply: Odpověď + draft: Koncept edit: Upravit reply_to_post: label: "Odpovědět na příspěvek %{postNumber} od %{postUsername}" @@ -1300,6 +1414,9 @@ cs: shared_draft: label: "Sdílený koncept" desc: "Navrhnout téma, které uvidí pouze redakce" + toggle_topic_bump: + label: "Přepnout zvýraznění tématu" + desc: "Odpovědět bez ovlivnění data poslední odpovědi" notifications: tooltip: regular: @@ -1352,6 +1469,8 @@ cs: posted: '{{username}} přispěl do "{{topic}}" - {{site_title}}' private_message: '{{username}} vám poslal soukromou zprávu v "{{topic}}" - {{site_title}}' linked: '{{username}} odkázal na vás příspěvek v "{{topic}}" - {{site_title}}' + confirm_title: 'Upozornění zapnuta - %{site_title}' + confirm_body: 'Upozornění úspěšně zapnuta.' upload_selector: title: "Vložit obrázek" title_with_attachments: "Nahrát obrázek nebo soubor" @@ -1377,6 +1496,11 @@ cs: select_all: "Vybrat vše" clear_all: "Vymazat vše" too_short: "Hledaný výraz je příliš krátký." + result_count: + one: "1 výsledek pro{{term}}" + few: "{{count}}{{plus}} výsledky pro{{term}}" + many: "{{count}}{{plus}} výsledků pro{{term}}" + other: "{{count}}{{plus}} výsledků pro{{term}}" title: "vyhledávat témata, příspěvky, uživatele nebo kategorie" full_page_title: "vyhledávat témata nebo příspěvky" no_results: "Nenalezeny žádné výsledky." @@ -1410,6 +1534,7 @@ cs: label: Se štítkem filters: label: "Pouze v tématech/příspěvcích, které" + title: Hledat jen v nadpisu likes: se mi líbí posted: Přidal jsem příspěvek watching: Sleduji @@ -1644,6 +1769,7 @@ cs: jump_prompt_of: "%{count} příspěvků" jump_prompt_long: "Na který příspěvek chcete přejít?" jump_bottom_with_number: "Skočit na příspěvěk %{post_number}" + jump_prompt_to_date: "do data" jump_prompt_or: "nebo" total: celkem příspěvků current: aktuální příspěvek @@ -1706,6 +1832,7 @@ cs: reset_read: "Vynulovat počet čtení" make_public: "Vytvořit Veřejné Téma" make_private: "Vytvořit soukromou zprávu " + reset_bump_date: "Resetovat datum zvýraznění" feature: pin: "Připevnit téma" unpin: "Odstranit připevnění" @@ -1818,9 +1945,15 @@ cs: action: "sluč vybrané příspěvky" error: "Během slučování vybraných příspěvků došlo k chybě." change_owner: + title: "Změnit vlastníka" action: "změna autora" error: "Chyba při měnění autora u příspevků." placeholder: "uživatelské jméno nového autora" + instructions: + one: "Prosíme vyberte nového vlastníka pro příspěvek od @{{old_user}}" + few: "Prosíme vyberte nového vlastníka pro {{count}} příspěvky od @{{old_user}}" + many: "Prosíme vyberte nového vlastníka pro {{count}} příspěvků od @{{old_user}}" + other: "Prosíme vyberte nového vlastníka pro {{count}} příspěvků od @{{old_user}}" change_timestamp: title: "Změnit časové razítko..." action: "změnit časovou značku" @@ -1905,7 +2038,6 @@ cs: upload: "Bohužel nastala chyba při nahrávání příspěvku. Prosím zkuste to znovu." file_too_large: "Soubor, který se snažíte nahrát, je bohužel příliš velký (maximální velikost je {{max_size_kb}}kb). Co třeba jej nahrát na cloudovou službu a nasdílet sem odkaz?" too_many_uploads: "Bohužel, najednou smíte nahrát jen jeden soubor." - too_many_dragged_and_dropped_files: "Bohužel, najednou smíte nahrát jen 10 souborů." upload_not_authorized: "Bohužel, soubor, který se snažíš nahrát, není povolený (povolená rozšíření: {{authorized_extensions}}). " image_upload_not_allowed_for_new_user: "Bohužel, noví uživatelé nemohou nahrávat obrázky." attachment_upload_not_allowed_for_new_user: "Bohužel, noví uživatelé nemohou nahrávat přílohy." @@ -1961,6 +2093,8 @@ cs: lock_post_description: "Zabránit přispěvatelům v úpravách tohoto příspěvku" unlock_post: "Odemknout příspěvek" unlock_post_description: "Dovolit přispěvatelům úpravy tohoto příspěvku" + delete_topic_disallowed_modal: "Nemáte práva smazat toto téma. Pokud opravdu má být smazáno, označte ho pro moderátory a popište důvody." + delete_topic_disallowed: "nemáte práva smazat toto téma" actions: flag: 'Nahlásit' defer_flags: @@ -2116,6 +2250,7 @@ cs: can: 'smí… ' none: '(bez kategorie)' all: 'Všechny kategorie' + choose: 'kategorie…' edit: 'upravit' edit_long: "Upravit" view: 'Zobrazit témata v kategorii' @@ -2164,6 +2299,7 @@ cs: show_subcategory_list: "Ukázat seznam podkategorií nad tématy v této kategorii." num_featured_topics: "Počet témat, která se zobrazují na stránce kategorie " subcategory_num_featured_topics: "Počet zobrazených témat na stránce nadřazené kategorie:" + all_topics_wiki: "Automaticky zakládat nová témata jako wiki" subcategory_list_style: "Styl seznamu podkategorií: " sort_order: "Seznam témat seřazený podle: " default_view: "Výchozí seznam témat: " @@ -2171,11 +2307,17 @@ cs: allow_badges_label: "Povolit používání odznaků v této kategorii" edit_permissions: "Upravit oprávnění" add_permission: "Přidat oprávnění" + require_topic_approval: "Požadovat schválení moderátorů pro všechna nová témata" + require_reply_approval: "Požadovat schválení moderátorů pro všechny nové odpovědi" this_year: "letos" + position: "Pozice:" default_position: "Výchozí umístění" position_disabled: "Kategorie jsou zobrazovány podle pořadí aktivity. Pro kontrolu pořadí kategorií v seznamech," position_disabled_click: 'povolte nastavení "neměnné pozice kategorií" (fixed category positions).' + minimum_required_tags: 'Nejmenší počet tagů požadovaných pro téma:' parent: "Nadřazená kategorie" + num_auto_bump_daily: 'Počet otevřených témat, která mají být denně automaticky zvýrazněna:' + navigate_to_first_post_after_read: 'Přesunout na první příspěvek, jakmile jsou přečtena témata' notifications: watching: title: "Hlídání" @@ -2418,6 +2560,7 @@ cs: this_week: "Týden" today: "Dnes" other_periods: "viz nahoře" + browser_update: 'Váš prohlížeč je bohužel příliš starý a tyto stránky nebudou fungovat. Prosíme pořiďte si novější verzi prohlížeče.' permission_types: full: "Vytvářet / Odpovídat / Prohlížet" create_post: "Odpovídat / Prohlížet" @@ -2437,6 +2580,7 @@ cs: bookmarks: 'g, b Záložky' profile: 'g, p Profil' messages: 'g, m Zprávy' + drafts: 'g, d Koncepty' navigation: title: 'Navigace' jump: '# Přejdi na příspěvek #' @@ -2459,6 +2603,7 @@ cs: composing: title: 'Skládání' return: 'shift+cnávrat ke skládání' + fullscreen: 'shift+F11 Editor na celou obrazovku' actions: title: 'Akce' bookmark_topic: 'Přidat záložku na téma' @@ -2550,6 +2695,10 @@ cs: sort_by_name: "jména" manage_groups: "Spravovat skupiny štítků" manage_groups_description: "Definice skupin pro organizaci štítků" + upload: "Nahrát tagy" + upload_description: "Vytvořit mnoho tagů naráz pomocí nahraného textového souboru" + upload_instructions: "Jeden na řádek, případně se skupinou tagů ve formátu 'jméno_tagu,skupina_tagu'." + upload_successful: "Tagy úspěšně nahrány" filters: without_category: "%{filter} %{tag} témata" with_category: "%{filter} %{tag} témata v %{category}" @@ -2558,15 +2707,19 @@ cs: notifications: watching: title: "Hlídání" + description: "Budete automaticky sledovat všechna nová témata s těmito štítky. Na všechny nové příspěvky a témata budete upozorněni. Počet nových příspěvků se zobrazí vedle tématu." watching_first_post: title: "Hlídané první příspěvky" + description: "Budete upozorněni na první příspěvek v každém novém tématu s tímto tagem." tracking: title: "Sledování" + description: "Budete automaticky sledovat všechna témata s tímto tagem. Počet nepřečtených a nových příspěvků bude vidět vedle tématu." regular: title: "Běžný" description: "Budete informováni pokud někdo zmíní vaše @jméno nebo odpoví na váš příspěvek." muted: title: "Ztišené" + description: "Nebudete upozorněni na žádná nová témata s tímto tagem, a nebudou se objevovat na vaší záložce s nepřečtenými." groups: title: "Skupiny štítků" about: "Přidej štítky do skupin pro přehlednost" @@ -2580,6 +2733,8 @@ cs: save: "Uložit" delete: "Smazat" confirm_delete: "Jste si jistí, že chcete smazat tuto skupinu štítků?" + everyone_can_use: "Tagy mohou být použity kýmkoliv" + usable_only_by_staff: "Tagy jsou viditelné pro všechny, ale jen redaktoři je mohou použít" visible_only_to_staff: "Štítky vidí pouze redaktoři" topics: none: @@ -2603,9 +2758,11 @@ cs: bookmarks: "Žádná další oblíbená témata nejsou k dispozici." search: "Vyhledávání nic nenašlo." invite: + custom_message: "Aby byla pozvánka osobnější, napište vlastní zprávu" custom_message_placeholder: "Vložte vlastní zprávu" custom_message_template_forum: "Ahoj! Měl by se připojit na toto fórum!" custom_message_template_topic: "Ahoj, myslím si, že by se ti mohlo tohle téma líbit!" + forced_anonymous: "Kvůli vysoké zátěži dočasně všichni vidí stránku tak, jak by ji viděli nepřihlášení uživatelé." safe_mode: enabled: "Bezpečný režim je povolený, k jeho zakázání zavři toto okno prohlížeče" admin_js: @@ -2613,9 +2770,13 @@ cs: admin: title: 'Administrátor' moderator: 'Moderátor' + reports: + title: "Seznam dostupných reportů" dashboard: title: "Rozcestník" last_updated: "Přehled naposled aktualizován:" + find_old: "Hledáte starý přehled?" + old_link: "navštivte jej zde" version: "Verze Discourse" up_to_date: "Máte aktuální!" critical_available: "Je k dispozici důležitá aktualizace." @@ -3451,7 +3612,7 @@ cs: flags_given_received_count: 'Rozdaná / obdržená nahlášení' approve: 'Schválit' approved_by: "schválil" - approve_success: "Uživatel bys schválen a byl mu zaslán aktivační email s instrukcemi." + approve_success: "Uživatel byl schválen a byl mu zaslán aktivační email s instrukcemi." approve_bulk_success: "Povedlo se! Všichni uživatelé byli schváleni a byly jim rozeslány notifikace." time_read: "Čas strávený čtením" anonymize: "Anonymní uživatel" @@ -3770,3 +3931,7 @@ cs: admin: "Administrátor" moderator: "Moderátor" regular: "pravidelný uživatel" + previews: + topic_title: "Diskuzní téma" + share_button: "Sdílet" + reply_button: "Odpovědět" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 9150dd890a..4c706abe2f 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -530,7 +530,6 @@ da: watched_first_post_tags: "Ser Første Indlæg" watched_first_post_tags_instructions: "Du får besked om første indlæg i hvert nyt emne med disse tags." muted_categories: "Ignoreret" - muted_categories_instructions: "Du får ikke beskeder om nye emner i disse kategorier og de fremstår ikke i seneste." no_category_access: "Som moderator har du begrænset kategori-adgang, at gemme er slået fra. " delete_account: "Slet min konto" delete_account_confirm: "Er du sikker på du vil slette din konto permanent? Dette kan ikke fortrydes!" @@ -667,7 +666,6 @@ da: always: "altid" never: "aldrig" email_digests: - title: "Når jeg ikke kommer forbi her, send mig da en email opsummering af populære emner og indlæg" every_30_minutes: "hvert 30. minut" every_hour: "hver time" daily: "dagligt" @@ -1536,7 +1534,6 @@ da: upload: "Beklager, der opstod en fejl ved upload af filen. Prøv venligst igen." file_too_large: "Beklager, filen du prøver at uploade er for stor (den maksimale størrelse er {{max_size_kb}}kb). Du kan evt. uploade filen til en fildelings service og dele linket her." too_many_uploads: "Beklager, men du kan kun uploade én fil ad gangen." - too_many_dragged_and_dropped_files: "Beklager, du kan maksimalt uploade 10 filer ad gangen" upload_not_authorized: "Beklager, filen som du forsøger at uploade, er ikke tiladt (tilladte filendelser: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Beklager, nye brugere kan ikke uploade billeder." attachment_upload_not_allowed_for_new_user: "Beklager, nye brugere kan ikke uploade vedhæftede filer." diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 3fde484ae8..916be16db1 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -536,7 +536,6 @@ de: topics_entered: "betrachtete Themen" post_count: "# Beiträge" confirm_delete_other_accounts: "Bist du sicher, dass du diese Konten löschen willst?" - powered_by: "powered by ipinfo.io" copied: "kopiert" user_fields: none: "(wähle eine Option aus)" @@ -555,6 +554,7 @@ de: private_messages: "Nachrichten" activity_stream: "Aktivität" preferences: "Einstellungen" + profile_hidden: "Das öffentliche Profil des Benutzers ist ausgeblendet." expand_profile: "Erweitern" collapse_profile: "Zuklappen" bookmarks: "Lesezeichen" @@ -621,7 +621,6 @@ de: watched_first_post_tags: "Ersten Beitrag beobachten" watched_first_post_tags_instructions: "Du erhältst eine Benachrichtigung für den ersten Beitrag in jedem neuen Thema mit diesen Schlagwörtern." muted_categories: "Stummgeschaltet" - muted_categories_instructions: "Du erhältst keine Benachrichtigungen über neue Themen in dieser Kategorie und die Themen werden auch nicht in der Liste der aktuellen Themen erscheinen." no_category_access: "Moderaturen haben eingeschränkte Kategorien-Berechtigungen, Speichern ist nicht verfügbar." delete_account: "Lösche mein Benutzerkonto" delete_account_confirm: "Möchtest du wirklich dein Benutzerkonto permanent löschen? Diese Aktion kann nicht rückgängig gemacht werden!" @@ -695,15 +694,14 @@ de: second_factor: title: "Zwei-Faktor-Authentifizierung" disable: "Zwei-Faktor-Authentifizierung deaktivieren" - enable: "Zwei-Faktor-Authentifizierung aktivieren, um das Benutzerkonto besser zu schützen" confirm_password_description: "Bitte bestätige dein Passwort um fortzufahren" label: "Code" enable_description: | - Scanne diesen QR-Code in einer unterstützten App (AndroidiOSWindows Phone) und gib’ deinen Authentifizierungscode ein. + Scanne diesen QR-Code in einer unterstützten App (AndroidiOS) und gib’ deinen Authentifizierungscode ein. disable_description: "Bitte gib den Authentifizierungscode aus deiner App ein." show_key_description: "Manuell eingeben" extended_description: | - Zwei-Faktor-Authentifizierung (2FA) sichert dein Konto zusätzlich ab, indem sie zusätzlich zu deinem Passwort einen einmalig gültigen Code anfordert. Codes können auf Android-, iOS- und Windows Phone-Geräten generiert werden. + Zwei-Faktor-Authentifizierung (2FA) sichert dein Konto zusätzlich ab, indem sie zusätzlich zu deinem Passwort einen einmalig gültigen Code anfordert. Codes können auf Android- und iOS--Geräten generiert werden. oauth_enabled_warning: "Beachte bitte, dass soziale Anmelde-Methoden deaktiviert werden, sobald die Zwei-Faktor-Authentifizierung für dein Konto aktiviert ist." change_about: title: "„Über mich“ ändern" @@ -723,7 +721,6 @@ de: title: "Ändere dein Profilbild" gravatar: "Gravatar, basierend auf" gravatar_title: "Ändere deinen Avatar auf der Gravatar-Webseite" - gravatar_failed: "Konnte Gravatar nicht abrufen. Ist mit dieser E-Mail-Adresse eines verknüpft?" refresh_gravatar_title: "Deinen Gravatar aktualisieren" letter_based: "ein vom System zugewiesenes Profilbild" uploaded_avatar: "Eigenes Bild" @@ -790,8 +787,6 @@ de: show_few: "Weniger anzeigen" was_this_you: "Warst das du?" browser_and_device: "{{browser}} auf {{device}}" - secure_account: "Mein Konto absichern" - latest_post: "Dein letzter Beitrag..." last_posted: "Letzter Beitrag" last_emailed: "Letzte E-Mail" last_seen: "Zuletzt gesehen" @@ -800,6 +795,7 @@ de: location: "Wohnort" website: "Website" email_settings: "E-Mail" + hide_profile_and_presence: "Blende mein öffentliches Profil und Anwesenheitsfunktionen aus" like_notification_frequency: title: "Benachrichtigung für erhaltene Likes anzeigen" always: "immer" @@ -812,7 +808,6 @@ de: always: "immer" never: "nie" email_digests: - title: "Sende mir eine E-Mail-Zusammenfassung mit beliebten Themen und Antworten, wenn ich länger nicht hier war:" every_30_minutes: "alle 30 Minuten" every_hour: "stündlich" daily: "täglich" @@ -987,6 +982,9 @@ 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 Diskussionen starten! Es existieren bisher %{currentTopics} von %{requiredTopics} benötigten Themen und %{currentPosts} von %{requiredPosts} benötigten Beiträgen. Neue Besucher benötigen bestehende Konversationen, die sie lesen und auf die sie antworten können." + too_few_topics_notice: "Lass' die Diskussionen starten! Es existieren bisher %{currentTopics} von %{requiredTopics} benötigten Themen. Neue Besucher benötigen bestehende Konversationen, die sie lesen und auf die sie antworten können." + too_few_posts_notice: "Lass' die Diskussionen starten! Es existieren bisher %{currentPosts} von %{requiredPosts} benötigten Beiträgen. Neue Besucher benötigen bestehende Konversationen, die sie lesen und auf die sie antworten können." logs_error_rate_notice: reached: "%{relativeAge}%{rate} hat die eingestellte Grenze für die Site von %{siteSettingRate} erreicht." exceeded: "%{relativeAge}%{rate} hat die eingestellte Grenze für die Site von %{siteSettingRate} überschritten." @@ -1162,6 +1160,7 @@ de: categories_with_featured_topics: "Kategorien mit empfohlenen Themen" categories_and_latest_topics: "Kategorien und aktuelle Themen" categories_and_top_topics: "Kategorien und angesagte Themen" + categories_boxes: "Boxen mit Unterkategorien" categories_boxes_with_topics: "Spalten mit hervorgehobenen Themen" shortcut_modifier_key: shift: 'Umschalt' @@ -1277,6 +1276,7 @@ de: link_description: "gib hier eine Link-Beschreibung ein" link_dialog_title: "Link einfügen" link_optional_text: "Optionaler Titel" + link_url_placeholder: "https://example.com" quote_title: "Zitat" quote_text: "Zitat" code_title: "Vorformatierter Text" @@ -1291,6 +1291,8 @@ de: help: "Hilfe zur Markdown-Formatierung" collapse: "Editor minimieren" abandon: "Editor schließen und Entwurf verwerfen" + enter_fullscreen: "Vollbild-Editor öffnen" + exit_fullscreen: "Vollbild-Editor verlassen" modal_ok: "OK" modal_cancel: "Abbrechen" cant_send_pm: "Entschuldige, aber du kannst keine Nachricht an %{username} senden." @@ -1892,7 +1894,6 @@ de: upload: "Entschuldige, es gab einen Fehler beim Hochladen der Datei. Bitte versuche es noch einmal." file_too_large: "Entschuldige, diese Datei ist zu groß (maximal erlaubt sind {{max_size_kb}} KB). Wie wär’s, wenn du deine große Datei bei einem Filehosting-Dienst hochlädst und dann den Link teilst?" too_many_uploads: "Entschuldige, du darfst immer nur eine Datei hochladen." - too_many_dragged_and_dropped_files: "Entschuldige, du kannst nur 10 Dateien auf einmal hochladen." upload_not_authorized: "Entschuldigung, die Datei die du hochladen möchtest ist nicht erlaubt (erlaubte Dateiendungen sind: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Entschuldige, neue Benutzer dürfen keine Bilder hochladen." attachment_upload_not_allowed_for_new_user: "Entschuldige, neue Benutzer dürfen keine Dateien hochladen." @@ -2341,6 +2342,7 @@ de: this_week: "Woche" today: "Heute" other_periods: "zeige angesagte Themen:" + browser_update: 'Leider ist dein Browser zu alt, damit diese Seite richtig funktioniert. Bitte aktualisiere deinen Browser.' permission_types: full: "Erstellen / Antworten / Ansehen" create_post: "Antworten / Ansehen" @@ -2383,6 +2385,7 @@ de: composing: title: 'Schreiben' return: 'shift+c Zurück zum Editor' + fullscreen: 'Umschalt+F11 Vollbild-Editor' actions: title: 'Aktionen' bookmark_topic: 'f Lesezeichen hinzufügen/entfernen' @@ -2464,6 +2467,10 @@ de: sort_by_name: "Name" manage_groups: "Schlagwort-Gruppen verwalten" manage_groups_description: "Gruppen definieren, um Schlagwörter zu organisieren" + upload: "Schlagwärter hochladen" + upload_description: "Lade eine Textdatei hoch, um mehrere Schlagwörter auf einmal zu erstellen" + upload_instructions: "Eine pro Zeile, optional mit einer Schlagwortgruppe nach dem Schema 'schlagwort_name,schlagwort_gruppe'." + upload_successful: "Schlagwörter erfolgreich hochgeladen" filters: without_category: "%{filter} %{tag} Themen" with_category: "%{filter} %{tag} Themen in %{category}" @@ -2876,6 +2883,7 @@ de: label: "Hochladen" title: "Eine Sicherung zu dieser Instanz hochladen" uploading: "Wird hochgeladen…" + uploading_progress: "Wird hochgeladen... {{progress}}%" success: "'{{filename}}' wurd erfolgreich hochgeladen. Die Datei wird nun verarbeitet. Es kann bis zu einer Minute dauern, bis sie in der Liste erscheint." error: "Beim Hochladen der Datei '{{filename}}' ist ein Fehler aufgetreten: {{message}}" operations: @@ -2906,6 +2914,9 @@ de: label: "Zurücksetzen" title: "Die Datenbank auf den letzten funktionierenden Zustand zurücksetzen" confirm: "Möchtest du wirklich die Datenbank auf den letzten funktionierenden Stand zurücksetzen?" + location: + local: "Lokal" + s3: "Amazon S3" export_csv: success: "Der Export wurde gestartet. Du erhältst eine Nachricht, sobald der Vorgang abgeschlossen ist." failed: "Der Export ist fehlgeschlagen. Bitte überprüfe die Logs." diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 43bef6117a..b0338e9587 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -439,7 +439,6 @@ el: topics_entered: "νήματα που προβλήθηκαν" post_count: "# αναρτήσεις" confirm_delete_other_accounts: "Είσε σίγουρος ότι θέλεις να διαγράψεις αυτούς τους λογαριασμούς;" - powered_by: "powered by ipinfo.io" user_fields: none: "(διαλέξτε μία επιλογή)" user: @@ -518,7 +517,6 @@ el: watched_first_post_tags: "Επιτήρηση Πρώτης Ανάρτησης" watched_first_post_tags_instructions: "Θα ειδοποιηθείς για την πρώτη ανάρτηση σε κάθε νέο νήμα με αυτές τις ετικέτες. " muted_categories: "Σε σίγαση" - muted_categories_instructions: "Δε θα λαμβάνεις ειδοποιήσεις για τίποτα σχετικά με τα νέα νήματα σε αυτές τις κατηγορίες και δε θα εμφανίζονται στα τελευταία." no_category_access: "Ως συντονιστής έχεις περιορισμένη πρόσβαση στην κατηγορία, η αποθήκευση είναι απενεργοποιημένη." delete_account: "Διαγραφή Λογαριασμού" delete_account_confirm: "Είσαι σίγουρος πως θέλεις να διαγράψεις μόνιμα τον λογαριασμό σου; Αυτή η πράξη είναι μη αναστρέψιμη!" @@ -589,7 +587,6 @@ el: title: "Αλλαγή της φωτογραφίας του προφίλ σου" gravatar: "Gravatar, που βασίζεται σε" gravatar_title: "Άλλαξε το άβατάρ σου στην ιστοσελίδα Gravatar " - gravatar_failed: "Δεν μπόρεσε να ληφθεί το Gravatar. Υπάρχει κάποιο συνδεδεμένο με αυτό το email;" refresh_gravatar_title: "Ανανέωσε το Gravatar σου" letter_based: "Εικόνα προφίλ που ανέθεσε το σύστημα" uploaded_avatar: "Προσαρμοσμένη εικόνα" @@ -657,7 +654,6 @@ el: always: "πάντα" never: "ποτέ" email_digests: - title: "Όταν δεν επισκέπτομαι αυτή τη σελίδα, στείλε μου με email την περίληψη των δημοφιλών νημάτων και απαντήσεων" every_30_minutes: "κάθε 30 λεπτά" every_hour: "ωριαία" daily: "καθημερινά" @@ -1595,7 +1591,6 @@ el: upload: "Λυπούμαστε, παρουσιάστηκε σφάλμα κατά το ανέβασμα του αρχείου. Προσπάθησε πάλι." file_too_large: "Λυπούμαστε, αυτό το αρχείο είναι πολύ μεγάλο (το μέγιστο μέγεθος είναι {{max_size_kb}}kb). Γιατί δεν μεταφορτώνεις το αρχείο σου σε μια υπηρεσία ανταλλαγής αρχείων κι έπειτα να μοιραστείς τον σύνδεσμο;" too_many_uploads: "Λυπούμαστε, μπορείς να ανεβάζεις μόνο ένα αρχείο τη φορά." - too_many_dragged_and_dropped_files: "Λυπούμαστε, μπορείς να ανεβάσεις μόνο 10 αρχεία τη φορά." upload_not_authorized: "Λυπούμαστε, το αρχείο που προσπαθείς να ανεβάσεις δεν επιτρέπεται (επιτρεπόμενες επεκτάσεις:{{authorized_extensions}})" image_upload_not_allowed_for_new_user: "Λυπούμαστε, οι νέοι χρήστες δεν μπορούν να ανεβάσουν εικόνες." attachment_upload_not_allowed_for_new_user: "Λυπούμαστε, οι νέοι χρήστες δεν μπορούν να επισυνάψουν αρχεία." diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 3a2d83fe96..51eb70250f 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -162,6 +162,7 @@ es: ap_southeast_1: "Asia Pacific (Singapur)" ap_southeast_2: "Asia Pacific (Sydney)" cn_north_1: "China (Pekín)" + cn_northwest_1: "China (Ningxia)" eu_central_1: "UE (Frankfurt)" eu_west_1: "UE (Irlanda)" eu_west_2: "EU (Londres)" @@ -245,7 +246,9 @@ es: unbookmark: "Haz clic para quitar todos los marcadores de este tema" bookmarks: created: "has guardado este post en marcadores" + not_bookmarked: "guarda este post" remove: "Eliminar marcador" + confirm_clear: "¿Seguro que deseas borrar todos tus marcadores de este tema?" drafts: resume: "Resúmen" remove: "Eliminar" @@ -268,6 +271,8 @@ es: saved: "¡Guardado!" upload: "Subir" uploading: "Subiendo..." + uploading_filename: "Subiendo: {{filename}}..." + clipboard: "portapapeles" uploaded: "¡Subido!" pasting: "Pegando..." enable: "Activar" @@ -350,6 +355,7 @@ es: make_user_group_owner: "Convertir en dueño" remove_user_as_group_owner: "Quitar de dueño" groups: + member_added: "Agregado" add_members: title: "Añadir miembros" description: "Administrar la membresía de éste grupo" @@ -411,6 +417,7 @@ es: all: "Todos los Grupos" empty: "No hay grupos visibles." filter: "Filtrar por tipo de grupo" + owner_groups: "Grupos que poseo" close_groups: "Grupos Cerrados" automatic_groups: "Grupos Automáticos" automatic: "Automáticos" @@ -514,6 +521,7 @@ es: topic_stat_sentence: one: "%{count} tema nuevo en los últimos %{unit}." other: "%{count} temas nuevos en los últimos %{unit}." + n_more: "Categorías (%{count} más) ..." ip_lookup: title: Búsqueda de Direcciones IP hostname: Nombre del host @@ -529,7 +537,7 @@ es: topics_entered: "temas vistos" post_count: "# posts" confirm_delete_other_accounts: "¿Seguro que quieres eliminar estas cuentas?" - powered_by: "powered by ipinfo.io" + powered_by: "usando MaxMindDB" copied: "copiado" user_fields: none: "(selecciona una opción)" @@ -548,6 +556,7 @@ es: private_messages: "Mensajes" activity_stream: "Actividad" preferences: "Preferencias" + profile_hidden: "Este perfil público del usuario ha sido ocultado." expand_profile: "Expandir" collapse_profile: "Contraer" bookmarks: "Marcadores" @@ -614,7 +623,7 @@ es: watched_first_post_tags: "Vigilando Primer Post" watched_first_post_tags_instructions: "Se te notificará del primer post en cada nuevo tema con estas etiquetas." muted_categories: "Silenciado" - muted_categories_instructions: "No serás notificado de ningún tema en estas categorías, y no aparecerán en la página de mensajes recientes." + muted_categories_instructions: "No serás notificado de ningún tema en estas categorías, y no aparecerán en la página de categorías o mensajes recientes." no_category_access: "Como un moderador, tienes acceso limitado a categorías, guardar está deshabilitado." delete_account: "Borrar Mi Cuenta" delete_account_confirm: "¿Estás seguro que quieres borrar permanentemente tu cuenta? ¡Esta acción no puede ser revertida!" @@ -688,15 +697,16 @@ es: second_factor: title: "Autenticación Dos Factores" disable: "Inhabilitar Autenticación Dos Factores" - enable: "Hablita two factor authentication para mejorar la seguridad de la cuenta" + enable: "Habilita la autenticación en dos pasos para mejorar la seguridad de la cuenta" confirm_password_description: "Por favor confirma tu contraseña para continuar" label: "Código" + rate_limit: "Por favor, espera antes de volver a intentar otro código de autenticación." enable_description: | - Escanee el siguiente código QR en una aplicación soportada (AndroidiOSWindows Phone) e ingresa tu código de autenticación. + Escanee este código QR en una app soportada (AndroidiOS e ingrese su código de autenticación. disable_description: "Por favor ingrese el código de autenticación desde su aplicación" show_key_description: "Ingrese Manualmente" extended_description: | - La Autenticación de Dos Factores agrega un paso de seguridad adicional en tu cuenta al inicio de sesión al requerir un token de un solo uso además de su contraseña. Estos tokens pueden ser generados en dispositivos Android, iOS, y Windows Phone. + La verificación en dos pasos añade una capa extra de seguridad requiriendo un token por única vez además de tu contraseña. Los códigos (token) se generan en dispositivos Android e iOS oauth_enabled_warning: "Por favor ten en cuenta que los accesos a través de redes sociales serán inhabilitados si habilitas el factor de autenticación en dos pasos de tu cuenta." change_about: title: "Cambiar 'Acerca de mí'" @@ -716,7 +726,7 @@ es: title: "Cambiar tu imagen de perfil" gravatar: "Gravatar, basado en" gravatar_title: "Cambia tu avatar en la web de Gravatar" - gravatar_failed: "No se pudo obtener el Gravatar. ¿Hay alguno asociado a esa dirección de correo electrónico?" + gravatar_failed: "No pudimos encontrar tu Gravatar con esta dirección de correo." refresh_gravatar_title: "Actualizar tu Gravatar" letter_based: "Imagen de perfil asignada por el sistema" uploaded_avatar: "Foto personalizada" @@ -774,6 +784,18 @@ es: title: "Introduce de nuevo la contraseña" auth_tokens: title: "Dispositivos utilizados recientemente" + ip: "IP" + details: "Detalles" + log_out_all: "Cerrar sesión de todos" + active: "activo ahora" + not_you: "¿No eres tú?" + show_all: "Mostrar todos ({{count}})" + show_few: "Mostrar menos" + was_this_you: "¿Eras tú?" + was_this_you_description: "Si no eras tú, recomendamos que cambies tu contraseña y salgas de sesión de todo." + browser_and_device: "{{browser}} en {{device}}" + secure_account: "Asegurar mi cuenta" + latest_post: "Tu última publicación..." last_posted: "Último post" last_emailed: "Último Enviado por email" last_seen: "Visto por última vez" @@ -782,6 +804,7 @@ es: location: "Ubicación" website: "Sitio Web" email_settings: "E-mail" + hide_profile_and_presence: "Ocultar mi perfil público y características de presencia" like_notification_frequency: title: "Notificar cuando me dan Me gusta" always: "Con cada Me gusta que reciban mis posts" @@ -969,6 +992,9 @@ es: enabled: "Este sitio está en modo 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 esté en modo solo lectura." logout_disabled: "Cerrar sesión está desactivado mientras el sitio se encuentre en modo de sólo lectura." + too_few_topics_and_posts_notice: "¡Vamos a dar por comenzada la comunidad! Hay %{currentTopics} / %{requiredTopics} temas y %{currentPosts} / %{requiredPosts} posts. Los nuevos visitantes necesitan algo que leer y a lo que responder." + too_few_topics_notice: "¡Vamos a dar por comenzada la comunidad! Hay %{currentTopics} / %{requiredTopics} temas. Los nuevos visitantes necesitan algo que leer y a lo que responder." + too_few_posts_notice: "¡Vamos a dar por empezada la comunidad! Hay %{currentPosts} / %{requiredPosts} posts. Los nuevos visitantes necesitan algo que leer y a lo que responder." logs_error_rate_notice: reached: "%{relativeAge}%{rate} alcanzó el límite establecido en las opciones del sitio del %{siteSettingRate}." exceeded: "%{relativeAge}%{rate} excedió el límite establecido en las opciones del sitio del %{siteSettingRate}." @@ -1144,6 +1170,8 @@ es: categories_with_featured_topics: "Categorías y temas destacados" categories_and_latest_topics: "Categorías y temas recientes" categories_and_top_topics: "Categorías y Temas Top" + categories_boxes: "Cajas con Subcategorías" + categories_boxes_with_topics: "Cajas con temas destacados" shortcut_modifier_key: shift: 'Shift' ctrl: 'Ctrl' @@ -1220,6 +1248,7 @@ es: category_missing: "Debes escoger una categoría." tags_missing: "Debes seleccionar al menos {{count}} etiquetas" save_edit: "Guardar edición" + overwrite_edit: "Sobrescribir Edición" reply_original: "Responder en el Tema Original" reply_here: "Responder Aquí" reply: "Responder" @@ -1258,6 +1287,7 @@ es: link_description: "introduzca descripción del enlace aquí" link_dialog_title: "Insertar Enlace" link_optional_text: "título opcional" + link_url_placeholder: "https://ejemplo.com" quote_title: "Cita" quote_text: "Cita" code_title: "Texto preformateado" @@ -1272,6 +1302,8 @@ es: help: "Ayuda de Edición con Markdown" collapse: "minimizar el panel de edición" abandon: "cerrar el editor y descartar borrador" + enter_fullscreen: "ingresar al editor en pantalla completa" + exit_fullscreen: "salir del editor en pantalla completa" modal_ok: "OK" modal_cancel: "Cancelar" cant_send_pm: "Lo sentimos, no puedes enviar un mensaje a %{username}." @@ -1305,6 +1337,7 @@ es: desc: "Haz borrador al tema que será visible únicamente por el staff" toggle_topic_bump: label: "Alternar BUMP del tema" + desc: "Responder sin alterar la fecha de última respuesta" notifications: tooltip: regular: @@ -1795,9 +1828,13 @@ es: action: "unir posts seleccionados" error: "Hubo un error al unir los posts seleccionados." change_owner: + title: "Cambiar Dueño" action: "cambiar dueño" error: "Hubo un error cambiando la autoría de los posts." placeholder: "nombre de usuario del nuevo dueño" + instructions: + one: "Por favor escoge el nuevo dueño del post de @{{old_user}}" + other: "Por favor escoge el nuevo dueño de los {{count}} posts de @{{old_user}}" change_timestamp: title: "Cambiar Timestamp..." action: "cambiar timestamp" @@ -1868,7 +1905,7 @@ es: upload: "Lo sentimos, hubo un error al subir el archivo. Por favor, inténtalo de nuevo." file_too_large: "Lo sentimos, ese archivo es demasiado grande (el tamaño máximo es {{max_size_kb}}kb). ¿Quizá podrías subir el archivo a un servicio de almacenamiento en la nube y compartir aquí el enlace?" too_many_uploads: "Lo siento solo puedes subir un archivo cada vez." - too_many_dragged_and_dropped_files: "Lo sentimos, sólo puedes subir 10 archivos a la vez." + too_many_dragged_and_dropped_files: "Lo sentimos, sólo puedes subir {{max}} archivos a la vez." upload_not_authorized: "Lo sentimos, el archivo que estás intentando subir no está permitido (extensiones autorizadas: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Lo siento, usuarios nuevos no pueden subir imágenes." attachment_upload_not_allowed_for_new_user: "Lo siento, usuarios nuevos no pueden subir archivos adjuntos." @@ -2321,6 +2358,7 @@ es: this_week: "Semana" today: "Hoy" other_periods: "ver temas top" + browser_update: 'Desafortunadamente, tu navegador es demasiado antiguo para funcionar en este sitio. Por favor actualiza tu navegador.' permission_types: full: "Crear / Responder / Ver" create_post: "Responder / Ver" @@ -2363,6 +2401,7 @@ es: composing: title: 'Redactando' return: 'shift+c Regresar al editor' + fullscreen: 'shift+F11 Fullscreen Editor' actions: title: 'Acciones' bookmark_topic: 'f Guardar/Quitar el tema de marcadores' @@ -2444,6 +2483,10 @@ es: sort_by_name: "nombre" manage_groups: "Administrar grupos de etiquetas" manage_groups_description: "Definir grupos para organizar etiquetas" + upload: "Subir Etiquetas" + upload_description: "Subir un archivo de texto para crear etiquetas de forma masiva" + upload_instructions: "Una por línea, opcional con un grupo de etiquetas en el formato 'tag_name,tag_group'." + upload_successful: "Etiquetas subidas exitosamente" filters: without_category: "%{filter} %{tag} temas" with_category: "%{filter} %{tag} temas en %{category}" @@ -2503,6 +2546,7 @@ es: bookmarks: "No hay más temas guardados en marcadores." search: "No hay más resultados de búsqueda." invite: + custom_message: "Dale a tu invitación un toque personal escribiendo un mensaje personalizado." custom_message_placeholder: "Introducir un mensaje personalizado" custom_message_template_forum: "Hey, ¡quizá deberías unirte a este foro!" custom_message_template_topic: "¡Hey, he pensado que este tema te va a encantar!" @@ -2649,6 +2693,19 @@ es: was_edited: "El post fue editado después del primer reporte" previous_flags_count: "Este post ya fue marcado {{count}} veces." show_details: "Mostrar detalles del reporte" + user_percentage: + summary: + one: "{{agreed}}, {{disagreed}}, {{ignored}} ({{count}} reporte en total)" + other: "{{agreed}}, {{disagreed}}, {{ignored}} ({{count}} reportes totales)" + agreed: + one: "{{count}}% de acuerdo" + other: "{{count}}% de acuerdo" + disagreed: + one: "{{count}}% en desacuerdo" + other: "{{count}}% en desacuerdo" + ignored: + one: "{{count}}% ignorado" + other: "{{count}}% ignorado" details: "detalles" flagged_topics: topic: "Tema" @@ -2855,6 +2912,7 @@ es: label: "Subir" title: "Subir un backup a esta instancia" uploading: "Subiendo..." + uploading_progress: "Subiendo... {{progress}}%" success: "'{{filename}}' ha sido exitosamente subido. El archivo se procesará ahora y tomará hasta un minuto para ser mostrado en la lista." error: "Ha ocurrido un error al subir el archivo '{{filename}}': {{message}}" operations: @@ -2885,6 +2943,9 @@ es: label: "Revertir" title: "Regresar la base de datos al estado funcional anterior" confirm: "¿Seguro que quieres retornar la base de datos al estado funcional previo?" + location: + local: "Local" + s3: "Amazon S3" export_csv: success: "Exportación iniciada, se te notificará a través de un mensaje cuando el proceso se haya completado." failed: "Exportación fallida, revisa los logs." @@ -2930,12 +2991,19 @@ es: theme: "Tema" component: "Componente" components: "Componentes" + theme_name: "Nombre del Theme" + component_name: "Nombre del Componente" + themes_intro: "Selecciona un theme existente o crear nuevo para empezar" + beginners_guide_title: "Guía para novatos usando Discourse Themes" + developers_guide_title: "Guía para desarrolladores para Discourse Themes" + browse_themes: "Navegar themes de la comunidad" import_theme: "Importar Theme" customize_desc: "Personalizar:" title: "Themes" create: "Crear" create_type: "Tipo:" create_name: "Nombre:" + name_too_short: "El nombre debe tener al menos 4 caracteres de largo." long_title: "Modifique los colores, los CSS y los contenidos HTML de su sitio" edit: "Editar" edit_confirm: "Este es un theme remoto, si editas CSS/HTML, los cambios serán borrados en la próxima actualización al theme." @@ -2950,6 +3018,17 @@ es: color_scheme_select: "Selecciona colores para ser usados en el theme" custom_sections: "Personalizaciones:" theme_components: "Componentes del Theme" + convert: "Convertir" + convert_component_alert: "¿Estás seguro que quieres convertir este componente en theme? Será eliminado como componente desde %{relatives}." + convert_component_tooltip: "Convertir este componente en theme" + convert_theme_alert: "¿Estás seguro que quieres convertir este theme en componente? Será eliminado como principal desde %{relatives}." + convert_theme_tooltip: "Convertir este theme en componente" + inactive_themes: "Themes inactivos:" + broken_theme_tooltip: "Este theme tiene errores en su CSS, HTML o YAML" + default_theme_tooltip: "Este theme es el theme por defecto del sitio" + updates_available_tooltip: "Hay actualizaciones disponibles para este theme" + and_x_more: "y {{count}} más." + collapse: Colapsar uploads: "Subidos" no_uploads: "Puedes subir archivos asociados con tu theme como fuentes e imágenes " add_upload: "Agregar Subido" @@ -2961,6 +3040,10 @@ es: no_overwrite: "Nombre de variable inválido. No debe sobrescribir una variable existente. " must_be_unique: "Nombre de variable inválido. Debe ser único." upload: "Subir" + select_component: "Seleccionar un componente..." + unsaved_changes_alert: "No has guardado los cambios aún, ¿los quieres deshacer y seguir adelante?" + discard: "Deshacer" + stay: "Permanecer" css_html: "Personalizar CSS/HTML" edit_css_html: "Editar CSS/HTML" edit_css_html_help: "No has editado ningún CSS o HTML" @@ -2968,6 +3051,7 @@ es: import_web_tip: "Repositorio que contiene el theme" import_file_tip: "archivo .dcstyle.json que contiene el theme" is_private: "El theme está en un repositorio privado de git" + remote_branch: "Nombre del Branch (opcional)" public_key: "Conceda la siguiente clave pública de acceso para el repositorio:" about_theme: "Acerca del Theme" license: "Licencia" @@ -2984,6 +3068,7 @@ es: one: "Theme está 1 commit detrás!" other: "Theme está {{count}} commits detrás!" compare_commits: "(Ver nuevos commits)" + repo_unreachable: "No se pudo contactar con el repositorio Git de este theme. Mensaje de error:" scss: text: "CSS" title: "Ingresa tu CSS, aceptamos estilos válidos de CSS y SCSS" @@ -3145,6 +3230,7 @@ es: filter: "Filtrar:" title: "Acciones del staff" clear_filters: "Mostrar todo" + staff_user: "Usuario" target_user: "Usuario enfocado" subject: "Sujeto" when: "Cuándo" @@ -3214,6 +3300,7 @@ es: change_badge: "cambiar distintivo" delete_badge: "borrar distintivo" merge_user: "unir usuario" + entity_export: "entidad exportadora" screened_emails: title: "Correos bloqueados" description: "Cuando alguien trata de crear una cuenta nueva, los siguientes correos serán revisados y el registro será bloqueado, o alguna otra acción será realizada." diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index e88d342554..1990dfc166 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -156,6 +156,7 @@ et: eu_central_1: "EL (Frankfurt)" eu_west_1: "EL (Iirimaa)" eu_west_2: "EU (London)" + eu_west_3: "EL (Pariis)" sa_east_1: "Lõuna-Ameerika (Sao Paulo)" us_east_1: "US Ida (N. Virginia)" us_east_2: "US Ida (Ohio)" @@ -186,6 +187,7 @@ et: privacy_policy: "Privaatsuspoliitika" privacy: "Privaatsus" tos: "Teenuse tingimused" + rules: "Reeglid" mobile_view: "Mobiilne vaade" desktop_view: "Täisvaade" you: "Sina" @@ -217,6 +219,8 @@ et: our_moderators: "Meie moderaatorid" stat: all_time: "Alates algusest" + last_7_days: "Viimased 7" + last_30_days: "Viimased 30" like_count: "Meeldimisi" topic_count: "Teemad" post_count: "Postitused" @@ -232,7 +236,13 @@ et: unbookmark: "Kliki selle teema kõigi järjehoidjate eemaldamiseks" bookmarks: created: "lisasid sellele postitusele järjehoidja" + not_bookmarked: "lisa sellele postitusele järjehoidja" remove: "Eemalda järjehoidja" + drafts: + resume: "Taasta" + remove: "Eemalda" + new_topic: "Uus teema mustand" + topic_reply: "Mustandi vastus" preview: "eelvaade" cancel: "tühista" save: "Salvesta muudatused" @@ -240,10 +250,12 @@ et: saved: "Salvestatud!" upload: "Lae üles" uploading: "Laen üles..." + clipboard: "lõikelaud" uploaded: "Üles laetud!" pasting: "Asetamine..." enable: "Võimalda" disable: "Tõkesta" + continue: "Jätka" undo: "Ennista" revert: "Võta tagasi" failed: "Ebaõnnestus" @@ -321,6 +333,32 @@ et: make_user_group_owner: "Määra omanikuks" remove_user_as_group_owner: "Eemalda omanik" groups: + add_members: + title: "Lisa liikmeid" + usernames: "Kasutajanimed" + manage: + title: 'Halda' + name: 'Nimi' + full_name: 'Täisnimi' + add_members: "Lisa liikmeid" + profile: + title: Profiil + interaction: + posting: Postitamine + notification: Teavitus + membership: + title: Liikmelisus + access: Juurdepääs + logs: + title: "Logid" + when: "Millal" + action: "Tegevus" + acting_user: "Tegev kasutaja" + target_user: "Sihtkasutaja" + subject: "Teema" + details: "Detailid" + from: "Kellelt" + to: "Kellele" public_exit: "Luba kasutajatel vabalt grupist lahkuda" empty: posts: "Selle grupi liikmetelt ei ole postitusi." @@ -330,21 +368,46 @@ et: topics: "Selle grupi liikmetelt ei ole teemasid." logs: "Sellele grupile logid puuduvad." add: "Lisa" + join: "Liitu" + leave: "Lahku" + request: "Päring" message: "Sõnum" membership_request: submit: "Saada päring" membership: "Liikmelisus" name: "Nimi" + group_name: "Grupi nimi" + user_count: "Kasutajad" bio: "Grupist" selector_placeholder: "sisesta kasutajanimi" owner: "omanik" index: title: "Grupid" empty: "Ühtegi nähtavat gruppi pole." + automatic_groups: "Automaatsed grupid" + automatic: "Automaatne" + closed: "Suletud" + public: "Avalik" + private: "Privaatne" + public_groups: "Avalikud grupid" + automatic_group: Automaatne grupp + close_group: Sulge grupp + my_groups: "Minu grupid" + group_type: "Grupi liik" + is_group_user: "Liige" + is_group_owner: "Omanik" title: one: "Grupp" other: "Grupid" activity: "Tegevused" + members: + title: "Liikmed" + filter_placeholder_admin: "kasutajanimi või e-post" + filter_placeholder: "kasutajanimi" + remove_member: "Eemalda liige" + make_owner: "Määra omanikuks" + remove_owner: "Eemalda omanik" + owner: "Omanik" topics: "Teemat" posts: "Postitused" mentions: "Mainimisi" @@ -393,8 +456,10 @@ et: "12": "Saatmisi" "13": "Postkast" "14": "Ootel" + "15": "Mustandid" categories: all: "kõik foorumid" + all_subcategories: "kõik" no_subcategory: "mitte ükski" category: "Foorum" category_list: "Kuva foorumid" @@ -431,6 +496,7 @@ et: topics_entered: "külastatud teemasid" post_count: "# postitust" confirm_delete_other_accounts: "Kindel, et soovid need kontod kustutada?" + copied: "kopeeritud" user_fields: none: "(vali võimalus)" user: @@ -449,6 +515,7 @@ et: activity_stream: "Tegevused" preferences: "Eelistused" expand_profile: "Näita veel" + collapse_profile: "Ahenda" bookmarks: "Järjehoidjad" bio: "Minust" invited_by: "Kasutaja, kes kutsus" @@ -510,7 +577,6 @@ et: watched_first_post_tags: "Vaatan esimest postitust" watched_first_post_tags_instructions: "Sind teavitatakse esimesest postitusest igas nende siltidega uues teemas." muted_categories: "Vaigistatud" - muted_categories_instructions: "Sind ei teavitata ühestki uuest teemast nendes foorumites, samuti ei ilmu nad viimaste teemade alla." delete_account: "Kustuta minu konto" delete_account_confirm: "Kas oled kindel, et soovid oma konto jäädavalt kustutada? Seda toimingut ei ole võimalik tagasi võtta!" deleted_yourself: "Konto on edukalt kustutatud." @@ -564,6 +630,13 @@ et: set_password: "Määra parool" choose_new: "Vali uus parool" choose: "Vali parool" + second_factor_backup: + regenerate: "Genereeri uus" + disable: "Keela" + enable: "Luba" + second_factor: + label: "Kood" + show_key_description: "Sisesta käsitsi" change_about: title: "Muuda minu andmeid" error: "Välja muutmisel tekkis viga." @@ -595,6 +668,9 @@ et: instructions: "Taustapildidd tsentreeritakse ja on vaikimisi 590 pikslit laiad." email: title: "Meiliaadress" + primary: "Peamine e-post" + secondary: "Teine e-post" + no_secondary: "Teist e-posti pole" instructions: "ei näidata kunagi avalikult" ok: "Saadame sulle kinnituseks meili" invalid: "Sisesta palun korrektne meiliaadress" @@ -603,6 +679,11 @@ et: frequency: one: "Saadame meili vaid siis, kui pole sind viimase minuti jooksul näinud." other: "Saadame meili vaid siis, kui pole Sind viimase {{count}} minuti jooksul näinud." + associated_accounts: + title: "Seotud kontod" + connect: "Ühenda" + revoke: "Tühista" + not_connected: "(pole ühendatud)" name: title: "Nimi" instructions: "sinu täisnimi (mittekohustuslik)" @@ -627,6 +708,16 @@ et: any: "iga" password_confirmation: title: "Salasõna uuesti" + auth_tokens: + title: "Hiljuti kasutatud seadmed" + ip: "IP" + details: "Üksikasjad" + log_out_all: "Logi kõik välja" + active: "praegu aktiivne" + not_you: "See pole sina?" + show_all: "Näita kõiki ({{count}})" + show_few: "Näita vähem" + was_this_you: "Kas see olid sina?" last_posted: "Viimane postitus" last_emailed: "Viimati meilitud" last_seen: "Vaadatud" @@ -647,7 +738,6 @@ et: always: "alati" never: "mitte kunagi" email_digests: - title: "Kui ma siinset paika ei külasta, saada mulle kokkuvõtlik meil populaarsemate teemade ja vastustega." every_30_minutes: "iga pooltund" every_hour: "iga tund" daily: "igapäevaselt" @@ -734,6 +824,7 @@ et: title: "Kokkuvõte" stats: "Statistika" time_read: "lugemise aeg" + recent_time_read: "viimane lugemise aeg" topic_count: one: "teema loodud" other: "teemat loodud" @@ -749,6 +840,9 @@ et: days_visited: one: "päev külastatud" other: "päevi külastatud" + topics_entered: + one: "vaadatud teema" + other: "vaadatud teemat" posts_read: one: "postitus loetud" other: "postitust loetud" @@ -770,6 +864,8 @@ et: most_liked_users: "Enim meeldinud" most_replied_to_users: "Enim vastatud teemasse" no_likes: "Meeldimisi veel pole." + topics: "Teemad" + replies: "Vastused" ip_address: title: "Viimane IP-aadress" registration_ip_address: @@ -779,6 +875,7 @@ et: header_title: "profiil, sõnumid, järjehoidjad ja eelistused" title: title: "Pealkiri" + none: "(pole)" filters: all: "Kõik" stream: @@ -834,6 +931,7 @@ et: mute: Vaigista unmute: Tühista vaigistus last_post: Postitatud + time_read: Loe last_reply_lowercase: viimane vastus replies_lowercase: one: vastus @@ -856,6 +954,8 @@ et: disable: "Näita kustutatud postitusi" private_message_info: title: "Sõnum" + invite: "Kutsu teisi..." + edit: "Lisa või eemalda..." remove_allowed_user: "Kas soovid tõesti {{name}} sellest sõnumist eemaldada?" remove_allowed_group: "Kas soovid tõesti {{name}} sellest sõnumist eemaldada?" email: 'Meiliaadress' @@ -909,21 +1009,27 @@ et: forgot: "Ma ei mäleta oma konto üksikasju" not_approved: "Kontot pole veel kinnitatud. Teid teavitatakse e-post teel, kui sisselogimine on võimaldatud." google_oauth2: + name: "Google" title: "Google abil" message: "Autentimine Google abil (veendu, et hüpikaknad oleks lubatud)" twitter: + name: "Twitter" title: "Twitteri abil" message: "Autentimine Twitteri abil (veendu, et hüpikaknad oleks lubatud)" instagram: + name: "Instagram" title: "Instagram'iga" message: "Autentimine Instagram'i kaudu (vaata, et hüpikaknad ei oleks keelatud)" facebook: + name: "Facebook" title: "Facebooki abil" message: "Autentimine Facebooki abil (veendu, et hüpikaknad oleks lubatud)" yahoo: + name: "Yahoo" title: "Yahoo abil" message: "Autentimine Yahoo abil (veendu, et hüpikaknad oleks lubatud)" github: + name: "GitHub" title: "GitHub abil" message: "Autentimine GitHub abil (veendu, et hüpikaknad oleks lubatud)" invites: @@ -955,10 +1061,13 @@ et: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + conditional_loading_section: + loading: Laadimine... select_kit: default_header_text: Vali... no_content: Midagi ei leitud filter_placeholder: Otsi... + filter_placeholder_with_any: Otsi või loo... emoji_picker: filter_placeholder: Otsi emojit people: Inimesed @@ -1041,6 +1150,7 @@ et: link_description: "sisesta viite kirjeldus siia" link_dialog_title: "Lisa hüperlink" link_optional_text: "valikuline pealkiri" + link_url_placeholder: "https://example.com" quote_title: "Plokktsitaat" quote_text: "Plokktsitaat" code_title: "Eelvormindatud tekst" @@ -1059,6 +1169,18 @@ et: title: "Kas unustasid saajad lisada?" body: "Hetkel saadetakse see sõnum vaid sulle endale!" admin_options_title: "Meeskonna valikulised sätted selle teema jaoks" + composer_actions: + reply: Vasta + draft: Mustand + edit: Muuda + reply_as_private_message: + label: Uus sõnum + reply_to_topic: + label: Vasta teemale + create_topic: + label: "Uus teema" + shared_draft: + label: "Jagatud mustand" notifications: tooltip: regular: @@ -1237,6 +1359,7 @@ et: other: "{{count}} postitust teemas" create: 'Uus teema' create_long: 'Loo uus teema' + open_draft: "Ava mustand" private_message: 'Alusta sõnumit' archive_message: help: 'Liiguta sõnum oma arhiivi' @@ -1244,6 +1367,8 @@ et: move_to_inbox: title: 'Liiguta Postkasti' help: 'Liiguta sõnum tagasi Postkasti' + edit_message: + title: 'Muuda sõnumit' list: 'Teemad' new: 'uus teema' unread: 'lugemata' @@ -1312,6 +1437,18 @@ et: one_year: "Aasta pärast" forever: "Igavesti" pick_date_and_time: "Vali kuupäev ja kellaaeg" + publish_to_category: + title: "Ajastatud avaldamine" + temp_open: + title: "Ava ajutiselt" + auto_reopen: + title: "Loo teema automaatselt" + temp_close: + title: "Sulge ajutiselt" + auto_close: + title: "Sulge teema automaatselt" + auto_delete: + title: "Kustuta teema automaatselt" reminder: title: "Tuleta mulle meelde" auto_close_title: 'Automaatse sulgumise sätted' @@ -1379,6 +1516,7 @@ et: open: "Ava teema" close: "Sulge teema" multi_select: "Vali postitused..." + timed_update: "Määra teema taimer..." pin: "Tõsta teema esile..." unpin: "Eemalda teema esiletõstmine..." unarchive: "Taasta teema arhiivist" @@ -1525,6 +1663,7 @@ et: deleted_by_author: one: "(autori poolt tagasivõetud postitus kustutatakse automaatselt %{count} tunni pärast, kui ei ole tähistatud)" other: "(autori poolt tagasivõetud postitus kustutatakse automaatselt %{count} tunni pärast, kui ei ole tähistatud)" + collapse: "ahenda" expand_collapse: "laienda/ahenda" gap: one: "vaata 1 peidetud vastust" @@ -1549,7 +1688,6 @@ et: upload: "Vabandame, selle faili üleslaadimisel tekkis viga. Palun proovi uuesti." file_too_large: "Vabandame. see fail on liiga suur (maksimum on {{max_size_kb}}kB). Miks mitte laadida see suur fail mõnda failijagamisteenusesse pilves ja jagada viidet selleni?" too_many_uploads: "Vabandame, faile saab üles laadida vaid ühekaupa." - too_many_dragged_and_dropped_files: "Vabandame, faile saab üles laadida vaid kuni 10 korraga." upload_not_authorized: "Vabandust, fail mida püüad üles laadida, ei ole lubatud (lubatud faililaiendid: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Vabandame, uued kasutajad ei saa pilte üles laadida." attachment_upload_not_allowed_for_new_user: "Vabandame, uued kasutajad ei saa manuseid üles laadida." @@ -1589,6 +1727,7 @@ et: rebake: "Rekonstrueeri HTML" unhide: "Too nähtavale" change_owner: "Omanikuvahetus" + lock_post: "Lukusta postitus" actions: flag: 'Tähis' undo: @@ -2151,9 +2290,15 @@ et: page_views: "Vaatamisi" page_views_short: "Vaatamisi" show_traffic_report: "Näita liikluse detailraportit" + all_reports: "Kõik aruanded" + general_tab: "Üldine" + moderation_tab: "Modereerimine" + disabled: Välja lülitatud reports: today: "Täna" yesterday: "Eile" + last_7_days: "Viimased 7" + last_30_days: "Viimased 30" all_time: "Alates algusest" 7_days_ago: "7 päeva tagasi" 30_days_ago: "30 päeva tagasi" @@ -2164,6 +2309,7 @@ et: start_date: "Alguskuupäev" end_date: "Lõpukuupäev" groups: "Kõik grupid" + no_data: "Pole andmeid, mida näidata." commits: latest_changes: "Viimased muudatused: palun värskenda tihti!" by: "autor" @@ -2173,6 +2319,9 @@ et: old_posts: "Vanad tähistatud postitused" agree: "Nõustun" agree_title: "Kinnita see tähis kui korrektne ja kehtiv" + agree_flag_hide_post: "Peida postitus" + agree_flag_silence: "Vaigista kasutaja" + ignore_flag: "Ignoreeri" delete: "Kustuta" delete_title: "Kustuta postitus, millele see tähis viitab." delete_post_defer_flag_title: "Kustuta postitus; kui on ainus, kustuta teema" @@ -2213,6 +2362,29 @@ et: notify_user: "kohandatud" notify_moderators: "kohandatud" groups: + new: + title: "Uus grupp" + create: "Loo" + name: + too_short: "Grupi nimi on liiga lühike" + too_long: "Grupi nimi on liiga pikk" + available: "Grupi nimi on saadaval" + not_available: "Grupi nimi pole saadaval" + blank: "Grupi nimi ei saa olla tühi" + manage: + interaction: + email: E-post + visibility: Nähtavus + visibility_levels: + title: "Kes näevad seda gruppi?" + public: "Kõik" + members: "Grupi omanikud, liikmed ja adminid" + staff: "Grupi omanikud ja personal" + owners: "Grupi omanikud ja adminid" + membership: + automatic: Automaatne + trust_level: Usalduse tase + trust_levels_none: "Pole" primary: "Peamine grupp" no_primary: "(peamine grupp puudub)" title: "Grupid" @@ -2374,6 +2546,9 @@ et: label: "Pööra tagasi" title: "Pööra andmebaas tagasi viimati toiminud olekusse" confirm: "Oled kindel, et soovid andmebaasi tagasi viimati toiminud olekusse pöörata?" + location: + local: "Kohalik" + s3: "Amazon S3" export_csv: success: "Eksportimine käivitatud. Protsessi lõppemisel saadetakse Sulle teade." failed: "Eksportimine ebaõnnestus. Kontrolli logisid." @@ -2413,20 +2588,32 @@ et: revert: "Loobu muudatustest" revert_confirm: "Oled kindel, et soovid oma muudatustest loobuda?" theme: + theme: "Teema" + component: "Komponent" + components: "Komponendid" + theme_name: "Teema nimi" + component_name: "Komponendi nimi" import_theme: "Impordi teema" customize_desc: "Kohanda:" title: "Kujundused" + create: "Loo" + create_type: "Liik:" + create_name: "Nimi:" edit: "Muuda" common: "Tavaline" desktop: "Töölaud" mobile: "Mobiil" + settings: "Seaded" preview: "Eelvaade" is_default: "Kujundus on vaikimisi sisse lülitatud" color_scheme: "Värvid" theme_components: "Kujunduse osad" + convert: "Konverdi" + collapse: Ahenda uploads: "Üleslaadimised" add_upload: "Lisa üleslaadimine" upload: "Laadi üles" + select_component: "Vali komponent..." about_theme: "Kujunduse info" license: "Litsents" updating: "Uuendamine..." diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 0e3692727a..c05d36fd01 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -591,7 +591,6 @@ fa_IR: watched_first_post_tags: "درحال مشاهده نوشته اول" watched_first_post_tags_instructions: "از اولین نوشته هر موضوع که شامل این برچسب‌ها باشد مطلع خواهید شد." muted_categories: "بی‌صدا شد" - muted_categories_instructions: "شما از اتفاقات جدید در این دسته‌بندی‌ها آگاه نمیشوید، و آن ها در آخرین ها نمایش داده نمیشوند." delete_account: "حساب‌کاربری من را پاک کن" delete_account_confirm: "آیا مطمئنید که می‌خواهید شناسه‌تان را برای همیشه پاک کنید؟ برگشتی در کار نیست!" deleted_yourself: "حساب‌ کاربری شما با موفقیت حذف شد." @@ -648,7 +647,6 @@ fa_IR: second_factor: title: "احراز هویت دو مرحله ای" disable: "از کار انداختن احراز هویت دو مرحله ای" - enable: "برای بهبود امنیت حساب کاربری احراز هویت دو مرحله ای را فعال کنید" change_about: title: "تغییر «درباره‌ی من»" error: "در فرآیند تغییر این مقدار خطایی روی داد." @@ -732,7 +730,6 @@ fa_IR: always: "همیشه" never: "هیچوقت" email_digests: - title: "وقت‌هایی که در انجمن نیستم خلاصه موضوعات و پاسخ‌های پر‌طرفدار را برایم بفرست." every_30_minutes: "هر 30 دقیقه" every_hour: "هر ساعت" daily: "روزانه" @@ -1607,7 +1604,6 @@ fa_IR: upload: "متأسفیم، در بارگذاری آن پرونده خطایی روی داد. لطفاً دوباره تلاش کنید." file_too_large: "با عرض پوزش، حجم فایل بسیار بالاست (بالاترین حجم قابل بارگذاری {{max_size_kb}} کیلوبایت) است. چرا فایل‌های حجیم را در سرویس‌های ابری بارگذاری نمی‌کنید و لینک‌های آن را در انجمن قرار نمی‌دهید؟" too_many_uploads: "متأسفیم، هر بار تنها می‌توانید یک پرونده را بار‌گذاری کنید." - too_many_dragged_and_dropped_files: "با عرض پوزش، شما فقط می‌توانید 10 فایل به صورت یک‌جا بارگذاری کنید." upload_not_authorized: "با عرض پوزش، فایلی که در حال‌ بارگذاری آن هستید مجاز نیست. (پسوند‌های قابل بارگذاری: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "با عرض پوزش، کاربران جدید نمی توانند تصویر بار‌گذاری کنند." attachment_upload_not_allowed_for_new_user: "با عرض پوزش، کاربران جدید نمی توانند فایل پیوست بار‌گذاری کنند." diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index ef3afcf142..1059853ae7 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -246,7 +246,9 @@ fi: unbookmark: "Klikkaa poistaaksesi kaikki tämän ketjun kirjanmerkit" bookmarks: created: "olet lisännyt tämän viestin kirjanmerkkeihisi" + not_bookmarked: "lisää viesti kirjanmerkkeihin" remove: "Poista kirjanmerkki" + confirm_clear: "Haluatko varmasti poistaa kaikki kirjanmerkkisi tästä ketjusta?" drafts: resume: "Jatka" remove: "Poista" @@ -269,6 +271,8 @@ fi: saved: "Tallennettu!" upload: "Liitä" uploading: "Lähettää..." + uploading_filename: "Ladataan: {{filename}}" + clipboard: "leikepöytä" uploaded: "Lähetetty!" pasting: "Liitetään..." enable: "Ota käyttöön" @@ -412,6 +416,7 @@ fi: all: "Kaikki ryhmät" empty: "Näkyvillä olevia ryhmiä ei ole." filter: "Suodata ryhmän tyypin mukaan" + owner_groups: "Ryhmät joita isännöin" close_groups: "Suljetut ryhmät" automatic_groups: "Automaattiset ryhmät" automatic: "Automaattinen" @@ -515,6 +520,7 @@ fi: topic_stat_sentence: one: "%{count} uusi ketju viimeisen %{unit} aikana." other: "%{count} uutta ketjua viimeisen %{unit} aikana." + n_more: "Alueet (%{count} muuta)..." ip_lookup: title: IP-osoitteen haku hostname: Isäntänimi @@ -530,7 +536,6 @@ fi: topics_entered: "katseltuja ketjuja" post_count: "# viestiä" confirm_delete_other_accounts: "Oletko varma, että haluat poistaa nämä tunnukset?" - powered_by: "voimanlähteenä 1ipinfo.io" copied: "kopioitu" user_fields: none: "(valitse vaihtoehto)" @@ -549,6 +554,7 @@ fi: private_messages: "Viestit" activity_stream: "Toiminta" preferences: "Asetukset" + profile_hidden: "Käyttäjän julkinen profiili ei ole nähtävillä." expand_profile: "Laajenna" collapse_profile: "Supista" bookmarks: "Kirjanmerkit" @@ -616,7 +622,6 @@ fi: watched_first_post_tags: "Tarkkaillaan uusia ketjuja" watched_first_post_tags_instructions: "Saat ilmoituksen uusista ketjuista, joilla on joku näistä tunnisteista." muted_categories: "Vaimennetut" - muted_categories_instructions: "Et saa ilmoituksia uusista viesteistä näillä alueilla, eivätkä ne näy tuoreimmissa." no_category_access: "Valvojana sinulla on rajoitetut oikeudet alueisiin, et voi tallentaa." delete_account: "Poista tilini" delete_account_confirm: "Oletko varma, että haluat lopullisesti poistaa käyttäjätilisi? Tätä toimintoa ei voi perua!" @@ -680,7 +685,7 @@ fi: disable: "Poista käytöstä" enable: "Ota käyttöön" enable_long: "Ota varakoodit käyttöön" - manage: "Hallinntoi varakoodeja" + manage: "Hallinnoi varakoodeja" copied_to_clipboard: "Kopioitiin leikepöydälle" copy_to_clipboard_error: "Virhe kopioimisessa leikepöydälle" remaining_codes: "Sinulla on {{count}} varakoodia jäljellä." @@ -690,15 +695,15 @@ fi: second_factor: title: "Kaksivaiheinen tunnistautuminen" disable: "Poista kaksivaiheinen tunnistautuminen käytöstä" - enable: "Ota käyttöön kaksivaiheinen tunnistautuminen tietoturvan parantamiseksi" confirm_password_description: "Jatka vahvistamalla salasanasi" label: "Koodi" + rate_limit: "Odota hetki ennen toisen todennuskoodin tarjoamista." enable_description: | - Skannaa QR-koodi laitteellesi sopivalla sovelluksella (AndroidiOSWindows Phone) ja syötä tunnistautumiskoodi. + Skannaa QR-koodi laitteellesi sopivalla sovelluksella (AndroidiOS ja syötä todennuskoodi. disable_description: "Syötä sovelluksen tarjoama tunnistautumiskoodi" show_key_description: "Syötä manuaalisesti" extended_description: | - Kaksivaiheinen tunnistautuminen lisää tilisi tietoturvaa vaatimalla kertakäyttöisen koodin salasanan lisäksi. Koodeja voi luoda Android, iOS ja Windows Phone -laitteilla. + Kaksivaiheinen tunnistautuminen lisää tilisi tietoturvaa vaatimalla kertakäyttöisen koodin salasanan lisäksi. Koodeja voi luoda Android- ja iOS-laitteilla. oauth_enabled_warning: "Huomioi, ettet voi kirjautua some-tilien avulla, jos kaksivaiheinen tunnistautuminen on käytössä." change_about: title: "Muokkaa kuvaustasi" @@ -718,7 +723,6 @@ fi: title: "Vaihda profiilikuvasi" gravatar: "Gravatar, osoitteesta" gravatar_title: "Vaihda profiilikuvasi Gravatar-sivustolla" - gravatar_failed: "Gravataria ei löytynyt. Liittyykö tähän sähköpostiosoitteeseen sellainen?" refresh_gravatar_title: "Päivitä Gravatar" letter_based: "Sivuston luoma profiilikuva" uploaded_avatar: "Oma kuva" @@ -776,6 +780,15 @@ fi: title: "Salasana uudelleen" auth_tokens: title: "Viimeksi käytetyt laitteet" + ip: "IP" + details: "Yksityiskohdat" + log_out_all: "Kirjaudu ulos kaikkialta" + active: "aktiivinen nyt" + not_you: "Et ole sinä?" + show_all: "Näytä kaikki ({{count}})" + show_few: "Näytä vähemmän" + was_this_you: "Olitko sinä?" + browser_and_device: "{{browser}} laitteella {{device}}" last_posted: "Viimeisin viesti" last_emailed: "Viimeksi lähetetty sähköpostitse" last_seen: "Nähty" @@ -784,6 +797,7 @@ fi: location: "Sijainti" website: "Nettisivu" email_settings: "Sähköposti" + hide_profile_and_presence: "Piilota julkinen profiilini ja läsnäolo-ominaisuudet" like_notification_frequency: title: "Ilmoita, kun viestistäni tykätään" always: "Aina" @@ -796,7 +810,6 @@ fi: always: "aina" never: "ei koskaan" email_digests: - title: "Jos en käy sivustolla, lähetä minulle kooste suosituista ketjuista ja vastauksista" every_30_minutes: "puolen tunnin välein" every_hour: "tunneittain" daily: "päivittäin" @@ -971,6 +984,9 @@ 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} / %{requiredTopics} ketjua ja %{currentPosts} / %{requiredPosts} viestiä. Uusia kävijöitä varten tarvitaan keskusteluita, joita voivat lukea ja joihin vastata" + too_few_topics_notice: "Laitetaanpa keskustelu alulle! Tällä hetkellä palstalla on %{currentTopics} / %{requiredTopics} ketjua. Uusia kävijöitä varten tarvitaan keskusteluja, joita voivat lukea ja joihin vastata." + too_few_posts_notice: "Laitetaanpa keskustelu alulle! Tällä hetkellä palstalla on %{currentPosts} / %{requiredPosts} viestiä. Uusia kävijöitä varten tarvitaan keskusteluja, joita voivat lukea ja joihin vastata." logs_error_rate_notice: reached: "%{relativeAge}%{rate} saavutti sivuston asetuksissa määritellyn rajan %{siteSettingRate}." exceeded: "%{relativeAge}%{rate} ylittää sivuston asetuksissa määritellyn rajan %{siteSettingRate}." @@ -1143,9 +1159,11 @@ fi: facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Vain alueet" - categories_with_featured_topics: "Alueet, joiden yhteydessä ketjuja" + categories_with_featured_topics: "Alueet ja esitellyt ketjut" categories_and_latest_topics: "Alueet ja tuoreimmat ketjut" - categories_and_top_topics: "Alueet ja huiput ketjut" + categories_and_top_topics: "Alueet ja kuumat ketjut" + categories_boxes: "Tytäralueiden laatikot" + categories_boxes_with_topics: "Esiteltyjen ketjujen laatikot" shortcut_modifier_key: shift: 'Shift' ctrl: 'Ctrl' @@ -1260,6 +1278,7 @@ fi: link_description: "kirjoita linkin kuvaus tähän" link_dialog_title: "Lisää linkki" link_optional_text: "vaihtoehtoinen kuvaus" + link_url_placeholder: "https://esimerkki.fi" quote_title: "Lainaus" quote_text: "Lainaus" code_title: "Teksti ilman muotoiluja" @@ -1274,6 +1293,8 @@ fi: help: "Markdown apu" collapse: "pienennä kirjoitusalue" abandon: "sulje kirjoitusalue ja poista luonnos" + enter_fullscreen: "siirry koko ruudun kirjoitustilaan" + exit_fullscreen: "poistu koko ruudun kirjoitustilasta" modal_ok: "OK" modal_cancel: "Peruuta" cant_send_pm: "Et voi lähettää viestiä käyttäjälle %{username}." @@ -1307,6 +1328,7 @@ fi: desc: "Luonnostele ketju, joka näkyy vain henkilökunnalle" toggle_topic_bump: label: "Ketjun nosto päälle/pois" + desc: "Vastaa muuttamatta ketjun viimeisin viesti -aikaleimaa" notifications: tooltip: regular: @@ -1487,7 +1509,7 @@ fi: hot: "Kuumia ketjuja ei ole." bookmarks: "Et ole vielä merkinnyt kirjanmerkkejä." category: "Alueella {{category}} ei ole ketjua." - top: "Huippuketjuja ei ole." + top: "Kuumia ketjuja ei ole." search: "Hakutuloksia ei löytynyt." educate: new: '

    Sinulle uudet ketjut näytetään tässä.

    Ketju tulkitaan uudeksi ja sen yhteydessä näytetään uusi-merkintä, jos se on aloitettu edellisten kahden päivän aikana.

    Aikarajaa voit muuttaa käyttäjäasetuksissasi.

    ' @@ -1500,7 +1522,7 @@ fi: new: "Uusia ketjuja ei ole enempää." unread: "Ketjuja ei ole enempää lukematta." category: "Alueen {{category}} ketjuja ei ole enempää." - top: "Huippuketjuja ei ole enempää." + top: "Kuumia ketjuja ei ole enempää." bookmarks: "Merkattuja ketjuja ei ole enempää." search: "Hakutuloksia ei ole enempää." topic: @@ -1797,9 +1819,13 @@ fi: action: "yhdistä valitut viestit" error: "Valittuja viestejä yhdistettäessä tapahtui virhe." change_owner: + title: "Vaihda omistajaa" action: "muokkaa omistajuutta" error: "Viestin omistajan vaihdossa tapahtui virhe." placeholder: "uuden omistajan käyttäjätunnus" + instructions: + one: "Valitse uusi omistaja käyttäjän @{{old_user}} viestille." + other: "Valitse uusi omistaja käyttäjän @{{old_user}} {{count}} viestille." change_timestamp: title: "Muuta aikaleimaa..." action: "muuta aikaleimaa" @@ -1870,7 +1896,6 @@ fi: upload: "Pahoittelut, tiedoston lähetys ei onnistunut. Ole hyvä ja yritä uudelleen." file_too_large: "Pahoittelut, tiedosto jonka latausta yritit on liian suuri (suurin tiedostokoko on {{max_size_kb}}kb). Mitäpä jos lataisit tiedoston johonkin pilvipalveluun ja jakaisit täällä siihen linkin?" too_many_uploads: "Pahoittelut, voit ladata vain yhden tiedoston kerrallaan." - too_many_dragged_and_dropped_files: "Pahoittelut, voit ladata korkeintaan 10 tiedostoa kerrallaan." upload_not_authorized: "Pahoittelut, tiedosto jota yrität ladata ei ole sallittu (sallitut laajennukset: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Pahoittelut, uudet käyttjät eivät saa ladata kuvia." attachment_upload_not_allowed_for_new_user: "Pahoittelut, uudet käyttäjät eivät saa ladata liitteitä." @@ -2096,7 +2121,7 @@ fi: subcategory_list_style: "Tytäraluelistan tyyli:" sort_order: "Ketjulistaus järjestetään:" default_view: "Oletuslistaus:" - default_top_period: "Huiput-listauksen oletusajanjakso:" + default_top_period: "Kuumat-listauksen oletusajanjakso:" allow_badges_label: "Salli ansiomerkkien myöntäminen tältä alueelta" edit_permissions: "Muokkaa oikeuksia" add_permission: "Lisää oikeus" @@ -2302,7 +2327,7 @@ fi: other: "{{categoryName}} ({{count}})" help: "Tuoreimmat alueella {{categoryName}}" top: - title: "Huiput" + title: "Kuumat" help: "Aktiivisimmat ketjut viimeisen vuoden, kuukauden ja päivän ajalta" all: title: "Kaikkina aikoina" @@ -2322,7 +2347,7 @@ fi: this_month: "Kuukausi" this_week: "Viikko" today: "Tänään" - other_periods: "katso huiput" + other_periods: "katso kuumat" permission_types: full: "Luoda / Vastata / Nähdä" create_post: "Vastata / Nähdä" @@ -2338,7 +2363,7 @@ fi: new: 'g, n Uudet' unread: 'g, u Lukemattomat' categories: 'g, c Alueet' - top: 'g, t Huiput' + top: 'g, t Kuumat' bookmarks: 'g, b Kirjanmerkit' profile: 'g, p Käyttäjäprofiili' messages: 'g, m Viestit' @@ -2492,7 +2517,7 @@ fi: latest: "Tuoreimpia ketjuja ei ole." hot: "Kuumia ketjuja ei ole." bookmarks: "Et ole vielä merkinnyt kirjanmerkkejä." - top: "Huippuketjuja ei ole." + top: "Kuumia ketjuja ei ole." search: "Hakutuloksia ei löytynyt." bottom: latest: "Tuoreimpia ketjuja ei ole enempää." @@ -2501,7 +2526,7 @@ fi: read: "Luettuja ketjuja ei ole enempää." new: "Uusia ketjuja ei ole enempää." unread: "Ketjuja ei ole enempää lukematta." - top: "Huippuketjuja ei ole enempää." + top: "Kuumia ketjuja ei ole enempää." bookmarks: "Merkattuja ketjuja ei ole enempää." search: "Hakutuloksia ei ole enempää." invite: diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 41e89d0a37..aaf0df3b1c 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -536,7 +536,6 @@ fr: topics_entered: "sujets visités" post_count: "# messages" confirm_delete_other_accounts: "Êtes-vous sûr de vouloir supprimer tous ces comptes ?" - powered_by: "propulsé par ipinfo.io" copied: "copié" user_fields: none: "(choisir une option)" @@ -621,7 +620,6 @@ fr: watched_first_post_tags: "Surveiller les nouveaux sujets" watched_first_post_tags_instructions: "Vous serez notifié du premier message de chaque sujet avec ces tags." muted_categories: "Silencieuses" - muted_categories_instructions: "Vous ne serez notifié de rien concernant les nouveaux sujets dans ces catégories, et elles n'apparaîtront pas dans les dernières catégories." no_category_access: "En tant que modérateur votre accès à la catégorie est limitée, la sauvegarde est désactivée." delete_account: "Supprimer mon compte" delete_account_confirm: "Êtes-vous sûr de vouloir supprimer définitivement votre compte ? Cette action ne peut pas être annulée !" @@ -695,16 +693,10 @@ fr: second_factor: title: "Authentification à deux étapes" disable: "Désactiver l'authentification à deux étapes" - enable: "Activer l'authentification à deux étapes pour une sécurité accrue des comptes." confirm_password_description: "Merci de confirmer votre mot de passe pour continuer" label: "Code" - enable_description: | - Scanner ce code QR dans une app (AndroidiOSWindows Phone) et saisir votre code d'authentification. disable_description: "Veuillez saisir le code d'authentification de votre app" show_key_description: "Saisir manuellement" - extended_description: | - L'authentification à deux étapes ajoute une sécurité supplémentaire à votre compte en exigeant un jeton unique en - plus de votre mot de passe. Les jetons peuvent être générés sur les appareils Android, iOS, et Windows Phone. oauth_enabled_warning: "Veuillez noter que les connexions sociales seront désactivées une fois que l'authentification à deux étapes aura été activée sur votre compte." change_about: title: "Modifier À propos de moi" @@ -724,7 +716,6 @@ fr: title: "Modifier votre image de profil" gravatar: "Gravatar, basé sur" gravatar_title: "Modifier votre avatar sur le site de Gravatar" - gravatar_failed: "Impossible de récupérer le Gravatar. En existe-t-il un associé à cette adresse courriel ?" refresh_gravatar_title: "Actualiser votre Gravatar" letter_based: "Image de profil attribuée par le système" uploaded_avatar: "Avatar personnalisé" @@ -802,7 +793,6 @@ fr: always: "toujours" never: "jamais" email_digests: - title: "Lorsque je ne visite pas le site, m'envoyer un courriel avec un résumé des sujets et réponses populaires" every_30_minutes: "toutes les 30 minutes" every_hour: "toutes les heures" daily: "quotidien" @@ -1881,7 +1871,6 @@ fr: upload: "Désolé, il y a eu une erreur lors de l'envoi du fichier. Merci de réessayer." file_too_large: "Désolé, ce fichier est trop gros (la taille maximale est {{max_size_kb}}kb). Pourquoi ne pas télécharger votre gros fichier sur un service partagé cloud, puis partager le lien?" too_many_uploads: "Désolé, vous ne pouvez envoyer qu'un seul fichier à la fois." - too_many_dragged_and_dropped_files: "Désolé, vous ne pouvez télécharger que 10 fichiers à la fois." upload_not_authorized: "Désolé, le fichier que vous essayez d'envoyer n'est pas autorisé (extensions autorisées : {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Désolé, les nouveaux utilisateurs ne peuvent pas envoyer d'images." attachment_upload_not_allowed_for_new_user: "Désolé, les nouveaux utilisateurs ne peuvent pas envoyer de fichier." diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index 53ab32b0f9..63e8b5e351 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -420,7 +420,6 @@ gl: watched_categories: "Visto" tracked_categories: "Seguido" muted_categories: "Silenciado" - muted_categories_instructions: "Non se che notificará nada sobre os temas novos nestas categorías e non aparecerán na lista de últimos." delete_account: "Eliminar a miña conta" delete_account_confirm: "Confirmas que queres eliminar definitivamente a túa conta? Esta acción non se pode desfacer!" deleted_yourself: "A túa conta acaba de ser eliminada completamente." diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index fff92e6d5b..8d3a47a7c4 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -549,7 +549,6 @@ he: watched_first_post_tags: "צפייה בפוסט ראשון" watched_first_post_tags_instructions: "אתם תיודעו לגבי הפוסט הראשון בכל נושא חדש בתגיות אלו." muted_categories: "מושתק" - muted_categories_instructions: "אתם לא תיודעו על שום דבר לגבי נושאים חדשים בקטגוריות אלו, והם לא יופיעו ב״אחרונים״." no_category_access: "בתור מנחים יש לכם גישה מוגבלת לקטגוריות, שמירה מנוטרלת." delete_account: "מחק את החשבון שלי" delete_account_confirm: "אתם בטוחים שברצונכם להסיר את החשבון? לא ניתן לבטל פעולה זו!" @@ -687,7 +686,6 @@ he: always: "תמיד" never: "אף פעם" email_digests: - title: "כאשר אינני מבקר פה, שלחו לי מייל מסכם של נושאים ותגובות פופולאריים" every_30_minutes: "מידי 30 דקות" every_hour: "שעתי" daily: "יומית" @@ -1618,7 +1616,6 @@ he: upload: "סליחה, הייתה שגיאה בהעלאת הקובץ שלך. אנא נסו שנית" file_too_large: "מצטערים, הקובץ גדול מידי (הגודל המירבי הוא {{max_size_kb}}kb). אולי תקצו להעלות קבצים גדולים לשירות שיתוף בענן ולשתף את הקישור." too_many_uploads: "סליחה, אך ניתן להעלות רק קובץ אחת כל פעם." - too_many_dragged_and_dropped_files: "מצטערים, אתם יכולים להעלות עד 10 קבצים בו זמנית." upload_not_authorized: "מצטערים, הקובץ שאתם מנסים להעלות אינו מורשה (סיומות מורשות: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "סליחה, משתמשים חדשים לא יכולים להעלות תמונות." attachment_upload_not_allowed_for_new_user: "סליחה, משתמשים חדשים לא יכולים להעלות קבצים." diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml index cd0ac14650..2580db9d7c 100644 --- a/config/locales/client.hu.yml +++ b/config/locales/client.hu.yml @@ -526,7 +526,6 @@ hu: topics_entered: "témákba lépett" post_count: "# bejegyzés" confirm_delete_other_accounts: "Biztosan törölni szeretnéd ezeket a fiókokat?" - powered_by: "ipinfo.io által" user_fields: none: "(válassz egy lehetőséget)" user: @@ -674,15 +673,10 @@ hu: second_factor: title: "Két-faktoros hitelesítés" disable: "Két-faktoros azonosítás letiltása" - enable: "Engedélyezd a két-faktoros azonosítást, hogy biztonságosabbá tedd a fiókod." confirm_password_description: "Kérlek erősítsd meg a jelszavad a továbbhaladáshoz" label: "Kód" - enable_description: | - Olvasd be ezt a QR-kódot egy támogatott alkalmazással (AndroidiOSWindows Phone) és add meg az azonosító kódodat. disable_description: "Írd be az azonosító kódodat az alkalmazásból" show_key_description: "Manuális beírás" - extended_description: | - A két-faktoros azonosítás plusz biztonságot a fiókhoz úgy, hogy egy extra tokent kér bejelentkezéskor a jelszó mellé. A tokenek generálhatóak Android, iOS vagy Windows Phone eszközökön. oauth_enabled_warning: "A közösségi bejelentkezések nem lesznek elérhetőek a fét-faktoros azonosítás aktiválása után a fiókhoz." change_about: title: "Rólam megváltoztatása" @@ -772,7 +766,6 @@ hu: always: "mindig" never: "soha" email_digests: - title: "Ha nem látogatom az oldalt. küldjön kivonatos e-mailt az újdonságokról" every_30_minutes: "minden 30 percben" every_hour: "óránként" daily: "naponta" diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index 58954ecc33..88ce683877 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -151,7 +151,7 @@ id: generic_error: "Maaf, terjadi kesalahan." generic_error_with_reason: "Terjadi kesalahan: %{error}" sign_up: "Daftar" - log_in: "Log In" + log_in: "Masuk" age: "Umur" joined: "Bergabung" admin_title: "Admin" @@ -481,7 +481,6 @@ id: watched_categories: "Dilihat" tracked_categories: "Dilacak" muted_categories: "Diredam" - muted_categories_instructions: "Anda tidak akan diberitahu apapun tentang topik baru di kategori ini, dan mereka tidak akan muncul diterbarunya. " delete_account: "Hapus Akun Saya" delete_account_confirm: "Apakah Anda yakin untuk menghapus akun ini secara permanen? Perintah ini tidak dapat dibatalkan!" deleted_yourself: "Akun Anda telah sukses dihapus." @@ -559,7 +558,7 @@ id: instructions: "Gambar latar belakang akan ditengahkan dan mempunyai lebar standar 590px" email: title: "Surel" - instructions: "Jangan pernah diperlihatkan ke khalayak umum" + instructions: "Tidak akan pernah diperlihatkan ke khalayak umum" ok: "Kami akan mengirimkan surel kepada anda untuk konfirmasi" invalid: "Silahkan masukkan alamat email yang valid" authenticated: "Email anda sudah dikonfirmasi oleh {{provider}}" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index 8b63c814e5..ac9dadf376 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -409,6 +409,7 @@ it: all: "Tutti i gruppi" empty: "Non ci sono gruppi visibili." filter: "Filtra per tipo di gruppo" + owner_groups: "Guppi di cui sono proprietario" close_groups: "Gruppi Chiusi" automatic_groups: "Gruppi Automatici" automatic: "Automatico" @@ -512,6 +513,7 @@ it: topic_stat_sentence: one: "%{count} nuovo argomento nell'ultimo %{unit}." other: "%{count} nuovi argomenti nell'ultimo %{unit}." + n_more: "Categorie (altre %{count}) ..." ip_lookup: title: Ricerca Indirizzo IP hostname: Hostname @@ -527,7 +529,7 @@ it: topics_entered: "argomenti visualizzati" post_count: "n° messaggi" confirm_delete_other_accounts: "Sicuro di voler cancellare questi account?" - powered_by: "fornito da ipinfo.io" + copied: "copiato" user_fields: none: "(scegli un'opzione)" user: @@ -545,6 +547,7 @@ it: private_messages: "Messaggi" activity_stream: "Attività" preferences: "Opzioni" + profile_hidden: "Il profilo pubblico di questo utente è nascosto." expand_profile: "Espandi" collapse_profile: "Raggruppa" bookmarks: "Segnalibri" @@ -611,7 +614,6 @@ 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 ti verrà notificato nulla sui nuovi argomenti in queste categorie, e non compariranno nell'elenco Recenti." 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!" @@ -630,6 +632,7 @@ it: revoke_access: "Revoca Accesso" undo_revoke_access: "Annullare Revoca Accesso" api_approved: "Approvato:" + api_last_used_at: "Ultimo utilizzo:" theme: "Tema" home: "Home Page Predefinita" staged: "Temporaneo" @@ -684,15 +687,10 @@ it: second_factor: title: "Autenticazione a Due Fattori" disable: "Disabilita l'autenticazione a due fattori" - enable: "Attival'autenticazione a due fattori per migliorare la sicurezza dell'account" confirm_password_description: "Per favore conferma la tua password per continuare" label: "Codice" - enable_description: | - Esegui la scansione di questo codice QR con un'app supportata (AndroidiOSWindows Phone) e inserisci il tuo codice di autenticazione. disable_description: "Inserisci il codice di autenticazione dalla tua app" show_key_description: "Inserisci manualmente" - extended_description: | - L'autenticazione a due fattori aggiunge ulteriore sicurezza al tuo account richiedendo un token monouso oltre alla tua password. I token possono essere generati su dispositivi Android, iOS e Windows Phone. oauth_enabled_warning: "Tieni presente che gli accessi ai social network saranno disabilitati dopo aver attivato l'autenticazione a due fattori nel tuo account." change_about: title: "Modifica i dati personali" @@ -712,7 +710,6 @@ it: title: "Cambia l'immagine del tuo profilo" gravatar: "Gravatar, basato su" gravatar_title: "Cambia il tuo avatar sul sito Gravatar" - gravatar_failed: "Impossibile recuperare il Gravatar. Ce n'è uno associato a questo indirizzo email?" refresh_gravatar_title: "Ricarica il tuo Gravatar" letter_based: "Immagine del profilo assegnata dal sistema" uploaded_avatar: "Immagine personalizzata" @@ -768,6 +765,17 @@ it: any: "qualunque" password_confirmation: title: "Ripeti la password" + auth_tokens: + title: "Dispositivi utilizzati di recente" + ip: "IP" + details: "Dettagli" + log_out_all: "Disconnetti tutti" + active: "attivo ora" + not_you: "Non sei tu?" + show_all: "Mostra tutti ({{count}})" + show_few: "Mostrane di meno" + was_this_you: "Eri tu?" + browser_and_device: "{{browser}} su {{device}}" last_posted: "Ultimo Messaggio" last_emailed: "Ultima email inviata" last_seen: "Ultima visita" @@ -776,6 +784,7 @@ it: location: "Località" website: "Sito Web" email_settings: "Email" + hide_profile_and_presence: "Nascondi il mio profilo pubblico e la mia presenza" like_notification_frequency: title: "Notifica alla ricezione di \"Mi piace\"." always: "Sempre" @@ -788,7 +797,6 @@ it: always: "sempre" never: "mai" email_digests: - title: "Quando non visito il sito, inviami un riassunto via email degli argomenti più discussi e delle risposte" every_30_minutes: "ogni 30 minuti" every_hour: "ogni ora" daily: "ogni giorno" @@ -996,6 +1004,8 @@ it: hide_session: "Ricordamelo domani" hide_forever: "no grazie" hidden_for_session: "Ok, te lo chiederò domani. Puoi anche usare 'Connetti' per creare un account." + intro: "Ciao! Sembra che la discussione ti interessi, ma non hai ancora registrato un account." + value_prop: "Quando crei un account, potremo ricordare esattamente cosa hai letto, in modo che tu possa riprendere esattamente da dove hai lasciato. Riceverai inoltre notifiche, qui o via email, quando qualcuno ti risponde. Potrai anche mettere \"Mi piace\" per mostrare il tuo apprezzamento :heartpulse:" summary: enabled_description: "Stai visualizzando un riepilogo dell'argomento: è la comunità a determinare quali sono i messaggi più interessanti." description: "Ci sono {{replyCount}} risposte." @@ -1009,6 +1019,8 @@ it: disable: "Mostra Messaggi Eliminati" private_message_info: title: "Messaggio" + invite: "Invita altri..." + edit: "Aggiungi o rimuovi..." leave_message: "Vuoi veramente abbandonare questo messaggio?" remove_allowed_user: "Davvero vuoi rimuovere {{name}} da questo messaggio?" remove_allowed_group: "Vuoi veramente rimuovere {{name}} da questo messaggio?" @@ -1144,6 +1156,7 @@ it: default_header_text: Selezione... no_content: Nessun risultato trovato filter_placeholder: Ricerca... + filter_placeholder_with_any: Cerca o crea... create: "Crea: '{{content}}'" max_content_reached: one: "Puoi selezionare solo {{count}} elemento." @@ -1247,6 +1260,7 @@ it: link_description: "inserisci qui la descrizione del collegamento" link_dialog_title: "Inserisci il collegamento" link_optional_text: "titolo opzionale" + link_url_placeholder: "https://example.com" quote_title: "Citazione" quote_text: "Citazione" code_title: "Testo preformattato" @@ -1846,7 +1860,6 @@ it: upload: "Spiacenti, si è verificato un errore durante il caricamento del file. Prova di nuovo." file_too_large: "Spiacenti, il file è troppo grande (la grandezza massima è {{max_size_kb}}kb). Perché non carichi il file con un servizio di cloud sharing e poi ne condividi il collegamento?" too_many_uploads: "Spiacenti, puoi caricare un solo file per volta." - too_many_dragged_and_dropped_files: "Spiacenti, puoi caricare solo 10 file alla volta." upload_not_authorized: "Spiacenti, il file che stai cercando di caricare non è autorizzato (estensioni autorizzate: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Spiacenti, i nuovi utenti non possono caricare immagini." attachment_upload_not_allowed_for_new_user: "Spiacenti, i nuovi utenti non possono caricare allegati." @@ -2067,6 +2080,7 @@ it: show_subcategory_list: "Mostra la lista delle sottocategorie sopra agli argomenti in questa categoria." num_featured_topics: "Numero degli argomenti mostrati nella pagina categorie:" subcategory_num_featured_topics: "Numero degli argomenti in evidenza nella pagina della categoria superiore" + all_topics_wiki: "Rendi automaticamente i nuovi Argomenti delle Wiki" subcategory_list_style: "Stile Lista Sottocategorie:" sort_order: "Lista Argomenti Ordinata Per:" default_view: "Lista Argomenti Predefinita:" @@ -2654,6 +2668,7 @@ it: trust_levels_title: "Livello Esperienza assegnato automaticamente agli utenti quando vengono aggiunti:" trust_levels_none: "Nessuno" primary_group: "Imposta automaticamente come gruppo primario" + name_placeholder: "Nome del gruppo, senza spazi, stesse regole del nome utente" primary: "Gruppo Primario" no_primary: "(nessun gruppo primario)" title: "Gruppi" @@ -2866,6 +2881,8 @@ it: theme: "Tema" component: "Componente" components: "Componenti" + theme_name: "Nome del Tema" + component_name: "Nome del Componente" import_theme: "Importa Tema" customize_desc: "Personalizza:" title: "Temi" @@ -2886,6 +2903,11 @@ it: color_scheme_select: "Seleziona i colori da usare nel tema" custom_sections: "Sezioni personalizzate:" theme_components: "Componenti Tema" + convert: "Converti" + convert_component_tooltip: "Converti questo Componente in Tema" + convert_theme_alert: "Sei sicuro di voler convertire questo Tema in Componente? Sarà rimosso come Tema Padre da %{relatives}." + convert_theme_tooltip: "Converti questo Tema in Componente" + inactive_themes: "Temi non attivi:" uploads: "Carica" no_uploads: "Puoi caricare le risorse associate al tuo tema come il tipo di carattere e le immagini" add_upload: "Aggiungi Caricamento" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index afe4125df9..3fb1561b69 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -38,21 +38,21 @@ ko: long_date_with_year_without_time: "'YY MMM D" long_date_without_year_with_linebreak: "MMM D
    LT" long_date_with_year_with_linebreak: "'YY MMM D
    LT" - wrap_ago: "%{date} 전" + wrap_ago: "%{date}전" tiny: half_a_minute: "< 1분" less_than_x_seconds: other: "< %{count}초" x_seconds: - other: "%{count}초 전" + other: "%{count}초전" less_than_x_minutes: other: "< %{count}분" x_minutes: - other: "%{count}분 전" + other: "%{count}분전" about_x_hours: other: "%{count}시간" x_days: - other: "%{count}일 전" + other: "%{count}일전" x_months: other: "%{count}달" about_x_years: @@ -73,35 +73,37 @@ ko: date_year: "'YY MMM D" medium_with_ago: x_minutes: - other: "%{count}분 전" + other: "%{count}분전" x_hours: - other: "%{count}시간 전" + other: "%{count}시간전" x_days: - other: "%{count}일 전" + other: "%{count}일전" later: x_days: - other: "%{count}일 후" + other: "%{count}일후" x_months: - other: "%{count}달 후" + other: "%{count}달후" x_years: - other: "%{count}년 후" + other: "%{count}년후" previous_month: '지난 달' next_month: '다음 달' + placeholder: 날짜 share: - topic: '토픽을 공유합니다' + topic: '이 글을 공유합니다' post: '게시글 #%{postNumber}' close: '닫기' - twitter: 'twitter로 공유' - facebook: 'Facebook으로 공유' + twitter: '트위터로 공유' + facebook: '페이스북으로 공유' google+: 'Google+로 공유' email: '이메일로 공유' action_codes: - public_topic: "이 토픽을 %{when} 에 공개" - split_topic: "이 토픽을 %{when}에 분리" + public_topic: "이 글을 %{when} 에 공개" + split_topic: "이 글을 %{when}에 분리" invited_user: "%{who}이(가) %{when}에 초대됨" invited_group: "%{who} 이(가) %{when} 에 초대됨" removed_user: "%{who}이(가) %{when}에 삭제됨" removed_group: "%{who}이(가) %{when}에 삭제됨" + autobumped: "%{when}에 자동으로 끌어올려짐" autoclosed: enabled: '%{when}에 닫힘' disabled: '%{when}에 열림' @@ -171,6 +173,7 @@ ko: privacy_policy: "개인보호 정책" privacy: "개인정보처리방침" tos: "서비스 이용약관" + rules: "규칙" mobile_view: "모바일로 보기" desktop_view: "PC로 보기" you: "당신" @@ -194,8 +197,8 @@ ko: title: "추천 토픽" pm_title: "추천 메세지" about: - simple_title: "About" - title: "About %{title}" + simple_title: "소개" + title: "소개: %{title}" stats: "사이트 통계" our_admins: "관리자" our_moderators: "운영자" @@ -215,8 +218,10 @@ ko: bookmark: "클릭하면 이 토픽의 첫번째 포스트가 북마크됩니다" unbookmark: "클릭하면 이 토픽에 속한 모든 북마크가 제거됩니다" bookmarks: - created: "이 게시글을 북마크 하였습니다." + created: "이 글을 북마크 했습니다" + not_bookmarked: "이 글을 북마크에 추가" remove: "북마크 삭제" + confirm_clear: "이 주제글의 모든 북마크를 지우시겠습니까?" drafts: remove: "삭제" preview: "미리보기" @@ -226,6 +231,8 @@ ko: saved: "저장 완료!" upload: "업로드" uploading: "업로드 중..." + uploading_filename: "업르도중: {{filename}}..." + clipboard: "클립보드" uploaded: "업로드 완료!" pasting: "붙혀넣는중..." enable: "활성화" @@ -305,10 +312,16 @@ ko: make_user_group_owner: "소유자로 지정하기" remove_user_as_group_owner: "소유자 지정 취소하기" groups: + add_members: + title: "사용자 추가" + description: "이 그룹의 회원 관리" + usernames: "아이디" manage: title: '관리' name: '이름' + full_name: '이름' add_members: "멤버 추가" + delete_member_confirm: "'%{username}'님을 '%{group}'그룹에서 삭제 하시겠습니까?" profile: title: 프로필 interaction: @@ -320,6 +333,7 @@ ko: logs: title: "로그" target_user: "타겟 사용자" + subject: "제목" public_admission: "사용자가 그룹에 자유롭게 가입할 수 있도록 허용합니다. (그룹이 공개되어야 함)" public_exit: "사용자가 그룹을 자유롭게 탈퇴할 수 있도록 허용합니다." empty: @@ -330,7 +344,7 @@ ko: topics: "이 그룹의 멤버가 작성한 토픽이 없습니다." logs: "이 그룹에 대한 기록이 없습니다." add: "추가" - message: "메세지" + message: "메시지" allow_membership_requests: "사용자가 그룹 소유자에게 가입신청을 할 수 있도록 허용합니다" membership_request_template: "가입 요청을 전송할 때 사용자에게 표시할 커스텀 템플릿" membership_request: @@ -339,17 +353,24 @@ ko: reason: "그룹 소유자에게 왜 이 그룹에 속해야하는지 알립니다." membership: "회원제" name: "이름" + group_name: "그룹명" + user_count: "사용자" bio: "이 그룹에 대하여" + selector_placeholder: "아이디 입력" owner: "소유자" index: title: "그룹" + all: "모든 그룹" empty: "보이는 그룹이 없습니다." + filter: "그룹 유형별로 분류" + close_groups: "닫힌 그룹" automatic_groups: "자동 그룹" automatic: "자동" title: other: "그룹" activity: "활동" members: + remove_member: "회원 삭제" remove_member_description: "이 그룹에서 %{username}(을)를 삭제합니다." make_owner_description: "이 그룹에서 %{username}(을)를 소유자로 설정합니다." topics: "토픽" @@ -404,6 +425,7 @@ ko: "14": "대기" categories: all: "전체 카테고리" + all_subcategories: "모두" no_subcategory: "없음" category: "카테고리" category_list: "카테고리 목록 표시" @@ -438,7 +460,7 @@ ko: topics_entered: "입력된 제목:" post_count: "포스트 개수" confirm_delete_other_accounts: "정말 이 계정들을 삭제하시겠습니까?" - powered_by: "powered by ipinfo.io" + copied: "복사됨" user_fields: none: "(옵션을 선택하세요)" user: @@ -456,6 +478,7 @@ ko: private_messages: "메시지" activity_stream: "활동" preferences: "환경 설정" + profile_hidden: "이 사용자의 프로필은 비공개 상태입니다." expand_profile: "확장" bookmarks: "북마크" bio: "내 소개" @@ -479,6 +502,7 @@ ko: disable_jump_reply: "댓글을 작성했을 때, 새로 작성한 댓글로 화면을 이동하지 않습니다." dynamic_favicon: "새 글이나 업데이트된 글 수를 브라우저 아이콘에 보이기" theme_default_on_all_devices: "이 테마를 모든 기기의 기본 테마로 설정합니다." + allow_private_messages: "다른 사용자가 나에게 개인 메시지를 보내는것을 허용" external_links_in_new_tab: "모든 외부 링크를 새 탭에 열기" enable_quoting: "강조 표시된 텍스트에 대한 알림을 사용합니다" change: "변경" @@ -518,7 +542,6 @@ ko: watched_first_post_tags: "첫번째 글 보기" watched_first_post_tags_instructions: "이 태그가 달린 토픽이 생길 때마다 알림을 받습니다." muted_categories: "알림 끄기" - muted_categories_instructions: "이 카테고리 내의 새 토픽에 대해 알림을 받지 않으며, 최근 토픽란에도 나타나지 않습니다." no_category_access: "운영자는 이 카테고리 접근에 제약을 받습니다. 저장이 해제 됩니다." delete_account: "내 계정 삭제" delete_account_confirm: "정말로 계정을 삭제할까요? 이 작업은 되돌릴 수 없습니다." @@ -556,6 +579,7 @@ ko: move_to_archive: "보관하기" failed_to_move: "선택한 메시지를 이동할 수 없습니다 (아마도 네트워크가 다운됨)" select_all: "모두 선택" + tags: "태그" preferences_nav: account: "계정" profile: "프로필" @@ -577,6 +601,8 @@ ko: regenerate: "재생성" disable: "해제" enable: "설정" + enable_long: "백업 코드 사용" + manage: "백업 코드 관리" copied_to_clipboard: "클립보드에 복사됨" copy_to_clipboard_error: "데이터를 클립보드에 복사하는데 오류가 발생했습니다." codes: @@ -584,7 +610,6 @@ ko: second_factor: title: "이중 인증" disable: "이중 인증 해제" - enable: "보다 향상된 계정 보안을 위해 이중 인증을 활성화 하세요." confirm_password_description: "비밀번호를 확인해주세요." label: "코드" change_about: @@ -592,6 +617,7 @@ ko: error: "값을 바꾸는 중 에러가 발생했습니다." change_username: title: "아이디 변경" + confirm: "정말로 아이디를 변경 하시겠습니까?" taken: "죄송합니다. 이미 사용 중인 아이디입니다." invalid: "아이디가 잘못되었습니다. 숫자와 문자를 포함해야합니다." change_email: @@ -619,6 +645,9 @@ ko: instructions: "배경 이미지는 가운데를 기준으로 표시되며 590px이 기본 가로 사이즈 입니다." email: title: "이메일" + primary: "기본 이메일" + secondary: "보조 이메일" + no_secondary: "보조 이메일이 없습니다" instructions: "절대로 공개되지 않습니다" ok: "내 이메일로 확인 메일이 전송됩니다." invalid: "유효한 이메일 주소를 입력해주세요." @@ -626,6 +655,9 @@ ko: frequency_immediately: "만약 전송된 메일을 읽지 않았을 경우, 즉시 메일을 다시 보내드립니다." frequency: other: "최근 {{count}}분 동안접속하지 않을 경우에만 메일이 전송됩니다." + associated_accounts: + connect: "연결" + not_connected: "(연결되지 않음)" name: title: "이름" instructions: "이름 (선택사항)" @@ -650,6 +682,11 @@ ko: any: "무관" password_confirmation: title: "비밀번호를 재입력해주세요." + auth_tokens: + title: "최근에 사용한 기기" + details: "세부 내용" + log_out_all: "모두 로그 아웃" + not_you: "사용자님이 아닌가요?" last_posted: "마지막글" last_emailed: "마지막 이메일" last_seen: "마지막 접속" @@ -658,6 +695,7 @@ ko: location: "위치" website: "웹사이트" email_settings: "이메일" + hide_profile_and_presence: "내 공개 프로필 및 현재 상태 기능 숨기기" like_notification_frequency: title: "좋아요를 받았을 때 알림받기" always: "항상 알림받기" @@ -670,7 +708,6 @@ ko: always: "항상 알림 받기" never: "알림 받지 않기" email_digests: - title: "이 사이트를 방문하지 않을 때, 인기있는 토픽과 댓글의 요약 메일 받기" every_30_minutes: "매 30분 마다" every_hour: "매 시간" daily: "매일" @@ -787,6 +824,9 @@ ko: most_liked_users: "가장 많이 좋아요를 받은" most_replied_to_users: "댓글을 가장 많이 단 사람" no_likes: "아직 좋아요가 없습니다." + top_categories: "상위 카테고리" + topics: "주제글" + replies: "댓글" ip_address: title: "마지막 IP 주소" registration_ip_address: @@ -796,6 +836,7 @@ ko: header_title: "프로필, 메시지, 북마크 그리고 설정" title: title: "호칭" + none: "(없음)" filters: all: "전체" stream: @@ -872,6 +913,7 @@ ko: disable: "삭제된 글 보기" private_message_info: title: "메시지" + edit: "추가 또는 삭제 ..." leave_message: "정말로 이 메시지를 남길까요?" remove_allowed_user: "{{name}}에게서 온 메시지를 삭제할까요?" remove_allowed_group: "{{name}}에게서 온 메시지를 삭제할까요?" @@ -904,6 +946,7 @@ ko: title: "로그인" username: "아이디" password: "비밀번호" + second_factor_backup: "백업 코드를 사용해 로그인" email_placeholder: "이메일 주소 또는 아이디" caps_lock_warning: "Caps Lock 켜짐" error: "알 수없는 오류" @@ -930,21 +973,27 @@ ko: forgot: "내 계정의 상세내역 기억하지 않는다." not_approved: "당신의 계정은 아직 활성화되지 않았습니다. 이메일을 확인하시고 로그인 해주세요." google_oauth2: + name: "구글" title: "with Google" message: "구글을 통해 인증 중 (파업이 허용되어 있는지 확인해주세요.)" twitter: + name: "트위터" title: "with Twitter" message: "Twitter 인증 중(팝업 차단을 해제 하세요)" instagram: + name: "인스타그램" title: "인스타그램" message: "인스타그램으로 인증 중 (팝업이 허용되어 있는지 확인해주세요)" facebook: + name: "페이스북" title: "with Facebook" message: "Facebook 인증 중(팝업 차단을 해제 하세요)" yahoo: + name: "야후" title: "Yahoo" message: "Yahoo 인증 중(팝업 차단을 해제 하세요)" github: + name: "GitHub" title: "GitHub" message: "GitHub 인증 중(팝업 차단을 해제 하세요)" invites: @@ -962,8 +1011,8 @@ ko: continue: "%{site_name}으로 가기" emoji_set: apple_international: "Apple/International" - google: "Google" - twitter: "Twitter" + google: "구글" + twitter: "트위터" emoji_one: "Emoji One" win10: "Win10" google_classic: "Google 클래식" @@ -976,6 +1025,12 @@ ko: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + conditional_loading_section: + loading: 로드중... + select_kit: + default_header_text: 선택... + no_content: 일치하는 결과가 없습니다 + filter_placeholder: 검색... emoji_picker: filter_placeholder: 이모지 찾기 people: 인물 @@ -1035,9 +1090,10 @@ ko: title_placeholder: "이야기 나누고자 하는 내용을 한문장으로 적는다면?" title_or_link_placeholder: "제목을 입력하거나, 링크를 붙여넣으세요" edit_reason_placeholder: "why are you editing?" - show_edit_reason: "(add edit reason)" + show_edit_reason: "(수정사유 추가)" topic_featured_link_placeholder: "타이틀과 함께 표시될 링크를 입력하세요." reply_placeholder: "여기에 타이핑 하세요. 마크다운 또는 BBCode, HTML 포맷을 이용하세요. 이미지를 끌어오거나 붙여넣기 하세요." + reply_placeholder_choose_category: "입력하기 전에 카테고리를 먼저 선택해야합니다." view_new_post: "새로운 글을 볼 수 있습니다." saving: "저장 중..." saved: "저장 완료!" @@ -1056,6 +1112,7 @@ ko: link_description: "링크 설명을 입력" link_dialog_title: "하이퍼링크 삽입" link_optional_text: "옵션 제목" + link_url_placeholder: "https://example.com" quote_title: "인용구" quote_text: "인용구" code_title: "코드 샘플" @@ -1067,14 +1124,28 @@ ko: ulist_title: "글 머리 기호 목록" list_item: "주제" help: "마크다운 편집 도움말" - modal_ok: "OK" + modal_ok: "확인" modal_cancel: "취소" cant_send_pm: "죄송합니다. %{username}님에게 메시지를 보낼 수 없습니다." yourself_confirm: title: "수신자 추가를 잊으셨나요?" body: "현재 이 메시지는 당신에게만 전송됩니다." admin_options_title: "이 주제에 대한 옵션 설정" + composer_actions: + reply: 댓글 + draft: 임시저장 + edit: 수정 + reply_as_private_message: + label: 새 메시지 + desc: 새 개인 메시지 쓰기 + create_topic: + label: "새 주제글" notifications: + tooltip: + regular: + other: "{{count}}개의 확인하지 않은 알림이 있습니다" + message: + other: "{{count}}개의 읽지않은 메시지가 있습니다" title: "@name 언급, 글과 주제에 대한 답글, 개인 메시지 등에 대한 알림" none: "현재 알림을 불러올 수 없습니다." empty: "알림이 없습니다." @@ -1108,6 +1179,8 @@ ko: replied: '"{{topic}}" - {{site_title}}에서 {{username}} 님이 내게 답글을 달았습니다' posted: '"{{topic}}" - {{site_title}}에서 {{username}}님이 글을 게시하였습니다' linked: '{{username}}님이 "{{topic}}" - {{site_title}}에 내 글을 링크했습니다' + confirm_title: '알림 활성 - %{site_title}' + confirm_body: '완료! 알림이 활성화되었습니다.' upload_selector: title: "이미지 추가하기" title_with_attachments: "이미지 또는 파일 추가하기" @@ -1122,6 +1195,7 @@ ko: uploading: "업로드 중입니다..." select_file: "파일 선택" image_link: "이 이미지를 누르면 이동할 링크" + default_image_alt_text: 이미지 search: sort_by: "다음으로 정렬" relevance: "관련성" @@ -1133,16 +1207,18 @@ ko: clear_all: "다 지우기" too_short: "검색 단어가 너무 짧습니다." title: "주제, 글, 사용자, 카테고리 검색" + full_page_title: "주제글 또는 글 검색" no_results: "검색 결과가 없습니다" no_more_results: "더 이상 결과가 없습니다." searching: "검색중..." post_format: "#{{post_number}} by {{username}}" + results_page: "'{{term}}'의 검색 결과" more_results: "검색 결과가 많습니다. 검색 조건을 좁혀보세요." cant_find: "원하는 걸 찾을 수 없으신가요?" start_new_topic: "새 토픽을 만들어볼까요?" or_search_google: "혹은 구글에서 검색해볼 수도 있습니다." search_google: "대신 구글에서 검색해보세요." - search_google_button: "Google" + search_google_button: "구글" search_google_title: "이 사이트 검색" context: user: "@{{username}}의 글 검색" @@ -1252,6 +1328,8 @@ ko: move_to_inbox: title: '수신함으로 이동' help: '메시지를 편지함으로 되돌리기' + edit_message: + title: '메시지 수정' list: '주제 목록' new: '새로운 주제' unread: '읽지 않은' @@ -1309,6 +1387,9 @@ ko: next_week: "다음 주" two_weeks: "2주" next_month: "다음 달" + three_months: "3개월" + six_months: "6개월" + one_year: "1년" forever: "영구적" pick_date_and_time: "날짜와 시간을 " set_based_on_last_post: "마지막 게시글 기준으로 닫기" @@ -1353,6 +1434,7 @@ ko: jump_prompt_of: "번째, 총 %{count} 개 포스트" jump_prompt_long: "어느 게시글로 점프할까요?" jump_bottom_with_number: "jump to post %{post_number}" + jump_prompt_or: "또는" total: 총 글 current: 현재 글 notifications: @@ -1413,6 +1495,7 @@ ko: visible: "목록에 넣기" reset_read: "값 재설정" make_public: "공개 토픽으로 만들기" + reset_bump_date: "끌어올림 날자 리셋" feature: pin: "주제 고정" unpin: "주제 고정 취소" @@ -1522,6 +1605,10 @@ ko: multi_select: select: '선택' selected: '({{count}})개가 선택됨' + select_post: + label: '선택' + selected_post: + label: '선택됨' delete: 선택 삭제 cancel: 선택을 취소 select_all: 전체 선택 @@ -1530,7 +1617,7 @@ ko: other: "{{count}}개의 개시글을 선택하셨어요." post: quote_reply: "인용하기" - edit_reason: "Reason: " + edit_reason: "사유: " post_number: "{{number}}번째 글" wiki_last_edited_on: "마지막으로 위키가 수정된 일시" last_edited_on: "마지막으로 편집:" @@ -1542,7 +1629,9 @@ ko: show_hidden: '숨겨진 내용을 표시' deleted_by_author: other: "(작성자에 의해 취소된 글입니다. 글이 신고된 것이 아닌 한 %{count} 시간 뒤에 자동으로 삭제됩니다)" + collapse: "축소" expand_collapse: "확장/축소" + locked: "이 글은 운영진에 의해 수정이 금지 되었습니다." gap: other: "{{count}}개의 숨겨진 답글 보기" unread: "읽지 않은 포스트" @@ -1561,7 +1650,6 @@ ko: upload: "죄송합니다. 파일을 업로드하는 동안 오류가 발생했습니다. 다시 시도하십시오." file_too_large: "죄송합니다. 파일의 크기가 너무 큽니다. (최대 사이즈는 {{max_size_kb}}kb 입니다). 클라우드 공유 서버에 올리신 다음에 링크를 공유하는 건 어떠세요?" too_many_uploads: "한번에 한 파일만 업로드 하실 수 있습니다." - too_many_dragged_and_dropped_files: "죄송합니다. 한번에 파일 10개만 올릴 수 있습니다." upload_not_authorized: "죄송합니다. 허가되지 않은 확장자의 파일이 있습니다. (허가된 확장자: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "죄송합니다. 새로운 유저는 이미지를 업로드 하실 수 없습니다." attachment_upload_not_allowed_for_new_user: "죄송합니다. 새로운 유저는 파일 첨부를 업로드 하실 수 없습니다." @@ -1591,6 +1679,8 @@ ko: undelete: "이 글 삭제를 취소합니다." share: "이 글에 대한 링크를 공유합니다." more: "더" + delete_replies: + confirm: "이 글에 대한 댓글을 삭제 하시겠습니까?" admin: "관리자 기능" wiki: "위키 만들기" unwiki: "위키 제거하기" @@ -1599,6 +1689,10 @@ ko: rebake: "HTML 다시 빌드하기" unhide: "숨기지 않기" change_owner: "소유자 변경" + lock_post: "글 잠그기" + unlock_post: "글 잠금 해제" + unlock_post_description: "작성자가 글을 수정하도록 허용" + delete_topic_disallowed: "이 글을 삭제할 수있는 권한이 없습니다" actions: flag: '신고하기' undo: @@ -1653,6 +1747,9 @@ ko: other: "{{count}}명이 북마크했습니다" like: other: "{{count}}명이 좋아합니다" + delete: + confirm: + other: "정말로 {{count}}개의 글을 삭제 하시겠습니까?" merge: confirm: other: "정말로 이 {{count}}개의 게시글을 합칠까요?" @@ -1693,6 +1790,7 @@ ko: can: '허용' none: '(카테고리 없음)' all: '모든 카테고리' + choose: '카테고리…' edit: '편집' edit_long: "카테고리 편집" view: '카테고리 안의 주제 보기' @@ -1747,6 +1845,7 @@ ko: edit_permissions: "권한 수정" add_permission: "권한 추가" this_year: "올해" + position: "위치:" default_position: "기본 위치" position_disabled: "카테고리는 활동량에 따라서 표시됩니다. 목록 내의 카테고리 순서를 지정하하려면" position_disabled_click: '"카테고리 위치 고정" 설정을 활성화 시키십시요.' @@ -2043,10 +2142,12 @@ ko:

    tagging: all_tags: "모든 태그" + other_tags: "기타 태그" selector_all_tags: "모든 태그" selector_no_tags: "태그 없음" changed: "바뀐 태그:" tags: "태그" + choose_for_topic: "선택적 태그" delete_tag: "태그 삭제" delete_confirm: other: "정말로 이 태그를 삭제하고 이 태그가 붙은 {{count}} 개의 토픽에서 태그를 제거할까요?" @@ -2088,6 +2189,7 @@ ko: save: "저장" delete: "삭제" confirm_delete: "정말로 이 태그 그룹을 삭제할까요?" + visible_only_to_staff: "태그는 운영진 에게만 표시됩니다" topics: none: unread: "읽지 않은 토픽이 없습니다." @@ -2146,17 +2248,22 @@ ko: space_free: "{{size}} free" uploads: "업로드" backups: "백업" + lastest_backup: "최근: %{date}" traffic_short: "트래픽" traffic: "어플리케이션 웹 요청" page_views: "페이지뷰" page_views_short: "페이지뷰" show_traffic_report: "자세한 트래픽 리포트 보기" + disabled: 비활성 + exception_error: 죄송합니다. 쿼리 실행 중 오류가 발생했습니다. reports: today: "오늘" yesterday: "어제" + last_7_days: "최근 7일" + last_30_days: "최근 30일" all_time: "모든 시간" - 7_days_ago: "7일" - 30_days_ago: "30일" + 7_days_ago: "7일전" + 30_days_ago: "30일전" all: "전체" view_table: "테이블" view_graph: "그래프" @@ -2176,6 +2283,7 @@ ko: agree: "동의" agree_title: "이 신고가 올바르고 타당함을 확인합니다" agree_flag_hide_post: "포스트 숨기기" + agree_flag_suspend: "사용자 차단" ignore_flag: "무시" delete: "삭제" delete_title: "신고에서 멘션된 글 삭제하기" @@ -2193,6 +2301,10 @@ ko: more: "(더 많은 답글...)" suspend_user: "정지된 사용자" suspend_user_title: "이 포스트에 한하여 사용자 일시정지" + replies: + other: "[%{count}개의 댓글]" + delete_replies: + other: "이 글에 대한 %{count}개의 댓글도 삭제 하시겠습니까?" dispositions: agreed: "동의" disagreed: "반대" @@ -2216,6 +2328,7 @@ ko: type: "타입" users: "사용자" last_flagged: "마지막으로 신고받은 때" + no_results: "신고된 주제글이 없습니다." short_names: off_topic: "주제 벗어남" inappropriate: "부적절" @@ -2232,12 +2345,19 @@ ko: available: "사용가능한 그룹 이름" not_available: "사용 불가능한 그룹 이름" blank: "그룹 이름은 공백이 될 수 없습니다" + add_members: + as_owner: "사용자를 이 그룹의 소유자로 설정" manage: interaction: email: 이메일 + incoming_email_placeholder: "이메일 주소 입력" + visibility_levels: + public: "모두" membership: automatic: 자동 trust_level: 신뢰도 + trust_levels_none: "없음" + primary_group: "기본 그룹으로 자동 설정" primary: "주 그룹" no_primary: "(주 그룹이 없습니다.)" title: "그룹" @@ -2311,6 +2431,14 @@ ko: details: "게시글이 생성, 편집, 삭제, 재생될 때." user_event: name: "사용자 이벤트" + group_event: + name: "그룹 이벤트" + category_event: + name: "카테고리 이벤트" + tag_event: + name: "태그 이벤트" + flag_event: + name: "신고 이벤트" delivery_status: title: "발송 상태" inactive: "비활성화" @@ -2350,6 +2478,7 @@ ko: change_settings: "설정 변경" change_settings_short: "설정" howto: "플러그인은 어떻게 설치하나요?" + official: "공식 플러그인" backups: title: "백업" menu: @@ -2373,6 +2502,7 @@ ko: label: "업로드" title: "백업을 업로드" uploading: "업로드 중..." + uploading_progress: "업로드중... {{progress}}%" success: "'{{filename}}'이(가) 성공적으로 업로드 되었습니다. 파일은 처리중이며 몇분후에 리스트에 보여집니다." error: "'{{filename}}' 파일 업로드중 에러가 발생하였습니다. ({{message}})" operations: @@ -2413,7 +2543,7 @@ ko: screened_ip: "모든 표시된 IP 목록을 CSV 형식으로 내보내기" screened_url: "모든 표시된 URL 목록을 CSV 형식으로 내보내기" export_json: - button_text: "내보니기" + button_text: "내보내기" invite: button_text: "초대장 보내기" button_title: "초대장 보내기" @@ -2428,7 +2558,7 @@ ko: import: "가져오기" delete: "삭제" delete_confirm: "이 테마를 삭제할까요?" - color: "색" + color: "색상" opacity: "투명도" copy: "복사" copy_to_clipboard: "클립보드에 복사하기" @@ -2444,15 +2574,21 @@ ko: revert: "변경사항 취소" revert_confirm: "정말로 변경사항을 되돌리시겠습니까?" theme: + theme: "테마" + theme_name: "테마명" + browse_themes: "커뮤니티 테마 보기" import_theme: "테마 가져오기" customize_desc: "커스터마이즈:" title: "테마" + create: "만들기" + create_type: "종류:" long_title: "사이트의 색상, CSS, HTML 수정하기" edit: "편집" edit_confirm: "이 테마는 원격테마입니다. 업데이트가 실행될 때 편집된 CSS/HTML 내용이 사라지게 됩니다." common: "공통" desktop: "데스크톱" mobile: "모바일" + settings: "설정" preview: "미리보기" is_default: "기본값에 의해 테마가 활성화 되었습니다" user_selectable: "사용자가 테마를 선택하게 하기" @@ -2460,6 +2596,8 @@ ko: color_scheme_select: "테마에서 사용될 컬러 선택" custom_sections: "커스텀 섹션:" theme_components: "테마 컴포넌트:" + convert: "변환" + inactive_themes: "비활성 테마:" uploads: "업로드할 파일" no_uploads: "폰트나 이미지같은 테마와 관련된 파일만 업로드 가능합니다" add_upload: "업로드할 파일 추가하기" @@ -2483,6 +2621,8 @@ ko: updating: "업데이트 중..." up_to_date: "테마가 최신입니다, 마지막으로 체크한 일시:" add: "추가" + theme_settings: "테마 설정" + empty: "항목이 없습니다" commits_behind: other: "테마가 {{count}} 개 커밋 뒤쳐졌습니다!" scss: @@ -2639,6 +2779,7 @@ ko: filter: "필터:" title: "스태프 기록" clear_filters: "전체 보기" + staff_user: "사용자" target_user: "타겟 사용자" subject: "제목" when: "언제" @@ -2689,6 +2830,7 @@ ko: backup_destroy: "백업 삭제하기" reviewed_post: "리뷰된 글" custom_staff: "플러그인 커스텀 액션" + merge_user: "사용자 병합" screened_emails: title: "블락된 이메일들" description: "누군가가 새로운 계정을 만들면 아래 이메일 주소는 체크되고 등록은 블락됩니다, 또는 다른 조치가 취해집니다." @@ -2719,6 +2861,10 @@ ko: roll_up: text: "Roll up" title: "Creates new subnet ban entries if there are at least 'min_ban_entries_for_roll_up' entries." + search_logs: + title: "검색 로그" + types: + full_page: "전체 페이지" logster: title: "에러 로그" watched_words: @@ -2792,7 +2938,7 @@ ko: not_verified: "확인되지 않은" check_email: title: "사용자의 이메일 주소 표시" - text: "Show" + text: "보이기" user: suspend_failed: "이 사용자를 접근 금지하는데 오류 발생 {{error}}" unsuspend_failed: "이 사용자를 접근 허용 하는데 오류 발생 {{error}}" @@ -2804,6 +2950,8 @@ ko: suspend_message: "이메일 메시지" suspend_message_placeholder: "필요한 경우, 사용자에게 이메일을 통하여 일시정지에 대한 더 많은 정보를 제공할 수 있습니다." suspended_by: "접근 금지자" + silence_reason: "사유" + silence_message: "이메일 메시지" suspended_until: "(%{until} 까지)" cant_suspend: "이 사용자는 일시정지할 수 없습니다." delete_all_posts: "모든 글을 삭제합니다" @@ -2862,6 +3010,7 @@ ko: delete_confirm: "정말 이 사용자를 삭제하시겠습니다? 삭제하면 복구 할 수 없습니다." delete_and_block: "이 이메일과 IP주소를 삭제하고 차단하기" delete_dont_block: "삭제만 하기" + deleting_user: "사용자 삭제중..." deleted: "사용자가 삭제되었습니다." delete_failed: "해당 사용자를 삭제하는 동안 오류가 발생했습니다. 모든 글은 사용자를 삭제하기 전에 삭제해야합니다." send_activation_email: "인증 메일 보내기" @@ -2968,12 +3117,24 @@ ko: go_back: "검색으로 돌아가기" recommended: "다음의 텍스트를 요구에 맞게 편집하는 것을 권장:" show_overriden: 'Override 된 설정만 보여주기' + settings: + reset: '초기화' + none: '없음' site_settings: title: '사이트 설정' - no_results: "No results found." + no_results: "검색된 결과가 없습니다." + more_than_30_results: "30개 이상의 결과가 있습니다. 검색 범위를 좁히거나 카테고리를 선택하세요." clear_filter: "Clear" add_url: "URL 추가" add_host: "Host 추가" + uploaded_image_list: + label: "목록 수정" + empty: "아직 이미지가 없습니다. 업로드 해주세요." + upload: + label: "업로드" + title: "이미지 업로드" + selectable_avatars: + title: "사용자가 선택할 수있는 아바타 목록" categories: all_results: '전체' required: '필수' @@ -2991,6 +3152,7 @@ ko: developer: '개발자' embedding: "Embedding" legal: "법률조항" + api: 'API' user_api: '사용자 API' uncategorized: '카테고리 없음' backups: "백업" @@ -3036,6 +3198,7 @@ ko: enabled: 배지 기능 사용 icon: 아이콘 image: 이미지 + image_help: "이미지의 URL을 입력하세요 (둘 다 설정된 경우 아이콘 필드보다 우선함)" query: 배지 쿼리(SQL) target_posts: 글들을 대상으로 하는 쿼리 auto_revoke: 회수 쿼리를 매일 실행 @@ -3124,6 +3287,7 @@ ko: step: "%{total} 중 %{current}" upload: "업로드" uploading: "업로드 중..." + upload_error: "죄송합니다. 파일을 업로드하는 중 오류가 발생했습니다. 다시 시도하십시오." quit: "아마도 다음에" staff_count: other: "커뮤니티에는 당신을 포함한 %{count}명의 스탭이 있습니다." @@ -3134,3 +3298,7 @@ ko: admin: "관리자" moderator: "운영자" regular: "일반 유저" + previews: + topic_title: "토론 주제" + share_button: "공유" + reply_button: "댓글" diff --git a/config/locales/client.lt.yml b/config/locales/client.lt.yml index 4ffff9ae99..6e017eefd9 100644 --- a/config/locales/client.lt.yml +++ b/config/locales/client.lt.yml @@ -569,7 +569,6 @@ lt: topics_entered: "paskelbtos temos" post_count: "# įrašai" confirm_delete_other_accounts: "Ar tikrai nori ištrinti šias paskyras?" - powered_by: "pagamino ipinfo.io" copied: "nukopijuota" user_fields: none: "(pasirink nustatymą)" @@ -653,7 +652,6 @@ lt: watched_first_post_tags: "Stebime Pirmą Įrašą" watched_first_post_tags_instructions: "Jūs būsite perspėjami apie įrašus kėkvienoje naujoje temoje su pasirinktomis etiketėmis." muted_categories: "Nutildytos" - muted_categories_instructions: "Jums bus pranešta dėl naujų temų šioje kategorijoje." delete_account: "Ištrinti mano vartotoją" delete_account_confirm: "Ar tikrai norite visam laikui ištrinti savo paskyrą? Šis veiksmas yra galutinis." deleted_yourself: "Jūsų paskyra sėkmingai ištrinta." @@ -810,7 +808,6 @@ lt: always: "visada" never: "niekada" email_digests: - title: "Kaip nesilankau čia, atsiųsti santraupas man į elektroninį paštą apie populiarias temas ir populiarius įrašus" every_30_minutes: "kas 30 minučių" every_hour: "kas valandą" daily: "kas dieną" diff --git a/config/locales/client.lv.yml b/config/locales/client.lv.yml index 7d9400b2dd..7a8c99583c 100644 --- a/config/locales/client.lv.yml +++ b/config/locales/client.lv.yml @@ -524,7 +524,6 @@ lv: watched_first_post_tags: "Seko pirmajam ierakstam" watched_first_post_tags_instructions: "Jums paziņos par pirmo ierakstu katrā jaunajā tēmā ar šiem tagiem." muted_categories: "Klusināts" - muted_categories_instructions: "Jums nepaziņos neko par jaunām tēmām šajās sadaļās, un tās neparādīsies starp tēmām ar pēdējām izmaiņām." no_category_access: "Kā moderatoram jums ir ierobežota kategoriju piekļuve, saglabāšana ir atspējota." delete_account: "Izdzēst manu profilu" delete_account_confirm: "Vai esat drošs, ka vēlaties neatgriezeniski dzēst savu profilu? Šo darbību nevar atcelt!" @@ -661,7 +660,6 @@ lv: always: "vienmēr" never: "nekad" email_digests: - title: "Kad es neapmeklēju šo vietni, sūtīt man kopsavilkumu par populārām tēmām un atbildēm" every_30_minutes: "katras 30 minūtes" every_hour: "katru stundu" daily: "katru dienu" @@ -1569,7 +1567,6 @@ lv: upload: "Atvainojiet, augšuplādējot šo failu, notika kļūda. Lūdzu, mēģiniet vēlreiz." file_too_large: "Atvainojiet, šis fails ir pārāk liels (maksimālais izmērs ir {{max_size_kb}} kb). Varbūt augšuplādējiet jūsu lielo failu uz kādu mākoņservisu, bet pēc tam padalieties ar saiti?" too_many_uploads: "Atvainojiet, jūs varat augšuplādēt tikai vienu failu vienā reizē." - too_many_dragged_and_dropped_files: "Atvainojiet, jūs varat augšuplādēt tikai 10 failus vienā reizē." upload_not_authorized: "Atvainojiet, fails, ko mēģināt augšuplādēt, nav atļauts (atļautie paplašinājumi: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Atvainojiet, jaunie lietotāji nevar augšuplādēt attēlus." attachment_upload_not_allowed_for_new_user: "Atvainojiet, jaunie lietotāji nevar augšuplādēt pielikumus." diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index 5b07fe70a6..71fc106887 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -528,7 +528,6 @@ nb_NO: topics_entered: "emner åpnet" post_count: "# innlegg" confirm_delete_other_accounts: "Er du sikker på at du vil slette disse kontoene?" - powered_by: "drevet av ipinfo.io" user_fields: none: "(velg et alternativ)" user: @@ -612,7 +611,6 @@ nb_NO: watched_first_post_tags: "Følger første innlegg" watched_first_post_tags_instructions: "Du vil bli varslet om det første innlegget i hvert nye emne med disse stikkordene." muted_categories: "Ignorert" - muted_categories_instructions: "Du vil ikke bli varslet om noe som omhandler nye emner i disse kategoriene, og de vil ikke vises i siste." no_category_access: "Som moderator har du begrenset kategoritilgang, lagring er avskrudd." delete_account: "Slett kontoen min" delete_account_confirm: "Er du sikker på at du vil slette kontoen din permanent? Denne handlingen kan ikke angres!" @@ -685,15 +683,10 @@ nb_NO: second_factor: title: "Totrinnsverifisering" disable: "Skru av totrinnsverifisering" - enable: "Aktiver tofaktorautentisering for å øke kontosikkerheten" confirm_password_description: "Bekreft passordet ditt for å fortsette" label: "Kode" - enable_description: | - Les av denne QR-koden med en støttet app (AndroidiOSWindows Phone) og skriv inn autentiseringskoden din. disable_description: "Vennligst oppgi autentiseringskoden fra appen" show_key_description: "Skriv inn manuelt" - extended_description: | - Tofaktor autentisering øker sikkerheten for kontoen din ved at den krever en unik engangskode i tillegg til passordet. Kodene kan genereres med en Android,iOS eller Windows Phone enhet. oauth_enabled_warning: "Vær oppmerksom på at sosiale login-metoder deaktiveres når tofaktor autentisering er aktivert på kontoen din." change_about: title: "Rediger om meg" @@ -713,7 +706,6 @@ nb_NO: title: "Bytt profilbilde" gravatar: "Gravatar, basert på" gravatar_title: "Endre din avatar på Gravatars nettside" - gravatar_failed: "Kunne ikke hente Gravatar-bilde. Er det et tilknyttet den e-postadressen?" refresh_gravatar_title: "Oppdater din Gravatar" letter_based: "Systemtildelt profilbilde" uploaded_avatar: "Egendefinert bilde" @@ -789,7 +781,6 @@ nb_NO: always: "alltid" never: "aldri" email_digests: - title: "Send meg en oppsummering på e-post av populære emner og svar dersom jeg ikke besøker siden" every_30_minutes: "hvert 30 minutt" every_hour: "hver time" daily: "daglig" @@ -1855,7 +1846,6 @@ nb_NO: upload: "Det skjedde en feil når filen ble lastet opp. Prøv igjen senere. " file_too_large: "Beklager, den filen er for stor. Største tillatte størrelse er {{max_size_kb}} kb. Hvorfor ikke heller laste opp filen til en fildelingstjeneste og dele lenken?" too_many_uploads: "Du kan bare laste opp ett bilde av gangen." - too_many_dragged_and_dropped_files: "Beklager, du kan bare laste opp 10 filer om gangen." upload_not_authorized: "Beklager, filen du forsøket å laste opp er ikke tillatt. Tillatte filendelser er {{authorized_extensions}}." image_upload_not_allowed_for_new_user: "Beklager, nye brukere kan ikke laste opp bilder" attachment_upload_not_allowed_for_new_user: "Beklager, nye brukere kan ikke laste opp vedlegg." diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 1c04a9f5f1..d996fecc5b 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -508,7 +508,6 @@ nl: topics_entered: "topics ingevoerd" post_count: "# berichten" confirm_delete_other_accounts: "Weet u zeker dat u deze accounts wilt verwijderen?" - powered_by: "powered by ipinfo.io" copied: "gekopieerd" user_fields: none: "(selecteer een optie)" @@ -588,7 +587,6 @@ nl: watched_first_post_tags: "Eerste bericht in de gaten houden" watched_first_post_tags_instructions: "U ontvangt een melding bij het eerste bericht in elk nieuw topic met deze tags." muted_categories: "Genegeerd" - muted_categories_instructions: "U ontvangt geen melding over nieuwe topics en berichten in deze categorieën, en ze verschijnen niet in Nieuwste." no_category_access: "Als een beheerder heeft u beperkte toegang tot categorieën, opslaan is uitgeschakeld." delete_account: "Mijn account verwijderen" delete_account_confirm: "Weet u zeker dat u uw account definitief wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt!" @@ -727,7 +725,6 @@ nl: always: "altijd" never: "nooit" email_digests: - title: "Mij bij afwezigheid een e-mailsamenvatting van populaire topics en antwoorden sturen" every_30_minutes: "elke 30 minuten" every_hour: "elk uur" daily: "dagelijks" @@ -1647,7 +1644,6 @@ nl: upload: "Sorry, er is een fout opgetreden bij het uploaden van dat bestand. Probeer het opnieuw." file_too_large: "Sorry, dat bestand is te groot (maximale grootte is {{max_size_kb}} kB). Misschien kunt u dit bestand uploaden naar een cloudopslagdienst en de koppeling ernaar delen?" too_many_uploads: "Sorry, u kunt maar één bestand tegelijk uploaden." - too_many_dragged_and_dropped_files: "Sorry, u kunt maar 10 bestanden tegelijk uploaden." upload_not_authorized: "Sorry, het bestand dat u probeert te uploaden is niet geautoriseerd (geautoriseerde extensies: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Sorry, nieuwe gebruikers kunnen geen afbeeldingen uploaden." attachment_upload_not_allowed_for_new_user: "Sorry, nieuwe gebruikers kunnen geen bijlagen uploaden." diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 75f7cc2ba0..3a21a42f2d 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -573,7 +573,6 @@ pl_PL: topics_entered: "wprowadzone tematy:" post_count: "# wpisów" confirm_delete_other_accounts: "Czy na pewno chcesz usunąć wybrane konta?" - powered_by: "obsługiwane przez ipinfo.io" user_fields: none: "(wybierz opcję)" user: @@ -655,7 +654,6 @@ pl_PL: watched_first_post_tags: "Oglądasz pierwszy post" watched_first_post_tags_instructions: "Zostaniesz powiadomiony tylko o pierwszym wpisie w każdym nowym temacie oznaczonym tymi tagami." muted_categories: "Wyciszone" - muted_categories_instructions: "Nie będziesz powiadamiany o nowych tematach w tych kategoriach. Nie pojawią się na liście nieprzeczytanych." no_category_access: "Jako moderator masz limitowany dostęp do kategorii, możliwość zapisu jest wyłączona." delete_account: "Usuń moje konto" delete_account_confirm: "Czy na pewno chcesz usunąć swoje konto? To nieodwracalne!" @@ -717,12 +715,8 @@ pl_PL: disable: "Wyłącz dwuskładnikowe uwierzytelnianie" confirm_password_description: "Potwierdź swoje hasło, aby kontynuować" label: "Kod" - enable_description: | - Zeskanuj ten kod QR za pomocą wspieranej aplikacji (AndroidiOSWindows Phone) i podaj swój kod uwierzytelniający. disable_description: "Podaj kod uwierzytelniający ze swojej aplikacji" show_key_description: "Wpisz ręcznie" - extended_description: | - Dwuskładnikowe uwierzytelnianie zwiększa bezpieczeństwo twojego konta dzięki wymogowi podawanie jednorazowego tokenu oprócz twego hasła przy każdej próbie zalogowania. Tokeny można generować na urządzeniach z systemem Android, iOS lub Windows Phone. change_about: title: "Zmień O mnie" error: "Wystąpił błąd podczas zmiany tej wartości." @@ -811,7 +805,6 @@ pl_PL: always: "zawsze" never: "nigdy" email_digests: - title: "Gdy nie odwiedzam strony, wysyłaj e-mail podsumowujący z popularnymi tematami i odpowiedziami." every_30_minutes: "co 30 minut" every_hour: "co godzinę" daily: "codziennie" @@ -1869,7 +1862,6 @@ pl_PL: upload: "Przepraszamy, wystąpił błąd podczas wczytywania Twojego pliku. Proszę, spróbuj ponownie." file_too_large: "Przepraszamy, ale plik jest za duży (maksymalny rozmiar to {{max_size_kb}}kb). Załaduj plik na zewnętrzny serwer plików i dodaj udostępniony link." too_many_uploads: "Przepraszamy, ale możesz wgrać tylko jeden plik naraz." - too_many_dragged_and_dropped_files: "Przepraszamy, ale możesz wgrać tylko 10 plików naraz." upload_not_authorized: "Przepraszamy, plik który próbujesz wgrać jest nie dozwolony (dozwolone rozszerzenia: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Przepraszamy, ale nowi użytkownicy nie mogą wgrywać obrazów." attachment_upload_not_allowed_for_new_user: "Przepraszamy, ale nowi użytkownicy nie mogą wgrywać załączników." diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 6eec867f94..e29b6cf1f7 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -435,7 +435,6 @@ pt: topics_entered: "tópicos inseridos" post_count: "# publicações" confirm_delete_other_accounts: "Tem a certeza que quer apagar estas contas?" - powered_by: "desenvolvido por ipinfo.io" user_fields: none: "(selecione uma opção)" user: @@ -510,7 +509,6 @@ pt: watched_first_post_tags: "A Vigiar a Primeira Resposta" watched_first_post_tags_instructions: "Será notificado acerca da primeira resposta em cada novo tópico nestas categorias." muted_categories: "Silenciado" - muted_categories_instructions: "Não será notificado de nada acerca de novos tópicos nestas categorias, e estes não irão aparecer nos recentes." delete_account: "Eliminar A Minha Conta" delete_account_confirm: "Tem a certeza que pretende eliminar a sua conta de forma permanente? Esta ação não pode ser desfeita!" deleted_yourself: "A sua conta foi eliminada com sucesso." @@ -646,7 +644,6 @@ pt: always: "sempre" never: "nunca" email_digests: - title: "Quando eu não vier aqui, enviar-me uma mensagem resumo dos tópicos e respostas populares" every_30_minutes: "a cada 30 minutos" every_hour: "de hora em hora" daily: "diariamente" @@ -1559,7 +1556,6 @@ pt: upload: "Pedimos desculpa, ocorreu um erro ao carregar esse ficheiro. Por favor, tente novamente." file_too_large: "Lamentamos mas esse ficheiro é demasiado grande (o tamanho máximo é de {{max_size_kb}}kb). Porque não carregar o seu ficheiro grande para um serviço de partilha na nuvem e depois partilhar o link?" too_many_uploads: "Pedimos desculpa, só pode carregar um ficheiro de cada vez." - too_many_dragged_and_dropped_files: "Lamentamos mas só pode carregar 10 ficheiros de cada vez." image_upload_not_allowed_for_new_user: "Pedimos desculpa, os novos utilizadores não podem carregar imagens." attachment_upload_not_allowed_for_new_user: "Pedimos desculpa, os novos utilizadores não podem carregar anexos." attachment_download_requires_login: "Desculpe, precisa de iniciar a sessão para transferir anexos." diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index d00868b4c1..248ac89d09 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -528,7 +528,6 @@ pt_BR: topics_entered: "tópicos em que entrou" post_count: "# mensagens" confirm_delete_other_accounts: "Você tem certeza que deseja apagar essas contas?" - powered_by: "distribuído por ipinfo.io" user_fields: none: "(selecione uma opção)" user: @@ -612,7 +611,6 @@ pt_BR: watched_first_post_tags: "Observando a primeira mensagem" watched_first_post_tags_instructions: "Você será notificado sobre a primeira postagem em cada novo tópico com estas etiquetas." muted_categories: "Silenciado" - muted_categories_instructions: "Você não será notificado sobre novos tópicos nessas categorias, e não aparecerão no Recentes" no_category_access: "Como moderador, você tem acesso limitado à categoria, a economia está desativada." delete_account: "Excluir Minha Conta" delete_account_confirm: "Tem certeza de que deseja excluir permanentemente a sua conta? Essa ação não pode ser desfeita!" @@ -685,15 +683,10 @@ pt_BR: second_factor: title: "Autenticação de dois fatores" disable: "Desabilitar autenticação de dois fatores" - enable: "Habilitar autenticação de dois fatores para segurança avançada da conta" confirm_password_description: "Por favor, confirme sua senha para continuar" label: "Código" - enable_description: | - Digitalize este código QR em um aplicativo compatível (Android iOSWindows Phone) e insira seu código de autenticação. disable_description: "Por favor, insira o código de autenticação do seu aplicativo" show_key_description: "Entre manualmente" - extended_description: | - A autenticação de dois fatores adiciona segurança extra à sua conta, exigindo um token único além da sua senha. Os tokens podem ser gerados em Android, iOS, e Windows Phone dispositivos. oauth_enabled_warning: "Tenha em atenção que os logins sociais serão desativados quando a autenticação de dois fatores estiver ativada na sua conta." change_about: title: "Modificar Sobre Mim" @@ -713,7 +706,6 @@ pt_BR: title: "Alterar sua imagem de perfil" gravatar: "Gravatar, baseado em" gravatar_title: "Alterar seu avatar no site do Gravatar" - gravatar_failed: "Não foi possível buscar o Gravatar. Existe um associado a esse endereço de e-mail?" refresh_gravatar_title: "Atualize seu Gravatar" letter_based: "Sistema concedeu imagem de perfil." uploaded_avatar: "Foto pessoal" @@ -789,7 +781,6 @@ pt_BR: always: "sempre" never: "nunca" email_digests: - title: "Mande-me um email com o sumário dos tópicos e respostas populares quando eu não visitar o fórum" every_30_minutes: "a cada 30 minutos" every_hour: "a cada hora" daily: "diariamente" @@ -1855,7 +1846,6 @@ pt_BR: upload: "Desculpe, houve um erro ao enviar esse arquivo. Por favor, tente outra vez." file_too_large: "Desculpe, o arquivo que você está tentando enviar é muito grande (o tamanho máximo permitido é {{max_size_kb}}kb). Que tal enviar o seu arquivo grande para um serviço de hospedagem na nuvem e depois compartilhar o link?" too_many_uploads: "Desculpe, você pode enviar apenas um arquivos por vez." - too_many_dragged_and_dropped_files: "Desculpe, você só pode subir até 10 arquivos de cada vez." upload_not_authorized: "Desculpe, o arquivo que você está tentando enviar não é permitido (extensões permitidas: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Desculpe, novos usuário não podem enviar imagens." attachment_upload_not_allowed_for_new_user: "Desculpe, usuários novos não podem enviar anexos." diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index b390abc178..833101e2de 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -555,7 +555,6 @@ ro: topics_entered: "Subiecte la care particip" post_count: "# postări" confirm_delete_other_accounts: "Ești sigur că vrei să ștergi aceste conturi?" - powered_by: "creat de ipinfo.io" user_fields: none: "(alege o opțiune)" user: @@ -637,7 +636,6 @@ ro: watched_first_post_tags: "Urmărire activă prima postare" watched_first_post_tags_instructions: "Vei fi notificat cu privire la prima postare din fiecare nou subiect cu aceste etichete." muted_categories: "Setat pe silențios" - muted_categories_instructions: "Nu vei fi notificat despre nimic legat de noile subiecte din aceste categorii și subiectele respective nu vor apărea în lista cu cele mai recente subiecte." no_category_access: "În calitate de moderator ai acces limitat la categorii, salvarea este dezactivată." delete_account: "Șterge-mi contul" delete_account_confirm: "Ești sigur că vrei să ștergi contul? Această acțiune este ireversibilă!" @@ -709,7 +707,6 @@ ro: title: "Schimbă poza de profil" gravatar: "Gravatar, bazat pe" gravatar_title: "Schimbă-ți avatarul de pe site-ul Gravatar." - gravatar_failed: "Gravatarul nu a putut fi preluat. Există unul asociat cu adresa de email respectivă?" refresh_gravatar_title: "Reactualizează Gravatarul" letter_based: "Poză de profil atribuită de sistem." uploaded_avatar: "Poză preferată" @@ -778,7 +775,6 @@ ro: always: "întotdeauna" never: "niciodată" email_digests: - title: "Când nu vin aici în vizită, trimite-mi un email cu rezumatul subiectelor și răspunsurilor populare" every_30_minutes: "La fiecare 30 de minute " every_hour: "În fiecare oră" daily: "Zilnic" @@ -1776,7 +1772,6 @@ ro: upload: "Ne pare rău, a apărut o eroare la încărcarea fișierului. Te rugăm să încerci iar." file_too_large: "Ne pare rău, fișierul este prea mare (mărimea maximă este {{max_size_kb}}kb). De ce nu încarci acest fișier mare pe un serviciu de distribuție prin cloud și apoi să îi partajezi link-ul?" too_many_uploads: "Ne pare rău, poți încărca doar câte un fișier." - too_many_dragged_and_dropped_files: "Ne pare rău, poți încărca doar 10 fișiere simultan." image_upload_not_allowed_for_new_user: "Ne pare rău, un utilizator nou nu poate încărca imagini." attachment_upload_not_allowed_for_new_user: "Ne pare rău, un utilizator nou nu poate încărca atașamente." attachment_download_requires_login: "Ne pare rău, dar trebuie să fii autentificat pentru a descărca ataşamente." diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 2bd9e7ac8e..ff238afeee 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -654,7 +654,6 @@ ru: watched_first_post_tags: "Просмотр Первого сообщения" watched_first_post_tags_instructions: "Уведомлять только о первом сообщении в каждой новой теме с этими тегами." muted_categories: "Выключенные разделы" - muted_categories_instructions: "Не уведомлять меня о новых темах в этих разделах и не показывать новые темы на странице «Непрочитанные»." no_category_access: "Как модератор Вы ограничены в доступе к разделу, сохранения отклонены." delete_account: "Удалить мою учётную запись" delete_account_confirm: "Вы уверены, что хотите удалить свою учётную запись? Отменить удаление будет невозможно!" @@ -716,7 +715,6 @@ ru: second_factor: title: "Двухфакторная аутентификация" disable: "Отключить двухфакторную аутентификацию" - enable_description: "Сканируйте этот QR-код в поддерживаемом приложении (AndroidiOSWindows Phone) и введите свой код аутентификации. \n" change_about: title: "Изменить информацию обо мне" error: "При изменении значения произошла ошибка." @@ -810,7 +808,6 @@ ru: always: "всегда" never: "никогда" email_digests: - title: "В случае моего отсутствия на форуме, присылайте мне сводку популярных новостей" every_30_minutes: "каждые 30 минут" every_hour: "каждый час" daily: "ежедневно" @@ -1864,7 +1861,6 @@ ru: upload: "К сожалению, не удалось загрузить файл. Попробуйте ещё раз." file_too_large: "К сожалению, этот файл слишком большой (максимально допустимый размер {{max_size_kb}} КБ). Почему бы не загрузить Ваш большой файл в службу облачного обмена, а затем поделиться ссылкой?" too_many_uploads: "К сожалению, за один раз можно загрузить только одно изображение." - too_many_dragged_and_dropped_files: "К сожалению, за один раз можно загрузить только 10 файлов." upload_not_authorized: "К сожалению, вы не можете загрузить файл данного типа (список разрешённых типов файлов: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "К сожалению, загрузка изображений недоступна новым пользователям." attachment_upload_not_allowed_for_new_user: "К сожалению, загрузка файлов недоступна новым пользователям." diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 39afa26669..1991cb6b9d 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -583,7 +583,6 @@ sk: topics_entered: "založených tém" post_count: "# príspevkov" confirm_delete_other_accounts: "Ste si istý že chcete zmazať tieto účty?" - powered_by: "Beží cez ipinfo.io" user_fields: none: "(vyberte možnosť)" user: @@ -667,7 +666,6 @@ sk: watched_first_post_tags: "Sledovaný prvý príspevok" watched_first_post_tags_instructions: "Budete upozornený na prvý príspevok v každej novej téme s týmito štítkami." muted_categories: "Ignorované" - muted_categories_instructions: "Nebudete informovaný o udalostiach v nových témach týchto kategórií. Tieto témy sa zároveň nebudú zobrazovať v zozname posledných udalostí." no_category_access: "Ako moderátor máte obmedzený prístup ku kategóriám. Ukladanie nie je povolené." delete_account: "Vymazať môj účet" delete_account_confirm: "Ste si istý, že chcete permanentne vymazať váš účet? Táto akcia je nenávratná." @@ -736,11 +734,8 @@ sk: second_factor: title: "Dvojfaktorová autentifikácia" disable: "Vypnúť dvojfaktorovú autentifikáciu" - enable: "Zapnúť dvojfaktorovú autentifikáciu pre vylepšené zabezpečenie účtu" confirm_password_description: "Pre pokračovanie potvrďte svoje heslo prosím" label: "Kód" - enable_description: | - Oskentujte tento QR kód v podporovanej aplikácii (AndroidiOSWindows Phone) a zadajte váš overovací kód. disable_description: "Prosím zadajte autentifikačný kód z vašej aplikácie" show_key_description: "Vložiť ručne" change_about: @@ -829,7 +824,6 @@ sk: always: "vždy" never: "nikdy" email_digests: - title: "Keď nenavštevujem toto fórum, chcem dostať zhrnutie o populárnych témach a odpovediach emailom" every_30_minutes: "každých 30 mintút" every_hour: "každú hodinu" daily: "denne" @@ -1693,7 +1687,6 @@ sk: upload: "Ľutujeme, pri nahrávaní súboru nastala chyba. Prosím, skúste znovu." file_too_large: "Ľutujeme, daný súbor je príliš veľký (maximálna veľkosť je {{max_size_kb}}kB). Čo takto nahrať ten súbor na zdielané cloudové úložisko a nazdielať odkaz?" too_many_uploads: "Ľutujeme, ale naraz je možné nahrať iba jeden súbor." - too_many_dragged_and_dropped_files: "Ľutujeme, ale naraz je možné nahrať iba 10 súborov." image_upload_not_allowed_for_new_user: "Ľutujeme, noví použivatelia nemôžu nahrávať obrázky." attachment_upload_not_allowed_for_new_user: "Ľutujeme, noví používatelia nemôžu nahrávať prílohy." attachment_download_requires_login: "Ľutujeme, pre stiahnutie príloh musíte byť prihlásený." diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml index 230df00b8a..ff0b9978e6 100644 --- a/config/locales/client.sl.yml +++ b/config/locales/client.sl.yml @@ -478,7 +478,6 @@ sl: topics_entered: "vstop v teme" post_count: "# objave" confirm_delete_other_accounts: "Si prepričan, da želiš izbrisati te račune?" - powered_by: "podprto s strani ipinfo.io" user_fields: none: "(izberi možnost)" user: @@ -538,7 +537,6 @@ sl: tracked_categories: "Sledena" watched_first_post_tags: "Opazuješ prvo objavo" muted_categories: "Zamolčano" - muted_categories_instructions: "O temah v teh kategorijah ne boste obveščeni in ne bodo se pojavile med najnovejšimi." no_category_access: "Kot moderator imaš omejen dostop do kategorije, shranjevanje je onemogočeno" delete_account: "Izbriši Moj Račun" delete_account_confirm: "Si prepričan, da želiš trajno izbrisati svoj račun? Tega postopka ni mogoče razveljaviti!" @@ -584,10 +582,6 @@ sl: set_password: "Nastavi geslo" choose_new: "Izberite novo geslo" choose: "Izberite geslo" - second_factor: - enable: "Omogoči preverjanje v dveh korakih za večjo varnost uporabniškega računa." - extended_description: | - Preverjanje v dveh korakih poveča varnost vašega uporabniškega računa tako, da zahteva enkratno kodo poleg vašega gesla. KEnkratno kodo lahko ustvarite na Android, iOS in Windows Phone napravah. change_about: title: "Spremeni O meni" change_username: @@ -662,7 +656,6 @@ sl: always: "zmeraj" never: "nikoli" email_digests: - title: "Kadar ne obiščem redno, mi pošljite pregled popularnih tem in objav." every_30_minutes: "vsakih 30 minut" every_hour: "vsako uro" daily: "dnevno" @@ -1124,8 +1117,12 @@ sl: yes_value: "Ja, zavrži" controls: reply: "sestavi odgovor na to objavo" + like: "všečkaj to objavo" + has_liked: "všečkali ste to objavo" + undo_like: "razveljavi všeček" edit: "uredi objavo" edit_action: "Uredi" + edit_anonymous: "Oprostite vendar morate biti prijavljeni, da lahko uredite to objavo." delete: "izbriši objavo" share: "deli povezavo do te objave" more: "Več" @@ -1208,17 +1205,30 @@ sl: created: "Ustvarjeno" sort_ascending: 'Naraščajoče' sort_descending: 'Padajoče' + subcategory_list_styles: + rows: "Vrstice" + rows_with_featured_topics: "Vrstice z izpostavljenimi temami" flagging: + title: 'Hvala, da pomagate ohraniti civilizirano skupnost!' action: 'Postavi zastavico na objavo' + take_action: "Ukrepaj" notify_action: 'Sporočilo' + official_warning: 'Uradno opozorilo' + delete_spammer: "Izbriši spammerja" ip_address_missing: "(N/A)" hidden_email_address: "(skrito)" + notify_staff: 'Zasebno obvestite skrbnika' + formatted_name: + inappropriate: "Ni primerno" + spam: "Je nezaželeno" flagging_topic: + title: "Hvala, da pomagate ohraniti civilizirano skupnost!" action: "Postavi zastavico na objavo" notify_action: "Sporočilo" topic_map: title: "Povzetek teme" links_title: "Popularne povezave" + links_shown: "prikaži več povezav..." topic_statuses: pinned_globally: title: "Pripeto globalno" @@ -1342,6 +1352,7 @@ sl: flag: '! Prijavi objavo' badges: title: Značke + multiple_grant: "To lahko osvojite večkrat." tagging: all_tags: "Vse oznake" selector_all_tags: "vse oznake" @@ -1421,6 +1432,9 @@ sl: notify_user: "po meri" notify_moderators: "po meri" groups: + manage: + membership: + trust_levels_title: "Stopnja zaupanja, ki je avtomatsko dodeljena članom, ko so dodani:" primary: "Primarna skupina" no_primary: "(ni primarne skupine)" title: "Skupine" @@ -1801,12 +1815,14 @@ sl: delete_confirm: "Si prepričan/a, da želiš odstraniti to značko?" revoke: Odstrani reason: Razlog + no_badges: "Ni značk, ki bi lahko bile podeljene." icon: Ikona trigger: Sproži trigger_type: none: "Posodobi dnevno" post_revision: "Ko uporabnik ustvari ali uredi objavo" preview: + link_text: "Oglejte si podeljene značke" bad_count_warning: header: "POZOR!" sample: "Primer:" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 6c8caf02a4..1113295b94 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -470,7 +470,6 @@ sq: watched_first_post_tags: "Postimi i parë nën vëzhgim" watched_first_post_tags_instructions: "Ju do të njoftoheni vetëm për postimin e parë të çdo teme nën këto etiketa." muted_categories: "Pa njoftime" - muted_categories_instructions: "Ju nuk do të njoftoheni për asgjë nga temat e reja të këtyre kategorive, dhe këto tema nuk do të afishohen në faqen \"Më të fundit\" për ju. " delete_account: "Fshi llogarinë time" delete_account_confirm: "Jeni i sigurtë që dëshironi ta mbyllni përgjithmonë llogarinë tuaj? Ky veprim nuk mund të zhbëhet!" deleted_yourself: "Llogaria juaj u fshi me sukses." @@ -605,7 +604,6 @@ sq: always: "gjithmonë" never: "asnjëherë" email_digests: - title: "Kur nuk vij shpesh në faqe, më dërgoni një përmbledhje me email të diskutimeve më popullore" every_30_minutes: "çdo 30 minuta" every_hour: "çdo orë" daily: "çdo ditë" @@ -1349,7 +1347,6 @@ sq: upload: "Na vjen keq, pati një gabim gjatë ngarkimit të skedarit. Provo përsëri. " file_too_large: "Na vjen keq, skedari është shumë i madh (maksimumi i lejuar është {{max_size_kb}}kb). Mund t'a vendosni këtë skedar të madh në një faqe tjetër dhe të vendosni këtu vetëm lidhjen." too_many_uploads: "Na vjen keq, por duhet t'i ngarkoni skedarët një nga një." - too_many_dragged_and_dropped_files: "Na vjen keq, po ju mund të ngarkoni vetëm 10 skedarë njëkohësisht. " image_upload_not_allowed_for_new_user: "Na vjen keq, anëtarët e rinj nuk mund të ngarkojnë skedarë. " attachment_upload_not_allowed_for_new_user: "Na vjen keq, anëtarët e rinj nuk mund të ngarkojnë skedarë. " attachment_download_requires_login: "Na vjen keq, duhet të identifikoheni për të shkarkuar një dokument. " diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index cc65d5f135..c0a3288815 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -522,7 +522,6 @@ sv: watched_first_post_tags: "Bevakar första inlägget" watched_first_post_tags_instructions: "Du kommer att bli notifierad om första inlägget i varje nytt ämne med dessa taggar." muted_categories: "Tystad" - muted_categories_instructions: "Du kommer inte att få notifieringar om nya ämnen inom dessa kategorier, och de kommer inte att visas under bland dina olästa ämnen. " delete_account: "Radera mitt konto" delete_account_confirm: "Är du säker på att du vill ta bort ditt konto permanent? Denna åtgärd kan inte ångras!" deleted_yourself: "Ditt konto har tagits bort." @@ -647,7 +646,6 @@ sv: always: "alltid" never: "aldrig" email_digests: - title: "Skicka mig en e-postsammanfattning av populära ämnen och inlägg när jag inte besökt sidan" every_30_minutes: "var 30:e minut" every_hour: "varje timma" daily: "dagligen" @@ -1467,7 +1465,6 @@ sv: upload: "Tyvärr, det uppstod ett fel under uppladdandet av den filen. Vad god försök igen." file_too_large: "Tyvärr, filen är för stor (maximal filstorlek är {{max_size_kb}}kb). Varför inte ladda upp din stora fil till en moln-delningstjänst och sen dela länken?" too_many_uploads: "Tyvärr, du kan bara ladda upp en bild i taget." - too_many_dragged_and_dropped_files: "Tyvärr, du kan bara ladda upp 10 filer åt gången." upload_not_authorized: "Tyvärr, filen du försöker ladda upp är inte tillåten (tillåtna filtyper: %{authorized_extensions})." image_upload_not_allowed_for_new_user: "Tyvärr, nya användare kan inte ladda upp bilder." attachment_upload_not_allowed_for_new_user: "Tyvärr, nya användare kan inte bifoga filer." diff --git a/config/locales/client.sw.yml b/config/locales/client.sw.yml index 9181064a99..f52cad6884 100644 --- a/config/locales/client.sw.yml +++ b/config/locales/client.sw.yml @@ -523,7 +523,6 @@ sw: topics_entered: "mada zilizoingizwa" post_count: "# machapisho" confirm_delete_other_accounts: "Una uhakika unataka kufuta hizi akaunti?" - powered_by: "inasimamiwa na ipinfo.io" user_fields: none: "(Chagua chaguo moja)" user: @@ -607,7 +606,6 @@ sw: watched_first_post_tags: "Chapisho la Kwanza Linaangaliwa" watched_first_post_tags_instructions: "Utajulishwa kuhusu chapisho la kwanza kwenye kila mada mpya yenye lebo hizi." muted_categories: "Imenyamazishwa" - muted_categories_instructions: "Hautajulishwa kuhusu mada mpya kwenye kategoria hizi, na hazitatokea kama taarifa za hivi karibuni." no_category_access: "Kama msimamizi una ufikivu kidogo wa kategoria, hifadhi imesitishwa." delete_account: "Futa Akaunti Yangu" delete_account_confirm: "Una uhakika unataka kufuta akaunti yako? Kitendo hiki hakiwezi kufanyika tena!" @@ -676,15 +674,10 @@ sw: second_factor: title: "Uhalalalishaji wa Viwango Viwili" disable: "Zuia uhalalalishaji wa Viwango Viwili" - enable: "Wezesha two factor authentication Kuongeza ulinzi katika akaunti" confirm_password_description: "Thibitisha nywila yako kuendelea" label: "Kodi" - enable_description: | - Skani hii QR Code kwa kutumia application inayoweza kuskani (AndroidiOSWindows Phone) na andika kodi ya uthibitisho. disable_description: "Tafadhali andika kodi ya uthibitisho kutoka kwenye app yako" show_key_description: "Andika kwa mkono" - extended_description: | - Viwango viwili vya uthibitisho vinaweka ulinzi mkubwa kwenye akaaunti yako kwa sababu kitu nyongeza zaidi ya nywila kitahitajika kuingia kwenye akaunti. Vitu hivyo vinaweza kutengenezwa kwa ajili ya vifaa vya Android,iOS, na Windows Phone. oauth_enabled_warning: "Tafadhali jua kuwa kuingia kupitia mitandao itasitishwa kama uthibitisho wa kiwango cha pili umewezeshwa kwenye akaunti yako." change_about: title: "Badilisha Taarifa Zangu" @@ -704,7 +697,6 @@ sw: title: "Badilisha Picha yako" gravatar: "imetoka kwa Ishara" gravatar_title: "Badilisha Ishara kwenye Mtandao wa Ishara" - gravatar_failed: "Tumeshindwa kupata ishara. Je una ishara ambayo inatumika kwenye barua pepe yako ?" refresh_gravatar_title: "Onesha Upya Ishara Yako" letter_based: "Mfumo imekabidhi Picha" uploaded_avatar: "Picha Binafsi" @@ -778,7 +770,6 @@ sw: always: "mara kwa mara" never: "kamwe" email_digests: - title: "Nisipotembelea hapa, nitumie muhtasari wa barua pepe ulio na mada na majibu maarufu" every_30_minutes: "kila baada ya dakika 30" every_hour: "kila saa" daily: "kila siku" @@ -1820,7 +1811,6 @@ sw: upload: "Samahani, hitilafu imetokea wakati wa kupakia faili hilo. Tafadhali jaribu tena." file_too_large: "Samahani, hilo faili ni kubwa sana (kiwango cha juu ni {{max_size_kb}}kb). Kwa nini usipakie faili lako kubwa kwenye huduma ya kugawa kwenye wingu kama Google Drive, Dropbox au OneDrive, alafu ukaandika kiungo hapa?" too_many_uploads: "Samahani, unaweza kupakia faili 1 tu kwa wakati mmoja." - too_many_dragged_and_dropped_files: "Samahani, unaweza kupakia mafaili 10 tu kwa wakati mmoja." upload_not_authorized: "Samahani, faili unalo jaribu kupakia halina kibali (authorized extensions: {{authorized_extensions}})." image_upload_not_allowed_for_new_user: "Samahani, watumiaji wapya hawawezi kupakia picha." attachment_upload_not_allowed_for_new_user: "Samahani, watumiaji wapya hawawezi kupakia viambatanisho." diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml index 5f46d3c1be..4db0146009 100644 --- a/config/locales/client.th.yml +++ b/config/locales/client.th.yml @@ -467,7 +467,6 @@ th: watched_categories: "ดูแล้ว" tracked_categories: "ติดตาม" muted_categories: "ปิดเสียง" - muted_categories_instructions: "คุณจะไม่ได้รับการแจ้งเตือนใดๆเกี่ยวกับกระทู้ใหม่ในหมวดนี้ และจะไม่แสดงในเมนูยังไม่ได้อ่าน" delete_account: "ลบบัญชีของคุณ" delete_account_confirm: "คุณแน่ใจใหม่ที่จะลบบัญชีอย่างถาวร? การกระทำนี้ไม่สามารถยกเลิกได้" deleted_yourself: "คุณลบบัญชีเสร็จเเรียบร้อยแล้ว" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index c165f9baff..229b7f465b 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -125,6 +125,7 @@ tr_TR: user_left: "%{who} bu mesajdan ayrıldı %{when}" removed_user: "%{when} %{who} kaldırıldı" removed_group: "%{who} %{when} kaldırıldı" + autobumped: "otomatik olarak çarptı %{when}" autoclosed: enabled: '%{when} kapatıldı' disabled: '%{when} açıldı' @@ -149,6 +150,7 @@ tr_TR: topic_admin_menu: "konu yöneticisi eylemleri" wizard_required: "Yeni Discourse'una hoşgeldin! Haydi kuruluma başlayalım! Kurulum Sihirbazı ✨" emails_are_disabled: "Giden tüm e-postalar yönetici tarafından devre dışı bırakıldı. Herhangi bir e-posta bildirimi gönderilmeyecek." + bootstrap_mode_enabled: "Yeni sitenizi daha kolay başlatmak için önyükleme modundasınız. Tüm yeni kullanıcılara güven düzeyi 1 verilir ve günlük e-posta özet güncellemeleri etkinleştirilir. Bu,%{min_users} kullanıcının katıldığı zaman otomatik olarak kapatılacaktır." bootstrap_mode_disabled: "Önyükleme modu 24 saat içinde devre dışı bırakılacak. " themes: default_description: "Varsayılan " @@ -160,6 +162,7 @@ tr_TR: ap_southeast_1: "Asya Pasifik (Singapur)" ap_southeast_2: "Asya Pasifik (Sidney)" cn_north_1: "Çin (Pekin)" + cn_northwest_1: "Çin (Pekin)" eu_central_1: "AB (Frankfurt)" eu_west_1: "AB (İrlanda)" eu_west_2: "AB (Londra)" @@ -194,6 +197,7 @@ tr_TR: privacy_policy: "Gizlilik Sözleşmesi" privacy: "Gizlilik" tos: "Kullanım Koşulları" + rules: "Kurallar" mobile_view: "Mobil Görünüm" desktop_view: "Masaüstü Görünümü" you: "Sen" @@ -242,7 +246,9 @@ tr_TR: unbookmark: "Bu konudaki tüm işaretlenenleri kaldırmak için tıkla" bookmarks: created: "bu gönderiyi işaretledin" + not_bookmarked: "bu yazıya yer işareti koy" remove: "İşareti Kaldır" + confirm_clear: "Bu konudaki tüm yer işaretlerinizi silmek istediğinizden emin misiniz?" drafts: resume: "Sürdür" remove: "Kaldır" @@ -265,6 +271,8 @@ tr_TR: saved: "Kaydedildi!" upload: "Yükle" uploading: "Yükleniyor..." + uploading_filename: "Yükleme: {{filename}}..." + clipboard: "pano" uploaded: "Yüklendi!" pasting: "Yapıştırılıyor..." enable: "Etkinleştir" @@ -408,6 +416,7 @@ tr_TR: all: "Tüm Gruplar" empty: "Görünen hiçbir grup bulunmuyor. " filter: "Grup tipine göre filtrele" + owner_groups: "Sahip olduğum gruplar" close_groups: "Kapanmış Gruplar" automatic_groups: "Otomatik Gruplar" automatic: "Otomatik" @@ -511,6 +520,7 @@ tr_TR: topic_stat_sentence: one: "%{unit} beri %{count} yeni konu." other: "%{unit} beri %{count} yeni konu." + n_more: "Kategoriler (%{count}daha fazla) ..." ip_lookup: title: IP Adresi Ara hostname: Sunucu ismi @@ -526,7 +536,7 @@ tr_TR: topics_entered: "girilen konular" post_count: "# gönderi" confirm_delete_other_accounts: "Bu hesapları silmek isteğine emin misin?" - powered_by: "ipinfo.io tarafından güçlendirildi" + copied: "kopyalanan" user_fields: none: "(bir seçenek tercih et)" user: @@ -544,6 +554,7 @@ tr_TR: private_messages: "Mesajlar" activity_stream: "Aktivite" preferences: "Tercihler" + profile_hidden: "Bu kullanıcının genel profili gizli." expand_profile: "Genişlet" collapse_profile: "Daralt" bookmarks: "İşaretlenenler" @@ -608,7 +619,6 @@ tr_TR: watched_first_post_tags: "İlk gönderiyi izleme" watched_first_post_tags_instructions: "Bu etiketlerdeki her yeni konu başlığındaki ilk gönderi için bildirim alacaksın. " muted_categories: "Sessiz" - muted_categories_instructions: "Bu kategorilerdeki yeni konu başlıkları hakkında herhangi bir bildiri almayacaksın ve bu konu başlıkları en son gönderilerde gözükmeyecek." no_category_access: "Bir moderatör olarak kategori erişimin sınırlı ve kaydetme devre dışıdır." delete_account: "Hesabımı Sil" delete_account_confirm: "Hesabını kalıcı olarak silmek istediğine emin misin? Bu eylemi geri alamazsın!" @@ -627,6 +637,7 @@ tr_TR: revoke_access: "Erişimi İptal Et" undo_revoke_access: "Erişim İptalini Geri Al" api_approved: "Onaylanmış:" + api_last_used_at: "Son kullanılan:" theme: "Tema" home: "Varsayılan Anasayfa" staged: "Aşamalı" @@ -683,12 +694,8 @@ tr_TR: disable: "İki faktörlü kimlik doğrulamayı devre dışı bırak" confirm_password_description: "Devam etmek için lütfen şifrenizi onaylayın" label: "Kod" - enable_description: | - Desteklenen bir uygulamada bu QR kodunu tarayın (Android - iOS - Windows Phone) ve kimlik doğrulama kodunuzu girin. disable_description: "Lütfen kimlik doğrulama kodunu \"Uygulama\"dan gir" show_key_description: "Manuel olarak gir" - extended_description: | - İki faktörlü kimlik doğrulama, şifrenize ek olarak tek seferlik bir kod gerektirerek hesabınıza ek güvenlik ekler. Kodlar Android, iOS, ve Windows Phone cihazlar üzerinde oluşturulabilir. oauth_enabled_warning: "Hesabınızda iki faktörlü kimlik doğrulaması etkinleştirildikten sonra sosyal girişlerin devre dışı bırakılacağını lütfen unutmayın." change_about: title: "\"Hakkımda\"yı Değiştir" @@ -708,7 +715,6 @@ tr_TR: title: "Profil resmini değiştir" gravatar: "Gravatar, baz alındı" gravatar_title: "Profil görselini \"Gravatar web sitesi\"nde değiştir" - gravatar_failed: "Gravatar getirilemedi. Bu e-posta adresiyle ilişkili bir yönlendirme bulunuyor mu?" refresh_gravatar_title: "Gravatar profil görselini yenile" letter_based: "Sistem profil görseli atadı" uploaded_avatar: "Kişisel resim" @@ -764,6 +770,16 @@ tr_TR: any: "hiçbir" password_confirmation: title: "Şifre tekrarı" + auth_tokens: + title: "Son Kullanılan Cihazlar" + ip: "IP" + details: "Detaylar" + log_out_all: "Hepisini Çıkış Yap" + active: "şimdi aktif" + not_you: "Sen değil?" + show_all: "Tümünü göster ({{count}})" + show_few: "Daha az göster" + was_this_you: "Bu sen miydin?" last_posted: "Son Gönderi" last_emailed: "Gönderilen Son E-posta" last_seen: "Görüldü" @@ -784,7 +800,6 @@ tr_TR: always: "her zaman" never: "asla" email_digests: - title: "Ben burda değilken konuşulan popüler konu ve cevapların özetini e-posta olarak gönder" every_30_minutes: "Her 30 dakikada" every_hour: "saatlik" daily: "günlük" @@ -1838,7 +1853,6 @@ tr_TR: upload: "Üzgünüz, dosya yüklenirken bir hata oluştu. Lütfen tekrar dene." file_too_large: "Üzgünüz, bu dosya çok büyük (en fazla {{max_size_kb}}kb). Neden paylaşımını buluta yükleyip bağlantını paylaşmıyorsun ?" too_many_uploads: "Üzgünüz, aynı anda sadece tek dosya yüklenebilir." - too_many_dragged_and_dropped_files: "Üzgünüz, aynı anda sadece 10 dosya yükleyebilirsin." upload_not_authorized: "Üzgünüz, yüklemeye çalıştığın dosya izinli değil (izinli uzantılar : {{izinli uzantılar}})." image_upload_not_allowed_for_new_user: "Üzgünüz, yeni kullanıcılar resim yükleyemez." attachment_upload_not_allowed_for_new_user: "Üzgünüz, yeni kullanıcılar dosya yükleyemez." @@ -2869,9 +2883,13 @@ tr_TR: revert: "Değişiklikleri Geri Al" revert_confirm: "Değişiklikleri geri almak istediğine emin misin?" theme: + browse_themes: "Topluluk temalarına göz atın" import_theme: "Temayı İçe Aktar" customize_desc: "Kişiselleştir:" title: "Temalar" + create: "Oluştur" + create_type: "Tür:" + create_name: "İsim:" long_title: "Sitenin renklerini, CSS ve HTML içeriğini değiştir" edit: "Düzenle" edit_confirm: "Bu, uzak bir temadır. Eğer CSS / HTML'yi düzenlersen, temayı güncellediğinde değişiklikler silinir." @@ -2886,6 +2904,7 @@ tr_TR: color_scheme_select: "Temada kullanılacak renkleri seç" custom_sections: "İsteğe uyarlanmış bölümler:" theme_components: "Tema Öğeleri" + convert: "Dönüştür" uploads: "Yüklemeler" no_uploads: "Fontlar ve resimler gibi temayla ilişkili varlıkları yükleyebilirsin" add_upload: "Yükleme Ekle" @@ -2897,6 +2916,8 @@ tr_TR: no_overwrite: "Geçersiz değişken adı. Mevcut bir değişkenin üzerine yazılmamalıdır." must_be_unique: "Geçersiz değişken adı. Benzersiz olmalıdır." upload: "Yükle" + discard: "At" + stay: "Kalmak" css_html: "İsteğe uyarlanmış CSS/HTML" edit_css_html: "CSS/HTML Düzenle" edit_css_html_help: "Herhangi bir CSS veya HTML düzenlemedin" @@ -2904,9 +2925,11 @@ tr_TR: import_web_tip: "Veri havuzu içeren tema" import_file_tip: "Tema içeren .dcstyle.json dosyası" is_private: "Tema özel bir git veri havuzunda" + remote_branch: "Şube adı (isteğe bağlı)" public_key: "Repo'ya aşağıdaki genel anahtar erişimini ver:" about_theme: "Tema Hakkında" license: "Lisans" + component_of: "Bileşen:" update_to_latest: "Sona Doğru Güncelle" check_for_updates: "Güncellemeleri kontrol et" updating: "Güncelleniyor..." @@ -3073,6 +3096,7 @@ tr_TR: filter: "Filtrele:" title: "Görevli Eylemleri" clear_filters: "Hepsini Göster" + staff_user: "Kullanıcı" target_user: "Hedef Kullanıcı" subject: "Konu" when: "Ne zaman" diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 49b89e8ef9..8d305c6bc3 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -505,7 +505,6 @@ ur: topics_entered: " داخل ہوئے ٹاپک" post_count: "# پوسٹ" confirm_delete_other_accounts: "کیا آپ واقعی یہ اکاؤنٹس حذف کرنا چاہتے ہیں؟" - powered_by: "ipinfo.io کے مرہونِ مِنَّت" user_fields: none: "(ایک آپشن منتخب کریں)" user: @@ -586,7 +585,6 @@ ur: watched_first_post_tags: "پہلی پوسٹ پر نظر رکھی ہوئی ہے" watched_first_post_tags_instructions: "آپ کو اِن ٹیگ والے ہر نئے ٹاپک کی پہلی پوسٹ کے بارے میں مطلع کیا جائے گا۔" muted_categories: "خاموش کِیا ہوا" - muted_categories_instructions: "آپ کو اِن زمرہ جات میں موجود نئے ٹاپکس کی کسی بھی چیز کے بارے میں مطلع نہیں کیا جائے گا، اور یہ تازہ ترین میں بھی نظر نہیں آئیں گے۔" no_category_access: "ایک ماڈریٹر کے طور پر آپ کو زمرہ پر محدود رسائی حاصل ہے، محفوظ کرنا غیر فعال ہے۔" delete_account: "میرا اکاؤنٹ حذف کریں" delete_account_confirm: "کیا آپ واقعی مستقل طور پر اپنا اکاؤنٹ حذف کرنا چاہتے ہیں؟ اس عمل کو کالعدم نہیں کیا جا سکتا!" @@ -647,12 +645,8 @@ ur: disable: "دو فیکٹر توثیق غیر فعال کریں" confirm_password_description: "جاری رکھنے کیلئے براہ کرم اپنے پاسوَرڈ کی تصدیق کریں" label: "کَوڈ" - enable_description: | - اِس QR کَوڈ کو کسی قابل اَیپ (اینڈرائڈiOSوِنڈَوز فون) میں اسکَین کریں اور اپنا توثیقی کَوڈ جمع کریں۔ disable_description: "براہ مہربانی اپنی اَیپ میں سے توثیقی کَوڈ درج کریں" show_key_description: "دستی طور پر درج کریں" - extended_description: | - دو فیکٹر توثیق پاسوَرڈ کے ساتھ ساتھ ایک دفعہ کے ٹَوکن کو مانگ کر آپ کے اکاؤنٹ کیلئے اضافی سیکورٹی شامل کرتا ہے۔ ٹَوکن اینڈرائڈ، iOS، اور وِنڈَوز فون ڈیوائسوں پر تخلیق کیے جا سکتے ہیں۔ oauth_enabled_warning: "براہ مہربانی نوٹ کریں کہ ایک بار جب آپ کے اکاؤنٹ پر دو فیکٹر توثیق فعال ہو جائے تو سَوشَل لاگ اِن غیر فعال ہوجائیں گے۔" change_about: title: "\"میرے بارے میں\" تبدیل کریں" @@ -671,7 +665,6 @@ ur: title: "اپنے پروفائل کی تصویر تبدیل کریں" gravatar: "گَرِیوَّٹار، کی بنیاد پر" gravatar_title: "گَرِیوَّٹار کی ویب سائٹ پر اپنے اوتار کو تبدیل کریں" - gravatar_failed: "گَرَیوَٹار درآمد نہیں کرایا جاسکا۔ کیا اس ای میل ایڈریس کے ساتھ ایک منسلک ہے؟ " refresh_gravatar_title: "اپنے گَرِیوَّٹار کو رِیفریش کریں" letter_based: "سسٹم تفویض کردہ پروفائل تصویر" uploaded_avatar: "اپنی مرضی کی تصویر" @@ -739,7 +732,6 @@ ur: always: "ہمیشہ" never: "کبھی نہیں " email_digests: - title: "جب میں یہاں کا دورا نہ کروں، مجھے مقبول ٹاپک اور جوابات کا ایک اِی میل خلاصہ بھیجیں" every_30_minutes: "ہر 30 منٹ" every_hour: "گھنٹہ وار" daily: "روزانہ " @@ -1768,7 +1760,6 @@ ur: upload: "معذرت، یہ فائل اَپ لوڈ کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ براہ مہربانی دوبارہ کوشش کریں۔" file_too_large: "معذرت، یہ فائل بہت بڑی ہے (زیادہ سے زیادہ سائز {{max_size_kb}}kb) ہے۔ کیوں نہ آپ اپنی بڑی فائل ایک کلاؤڈ شیئرنگ سروس پر اَپ لوڈ کریں اور اس کا لنک شیئر کریں؟" too_many_uploads: "معذرت، آپ ایک وقت میں صرف ایک ہی فائل اَپ لوڈ کر سکتے ہیں۔" - too_many_dragged_and_dropped_files: "معذرت، آپ ایک وقت میں صرف 10 فائلیں ہی اَپ لوڈ کر سکتے ہیں۔" upload_not_authorized: "معذرت، جو فائل آپ اَپ لوڈ کرنے کے کوشش کر رہے ہیں اُس کی اجازت نہیں ہے (اجازت یافتہ ایکسٹینشنز: {{authorized_extensions}})۔" image_upload_not_allowed_for_new_user: "معذرت، نئے صارفین تصاویر اَپ لوڈ نہیں کر سکتے۔" attachment_upload_not_allowed_for_new_user: "معذرت، نئے صارفین اٹیچمنٹس اَپ لوڈ نہیں کر سکتے۔" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 06892855ab..3e397a99fd 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -495,7 +495,6 @@ vi: watched_first_post_tags: "Xem bài viết đầu tiên" watched_first_post_tags_instructions: "Bạn sẽ nhận được thông báo khi có ai đó đăng chủ đề mới có chứa thẻ này." muted_categories: "Im lặng" - muted_categories_instructions: "Bạn sẽ không bao giờ được thông báo về bất cứ điều gì về các chủ đề mới trong các chuyên mục này, và chúng sẽ không hiển thị mới nhất" delete_account: "Xoá Tài khoản của tôi" delete_account_confirm: "Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của bạn? Hành động này không thể được hoàn tác!" deleted_yourself: "Tài khoản của bạn đã được xóa thành công." @@ -628,7 +627,6 @@ vi: always: "luôn luôn" never: "không" email_digests: - title: "Khi tôi không ghé thăm, hãy gửi cho tôi email tóm tắt về các chủ đề nhiều người quan tâm nhất và các câu trả lời." every_30_minutes: "mỗi 30 phút" every_hour: "hàng giờ" daily: "hàng ngày" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 3180b8f97d..1b60701bb2 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -9,8 +9,8 @@ zh_CN: js: number: format: - separator: "." - delimiter: "," + separator: "。" + delimiter: "," human: storage_units: format: '%n %u' @@ -90,7 +90,7 @@ zh_CN: placeholder: 日期 share: topic: '分享指向这个主题的链接' - post: '#%{postNumber} 楼' + post: '#%{postNumber}楼' close: '关闭' twitter: '分享至 Twitter' facebook: '分享至 Facebook' @@ -105,7 +105,7 @@ zh_CN: user_left: "%{who}于%{when}离开了该私信" removed_user: "于%{when}移除了%{who}" removed_group: "于%{when}移除了%{who}" - autobumped: "自动碰撞 %{when}" + autobumped: "%{when}自动顶帖" autoclosed: enabled: '于%{when}关闭' disabled: '于%{when}打开' @@ -128,9 +128,9 @@ zh_CN: enabled: '%{when}将其设置为横幅。在用户关闭横幅前,横幅将显示在每一页的顶部。' disabled: '%{when}撤销了该横幅。横幅将不在显示每一页的顶部。' topic_admin_menu: "管理主题" - wizard_required: "欢迎来到你新安装的 Discourse!让我们开始设置向导✨" + wizard_required: "欢迎来到你新安装的Discourse!让我们开始设置向导✨" emails_are_disabled: "出站邮件已经被管理员全局禁用。将不发送任何邮件提醒。" - bootstrap_mode_enabled: "为方便站点准备发布,其正处于初始化模式中。所有新用户将被授予信任等级1,并为他们设置接受每日邮件摘要。初始化模式会在用户数超过 %{min_users} 个时关闭。" + bootstrap_mode_enabled: "为方便站点准备发布,其正处于初始化模式中。所有新用户将被授予信任等级1,并为他们设置接受每日邮件摘要。初始化模式会在用户数超过%{min_users}个时关闭。" bootstrap_mode_disabled: "初始化模式将会在24小时后关闭。" themes: default_description: "默认" @@ -224,8 +224,15 @@ zh_CN: unbookmark: "点击删除本主题的所以书签" bookmarks: created: "已经收藏了" + not_bookmarked: "收藏此贴" remove: "取消收藏" confirm_clear: "你确定要清空这个主题中的所有书签?" + drafts: + resume: "复位" + remove: "移除" + new_topic: "新主题草稿" + new_private_message: "新私信草稿" + topic_reply: "草稿回复" topic_count_latest: other: "有 {{count}} 个更新或新主题" topic_count_unread: @@ -239,6 +246,8 @@ zh_CN: saved: "已保存!" upload: "上传" uploading: "上传中…" + uploading_filename: "上传中:{{filename}}..." + clipboard: "剪贴板" uploaded: "上传成功!" pasting: "粘贴中…" enable: "启用" @@ -312,14 +321,16 @@ zh_CN: other: "%{count} 位用户" group_histories: actions: - change_group_setting: "更改小组设置" + change_group_setting: "更改群组设置" add_user_to_group: "增加用户" remove_user_from_group: "移除用户" make_user_group_owner: "设为所有者" remove_user_as_group_owner: "撤销所有者" groups: + member_added: "已添加" add_members: title: "添加成员" + description: "管理该组成员" usernames: "用户名" manage: title: '管理' @@ -330,6 +341,8 @@ zh_CN: profile: title: 个人信息 interaction: + title: 交互 + posting: 发帖 notification: 通知 membership: title: 成员资格 @@ -338,12 +351,14 @@ zh_CN: title: "日志" when: "时间" action: "操作" + acting_user: "操作用户" + target_user: "目标用户" subject: "主题" details: "详情" from: "从" to: "到" - public_admission: "允许用户自由加入小组(需要小组公开可见)" - public_exit: "允许用户自由离开小组" + public_admission: "允许用户自由加入群组(需要群组公开可见)" + public_exit: "允许用户自由离开群组" empty: posts: "群组成员没有发布帖子。" members: "群组没有成员。" @@ -356,12 +371,12 @@ zh_CN: leave: "离开" request: "请求" message: "私信" - allow_membership_requests: "允许用户向小组拥有者发送成员请求" + allow_membership_requests: "允许用户向群组拥有者发送成员请求" membership_request_template: "用户发送会员请求时向其显示的自定义模板" membership_request: submit: "提交成员申请" title: "申请加入%{group_name}" - reason: "向小组拥有者说明你为何属于这个小组" + reason: "向群组拥有者说明你为何属于这个群组" membership: "成员资格" name: "群组ID" group_name: "群组名" @@ -374,6 +389,7 @@ zh_CN: all: "所有群组" empty: "没有可见的群组。" filter: "根据群组类型筛选" + owner_groups: "拥有的群组" close_groups: "封闭群组" automatic_groups: "自动分组" automatic: "自动" @@ -388,24 +404,27 @@ zh_CN: is_group_user: "成员" is_group_owner: "所有者" title: - other: "小组" + other: "群组" activity: "活动" members: title: "成员" filter_placeholder_admin: "用户名或电子邮件" filter_placeholder: "用户名" remove_member: "移除成员" + remove_member_description: "从群组中移除%{username}" make_owner: "设为所有者" + make_owner_description: "使%{username}成为群组所有者" remove_owner: "撤销所有者" + remove_owner_description: "把%{username}从群组所有者中移除" owner: "所有者" topics: "主题" posts: "帖子" mentions: "提及" messages: "私信" - notification_level: "小组私信的默认通知等级" + notification_level: "群组私信的默认通知等级" alias_levels: - mentionable: "谁能@该小组" - messageable: "谁能私信此小组" + mentionable: "谁能@该群组" + messageable: "谁能私信此群组" nobody: "没有人" only_admins: "管理员" mods_and_admins: "版主与管理员" @@ -448,6 +467,7 @@ zh_CN: "12": "发送" "13": "收件" "14": "待定" + "15": "草稿" categories: all: "所有分类" all_subcategories: "全部" @@ -486,7 +506,8 @@ zh_CN: topics_entered: "进入的主题" post_count: "# 帖子" confirm_delete_other_accounts: "确定要删除这些账户?" - powered_by: "由ipinfo.io驱动" + powered_by: "使用MaxMindDB" + copied: "已复制" user_fields: none: "(选择一项)" user: @@ -504,6 +525,7 @@ zh_CN: private_messages: "私信" activity_stream: "活动" preferences: "设置" + profile_hidden: "此用户公共信息已被影藏" expand_profile: "展开" collapse_profile: "折叠" bookmarks: "收藏" @@ -513,6 +535,7 @@ zh_CN: notifications: "通知" statistics: "统计" desktop_notifications: + label: "实时通知" not_supported: "通知功能暂不支持该浏览器。抱歉。" perm_default: "启用通知" perm_denied_btn: "拒绝授权" @@ -520,6 +543,7 @@ zh_CN: disable: "停用通知" enable: "启用通知" each_browser_note: "注意:你必须在你使用的所用浏览器中更改这项设置。" + consent_prompt: "有回复时是否接收通知?" dismiss: '忽略' dismiss_notifications: "忽略所有" dismiss_notifications_tooltip: "标记所有未读通知为已读" @@ -551,6 +575,7 @@ zh_CN: individual_no_echo: "为每个除了我发表的新帖发送一封邮件通知" many_per_day: "为每个新帖给我发送邮件(大约每天 {{dailyEmailEstimate}} 封)" few_per_day: "为每个新帖给我发送邮件(大约每天 2 封)" + warning: "邮件列表模式启用。邮件通知设置被覆盖。" tag_settings: "标签" watched_tags: "监看" watched_tags_instructions: "你将自动监看有这些标签的所有主题。所有新帖子和新主题会通知你,新帖数量也会显示在主题旁边。" @@ -567,11 +592,12 @@ zh_CN: watched_first_post_tags: "监看头一帖" watched_first_post_tags_instructions: "在有了这些标签的每一个新主题,第一帖会通知你。" muted_categories: "静音" - muted_categories_instructions: "在这些分类里面,你将不会收到新主题任何通知,它们也不会出现在“最新”主题列表。" - no_category_access: "无法保存,作为审核人您仅具有受限的 分类 访问权限" + muted_categories_instructions: "你不会收到有关这些分类中新主题的任何通知,也不会出现在类别或最新页面上。" + no_category_access: "无法保存,作为审核人你仅具有受限的 分类 访问权限" delete_account: "删除我的帐号" delete_account_confirm: "你真的要永久删除自己的帐号吗?删除之后无法恢复!" deleted_yourself: "你的帐号已被删除。" + delete_yourself_not_allowed: "想删除账户请联系管理人员。" unread_message_count: "私信" admin_delete: "删除" users: "用户" @@ -585,6 +611,7 @@ zh_CN: revoke_access: "撤销许可" undo_revoke_access: "解除撤销许可" api_approved: "已批准:" + api_last_used_at: "最后使用于:" theme: "主题" home: "默认主页" staged: "暂存" @@ -623,18 +650,39 @@ zh_CN: set_password: "设置密码" choose_new: "输入新密码" choose: "输入密码" + second_factor_backup: + title: "两步备份码" + regenerate: "重新生成" + disable: "停用" + enable: "启用" + enable_long: "启用备份码" + manage: "管理备份码" + copied_to_clipboard: "已复制到剪贴板" + copy_to_clipboard_error: "复制到剪贴板时出错" + remaining_codes: "你有{{count}}个备份码" + codes: + title: "备份码生成" + description: "每个备份码只能使用一次。请存放于安全可读的地方。" second_factor: title: "双重验证" disable: "停用双重验证" + enable: "启用两步认证加强账号安全" confirm_password_description: "确认密码以继续" label: "编码" + rate_limit: "请等待另一个验证码。" + enable_description: | + 在支持的应用中扫描此二维码(Android - iOS并输入验证码。 disable_description: "请输入来自 app 的验证码" show_key_description: "手动输入" + extended_description: | + 双重身份验证除了你的密码之外还需要一次性令牌,从而为你的帐户增加了额外的安全性。 可以在AndroidiOS设备。 + oauth_enabled_warning: "请注意,一旦你的帐户启用了双重身份验证,系统就会停用社交登录。" change_about: title: "更改个人信息" error: "提交修改时出错了" change_username: title: "更换用户名" + confirm: "你确定要更改用户名吗?" taken: "抱歉,此用户名已经有人使用了。" invalid: "此用户名不合法,用户名只能包含字母和数字" change_email: @@ -647,7 +695,7 @@ zh_CN: title: "更换头像" gravatar: "Gravatar头像,基于:" gravatar_title: "在 Gravatar 网站上更改你的头像" - gravatar_failed: "无法获取 Gravatar 。有关联到该电子邮件地址的 Gravatar ?" + gravatar_failed: "我们找不到包含该电子邮件地址的Gravatar。" refresh_gravatar_title: "刷新你的 Gravatar 头像" letter_based: "默认头像" uploaded_avatar: "自定义图片" @@ -663,6 +711,9 @@ zh_CN: instructions: "显示在用户卡片中,上传的图片将被居中且默认宽度为 590px。" email: title: "邮箱" + primary: "主邮箱" + secondary: "次邮箱" + no_secondary: "没有次邮箱" instructions: "永不公开显示" ok: "将通过邮件验证确认" invalid: "请填写正确的邮箱地址" @@ -670,6 +721,11 @@ zh_CN: frequency_immediately: "如果你没有阅读过摘要邮件中的相关内容,将立即发送电子邮件给你。" frequency: other: "仅在 {{count}} 分钟内没有访问时发送邮件给你。" + associated_accounts: + title: "关联账户" + connect: "连接" + revoke: "撤销" + not_connected: "(没有连接)" name: title: "昵称" instructions: "你的全名(可选)" @@ -694,6 +750,20 @@ zh_CN: any: "任意" password_confirmation: title: "请再次输入密码" + auth_tokens: + title: "最近使用的设备" + ip: "IP" + details: "详情" + log_out_all: "全部登出" + active: "现在活跃" + not_you: "不是你?" + show_all: "显示所有({{count}})" + show_few: "显示部分" + was_this_you: "这是你吗?" + was_this_you_description: "如果不是你,我们建议你更改密码并在任何地方注销。" + browser_and_device: "{{browser}}在{{device}}" + secure_account: "保护我的帐户" + latest_post: "你上次发布了......" last_posted: "最后发帖" last_emailed: "最后邮寄" last_seen: "最后活动" @@ -702,6 +772,7 @@ zh_CN: location: "地点" website: "网址" email_settings: "邮箱" + hide_profile_and_presence: "隐藏我的公开个人资料和状态功能" like_notification_frequency: title: "用户被赞时通知提醒" always: "始终" @@ -753,6 +824,7 @@ zh_CN: title: "邀请" user: "邀请用户" sent: "已发送" + none: "无邀请显示。" truncated: other: "只显示前 {{count}} 个邀请。" redeemed: "确认邀请" @@ -831,6 +903,9 @@ zh_CN: most_liked_users: "赞谁最多" most_replied_to_users: "最多回复至" no_likes: "暂无赞。" + top_categories: "热门分类" + topics: "主题" + replies: "回复" ip_address: title: "最后使用的 IP 地址" registration_ip_address: @@ -840,6 +915,7 @@ zh_CN: header_title: "个人页面、私信、书签和设置" title: title: "头衔" + none: "(无)" filters: all: "全部" stream: @@ -875,6 +951,9 @@ zh_CN: enabled: "站点正处于只读模式。你可以继续浏览,但是回复、赞和其他操作暂时被禁用。" login_disabled: "只读模式下不允许登录。" logout_disabled: "站点在只读模式下无法登出。" + too_few_topics_and_posts_notice: "让我们开始讨论!目前有%{currentTopics} / %{requiredTopics}个主题和%{currentPosts} / %{requiredPosts}个帖子。新访客需要能够阅读和回复一些对话。" + too_few_topics_notice: "让我们开始讨论!目前有%{currentTopics} / %{requiredTopics}个主题。新访客需要能够阅读和回复一些讨论。" + too_few_posts_notice: "让我们开始讨论!目前有%{currentPosts} / %{requiredPosts}个帖子。新访客需要能够阅读和回复一些讨论。" logs_error_rate_notice: reached: "%{relativeAge}%{rate} 达到了站点设置中的 %{siteSettingRate}。" exceeded: "[%{relativeAge}] 目前的错误率 %{rate} 已超出了站点设置中的 %{siteSettingRate}。" @@ -906,6 +985,8 @@ zh_CN: hide_session: "明天提醒我" hide_forever: "不了" hidden_for_session: "好的,我会在明天提醒你。不过你随时都可以使用“登录”来创建账户。" + intro: "你好!看起来你正在享受讨论,但你尚未注册帐户。" + value_prop: "当你创建了帐号,我们就可以准确地记录你的阅读进度。这样你再次访问时就可以回到上次离开的地方。只要有人回复你,你也可以通过此处和电子邮件收到通知。而且你还可以赞帖子,向他人分享感激之情。:heartbeat:" summary: enabled_description: "你正在查看主题的精简摘要版本:一些社区公认有意思的帖子。" description: "有 {{replyCount}} 个回复。" @@ -919,6 +1000,8 @@ zh_CN: disable: "显示已删除的帖子" private_message_info: title: "私信" + invite: "邀请其他人..." + edit: "添加或移除..." leave_message: "你真的想要发送消息么?" remove_allowed_user: "确定将 {{name}} 从本条私信中移除?" remove_allowed_group: "确定将 {{name}} 从本条私信中移除?" @@ -950,15 +1033,26 @@ zh_CN: email_login: link_label: "给我通过邮件发送一个登录链接" button_label: "通过邮件" + complete_username: "如果帐户与用户名%{username}匹配,你很快就会收到一封带有登录链接的电子邮件。" + complete_email: "如果帐户与%{email}匹配,你很快就会收到一封带有登录链接的电子邮件。" + complete_username_found: "我们找到了一个与用户名%{username}匹配的帐户,你很快就会收到一封带有登录链接的电子邮件。" + complete_email_found: "我们发现了一个与%{email}相匹配的帐户,你很快就会收到一封带有登录链接的电子邮件。" + complete_username_not_found: "没有帐户与用户名%{username}匹配" + complete_email_not_found: "没有帐户匹配%{email}" login: title: "登录" username: "用户" password: "密码" second_factor_title: "双重验证" second_factor_description: "请输入来自 app 的验证码:" + second_factor_backup: "使用备用码登录" + second_factor_backup_title: "两步验证备份" + second_factor_backup_description: "请输入你的备份码:" + second_factor: "使用Authenticator app登陆" email_placeholder: "电子邮件或者用户名" caps_lock_warning: "大写锁定开启" error: "出错了" + cookies_error: "你的浏览器似乎禁用了Cookie。如果不先启用它们,你可能无法登录。" rate_limit: "请请稍后再重试" blank_username: "请输入你的邮件地址或用户名。" blank_username_or_password: "请输入你的邮件地址或用户名,以及密码。" @@ -973,7 +1067,7 @@ zh_CN: not_allowed_from_ip_address: "你使用的 IP 地址已被封禁。" admin_not_allowed_from_ip_address: "你不能从这个 IP 地址以管理员身份登录。" resend_activation_email: "点击此处重新发送激活邮件。" - omniauth_disallow_totp: "您的账户已启用双重验证,请使用密码登录。" + omniauth_disallow_totp: "你的账户已启用双重验证,请使用密码登录。" resend_title: "重发激活邮件" change_email: "更改邮件地址" provide_new_email: "给个新地址!然后我们会再给你发一封确认邮件。" @@ -984,21 +1078,27 @@ zh_CN: forgot: "我记不清帐号详情了" not_approved: "你的帐号还未通过审核。一旦审核通过,我们将邮件通知你。" google_oauth2: + name: "Google" title: "Google 登录" message: "正在通过 Google 帐号验证登录(请确保浏览器没有禁止弹出窗口)" twitter: + name: "Twitter" title: "Twitter 登录" message: "正在通过 Twitter 帐号验证登录(请确保浏览器没有禁止弹出窗口)" instagram: + name: "Instagram" title: "Instagram 登录" message: "正在通过 Instagram 帐号验证登录(请确保浏览器没有禁止弹出窗口)" facebook: + name: "Facebook" title: "Facebook 登录" message: "正在通过 Facebook 帐号验证登录(请确保浏览器没有禁止弹出窗口)" yahoo: + name: "Yahoo" title: "Yahoo 登录" message: "正在通过 Yahoo 帐号验证登录(请确保浏览器没有禁止弹出窗口)" github: + name: "GitHub" title: "GitHub 登录" message: "正在通过 GitHub 帐号验证登录(请确保浏览器没有禁止弹出窗口)" invites: @@ -1008,7 +1108,7 @@ zh_CN: social_login_available: "你也可以通过任何使用这个邮箱的社交网站登陆。" your_email: "你的帐户邮箱地址是%{email}。" accept_invite: "接受邀请" - success: "已创建您的帐号,您现在可以登录了。" + success: "已创建你的帐号,你现在可以登录了。" name_label: "昵称" password_label: "设置密码" optional_description: "(可选)" @@ -1027,6 +1127,8 @@ zh_CN: categories_with_featured_topics: "有推荐主题的分类" categories_and_latest_topics: "分类和最新主题" categories_and_top_topics: "分类和最热主题" + categories_boxes: "带子分类的框" + categories_boxes_with_topics: "有特色主题的框" shortcut_modifier_key: shift: 'Shift' ctrl: 'Ctrl' @@ -1037,8 +1139,12 @@ zh_CN: default_header_text: 选择… no_content: 无符合的结果 filter_placeholder: 搜索…… + filter_placeholder_with_any: 搜索或创建... + create: "创建:“{{content}}”" max_content_reached: - other: "您只能选择 {{count}} 条记录。" + other: "你只能选择 {{count}} 条记录。" + min_content_not_reached: + other: "选择至少{{count}}条。" emoji_picker: filter_placeholder: 查找表情符号 people: 人物 @@ -1058,6 +1164,11 @@ zh_CN: dark_tone: 深肤色 shared_drafts: title: "共享草稿" + notice: "只有那些可以看到{{category}}分类的人才能看到此主题。" + destination_category: "目标分类" + publish: "发布共享草稿" + confirm_publish: "你确定要发布此草稿吗?" + publishing: "发布主题中......" composer: emoji: "Emoji :)" more_emoji: "更多…" @@ -1074,6 +1185,7 @@ zh_CN: saved_local_draft_tip: "已本地保存" similar_topics: "你的主题有点类似于…" drafts_offline: "离线草稿" + group_mentioned_limit: "警告!你提到了 {{group}} ,但该群组的成员数超过了的管理员配置的最大{{max}}人数。没人会收到通知。" group_mentioned: other: "提及 {{group}} 时,你将通知 {{count}} 人 - 确定吗?" cannot_see_mention: @@ -1088,7 +1200,9 @@ zh_CN: post_length: "帖子至少应有 {{min}} 个字" try_like: '试试 按钮?' category_missing: "未选择分类" + tags_missing: "你必须至少选择{{count}}个标签" save_edit: "保存编辑" + overwrite_edit: "覆盖编辑" reply_original: "回复原始主题" reply_here: "在此回复" reply: "回复" @@ -1096,6 +1210,8 @@ zh_CN: create_topic: "创建主题" create_pm: "私信" create_whisper: "密语" + create_shared_draft: "创建共享草稿" + edit_shared_draft: "编辑共享草稿" title: "或 Ctrl + 回车" users_placeholder: "添加用户" title_placeholder: "一句话概况讨论内容…" @@ -1105,6 +1221,8 @@ zh_CN: topic_featured_link_placeholder: "在标题里输入链接" remove_featured_link: "从主题中移除链接。" reply_placeholder: "在此键入。使用Markdown,BBCode,或HTML格式。可拖拽或粘贴图片。" + reply_placeholder_no_images: "在此输入。 使用Markdown,BBCode或HTML格式。" + reply_placeholder_choose_category: "在此处输入前你必须选择一个分类。" view_new_post: "浏览新帖。" saving: "保存中" saved: "已保存!" @@ -1123,6 +1241,7 @@ zh_CN: link_description: "输入链接描述" link_dialog_title: "插入链接" link_optional_text: "可选标题" + link_url_placeholder: "https://example.com" quote_title: "引用" quote_text: "引用" code_title: "预格式化文本" @@ -1137,6 +1256,8 @@ zh_CN: help: "Markdown 编辑帮助" collapse: "最小号编辑面板" abandon: "关闭编辑面板并放弃草稿" + enter_fullscreen: "进入全屏编辑模式" + exit_fullscreen: "退出全屏编辑模式" modal_ok: "确认" modal_cancel: "取消" cant_send_pm: "抱歉,你不能向 %{username} 发送私信。" @@ -1148,6 +1269,9 @@ zh_CN: reply: 回复 draft: 草稿 edit: 编辑 + reply_to_post: + label: "通过%{postUsername}回复帖子%{postNumber}" + desc: 回复特定帖子 reply_as_new_topic: label: 回复为联结主题 desc: 创建一个新主题链接到这一主题 @@ -1156,10 +1280,18 @@ zh_CN: desc: 新建一个私信 reply_to_topic: label: 回复主题 + desc: 回复主题,不是任何特定的帖子 + toggle_whisper: + label: 切换密语 + desc: 只有管理人员才能看到密语 create_topic: label: "新主题" shared_draft: label: "共享草稿" + desc: "起草一个只对管理人员可见的主题" + toggle_topic_bump: + label: "切换主题置顶" + desc: "回复而不更改最新回复日期" notifications: tooltip: regular: @@ -1198,8 +1330,10 @@ zh_CN: quoted: '{{username}}在“{{topic}}”引用了你的帖子 - {{site_title}}' replied: '{{username}}在“{{topic}}”回复了你 - {{site_title}}' posted: '{{username}}在“{{topic}}”中发布了帖子 - {{site_title}}' + private_message: '{{username}}在“{{topic}}”中向你发送了个人消息 - {{site_title}}' linked: '{{username}}在“{{topic}}”中链接了你的帖子 - {{site_title}}' confirm_title: '通知已启用 - %{site_title}' + confirm_body: '成功!通知已启用。' upload_selector: title: "插入图片" title_with_attachments: "上传图片或文件" @@ -1225,6 +1359,8 @@ zh_CN: select_all: "全选" clear_all: "清除所有" too_short: "你的搜索词太短。" + result_count: + other: "{{count}}{{plus}}结果{{term}}" title: "搜索主题、帖子、用户或分类" full_page_title: "搜索主题或帖子" no_results: "没有找到结果。" @@ -1232,7 +1368,7 @@ zh_CN: searching: "搜索中…" post_format: "#{{post_number}} 来自于 {{username}}" results_page: "关于“{{term}}”的搜索结果" - more_results: "还有更多结果。请增加您的搜索条件。" + more_results: "还有更多结果。请增加你的搜索条件。" cant_find: "找不到你要找的内容?" start_new_topic: "不如创建一个新主题?" or_search_google: "或者尝试使用Google进行搜索:" @@ -1258,6 +1394,7 @@ zh_CN: label: 标签 filters: label: 只返回主题/帖子…… + title: 仅在标题中匹配 likes: 我给了赞的 posted: 我参与发帖 watching: 我正在监看 @@ -1359,6 +1496,7 @@ zh_CN: title: '移动到收件箱' help: '移动私信到收件箱' edit_message: + help: '编辑消息中的第一帖' title: '编辑消息' list: '主题' new: '近期主题' @@ -1464,6 +1602,8 @@ zh_CN: jump_prompt_of: "%{count} 帖子" jump_prompt_long: "你想跳转至哪一贴?" jump_bottom_with_number: "跳至第 %{post_number} 帖" + jump_prompt_to_date: "至今" + jump_prompt_or: "或" total: 全部帖子 current: 当前帖子 notifications: @@ -1525,6 +1665,7 @@ zh_CN: reset_read: "重置阅读数据" make_public: "设置为公共主题" make_private: "设置为私信" + reset_bump_date: "重置顶帖日期" feature: pin: "置顶主题" unpin: "取消置顶主题" @@ -1622,9 +1763,12 @@ zh_CN: action: "合并选择的帖子" error: "合并选择的帖子试出错。" change_owner: + title: "更改所有者" action: "更改作者" error: "更改帖子作者时发生错误。" placeholder: "新作者的用户名" + instructions: + other: "请选择@{{old_user}}创建的{{count}}个帖子的新作者。" change_timestamp: title: "修改时间" action: "修改时间" @@ -1642,6 +1786,10 @@ zh_CN: title: '单击以将帖子从中移除' select_replies: label: '选择与回复' + title: '选择帖子及其所有回复' + select_below: + label: '选择 +以下' + title: '选择帖子及其后的所有内容' delete: 删除所选 cancel: 取消选择 select_all: 全选 @@ -1665,6 +1813,7 @@ zh_CN: other: "(帖子被作者删除,如无标记将在 %{count} 小时后自动删除)" collapse: "折叠" expand_collapse: "展开/折叠" + locked: "一管理人员锁定了该帖的编辑" gap: other: "查看 {{count}} 个隐藏回复" unread: "未读帖子" @@ -1683,7 +1832,7 @@ zh_CN: upload: "抱歉,在上传文件时发生了错误。请重试。" file_too_large: "文件过大(最大 {{max_size_kb}}KB)。为什么不就大文件上传至云存储服务后再分享链接呢?" too_many_uploads: "抱歉,一次只能上传一张图片。" - too_many_dragged_and_dropped_files: "抱歉,一次只能上传 10 个文件。" + too_many_dragged_and_dropped_files: "抱歉,你一次只能上传最多{{max}}个文件。" upload_not_authorized: "抱歉,你没有上传文件的权限(验证扩展:{{authorized_extensions}})。" image_upload_not_allowed_for_new_user: "抱歉,新用户无法上传图片。" attachment_upload_not_allowed_for_new_user: "抱歉,新用户无法上传附件。" @@ -1714,6 +1863,11 @@ zh_CN: share: "分享指向这个帖子的链接" more: "更多" delete_replies: + confirm: "你也想删除该贴的回复?" + direct_replies: + other: "是,{{count}}个直接回复" + all_replies: + other: "是,所有{{count}}个回复" just_the_post: "不,只是这篇帖子" admin: "帖子管理" wiki: "公共编辑" @@ -1727,6 +1881,9 @@ zh_CN: lock_post: "锁定帖子" lock_post_description: "禁止发帖者编辑这篇帖子" unlock_post: "解锁帖子" + unlock_post_description: "允许发布者编辑帖子" + delete_topic_disallowed_modal: "你无权删除该贴。如果你真想删除,向版主提交原因并标记。" + delete_topic_disallowed: "你无权删除此主题" actions: flag: '标记' defer_flags: @@ -1785,6 +1942,9 @@ zh_CN: other: "{{count}} 人收藏了这个帖子" like: other: "{{count}} 人赞了它" + delete: + confirm: + other: "你确定要删除{{count}}个帖子吗?" merge: confirm: other: "确定要合并这 {{count}} 个帖子吗?" @@ -1825,6 +1985,7 @@ zh_CN: can: '能够… ' none: '(未分类)' all: '所有分类' + choose: '分类&hellip;' edit: '编辑' edit_long: "编辑" view: '浏览分类的主题' @@ -1836,7 +1997,7 @@ zh_CN: tags_allowed_tag_groups: "仅在该 分类 内可以使用的标签组" tags_placeholder: "(可选)允许使用的标签列表" tag_groups_placeholder: "(可选)允许使用的标签组列表" - topic_featured_link_allowed: "运行在该分类中发布特色链接" + topic_featured_link_allowed: "允许在该分类中发布特色链接标题" delete: '删除分类' create: '新分类' create_long: '创建新的分类' @@ -1873,6 +2034,7 @@ zh_CN: show_subcategory_list: "在这个分类中把子分类列表显示在主题的上面" num_featured_topics: "分类页面上显示的主题数量:" subcategory_num_featured_topics: "父分类页面上的推荐主题数量:" + all_topics_wiki: "默认将新主题设为维基主题" subcategory_list_style: "子分类列表样式:" sort_order: "列表样式,按排序:" default_view: "默认主题列表:" @@ -1880,13 +2042,17 @@ zh_CN: allow_badges_label: "允许在这个分类中授予徽章" edit_permissions: "编辑权限" add_permission: "添加权限" + require_topic_approval: "所有新主题需要版主审批" + require_reply_approval: "所有新回复需要版主审批" this_year: "今年" + position: "位置:" default_position: "默认位置" position_disabled: "分类按照其活跃程度的顺序显示。要固定分类列表的显示顺序," position_disabled_click: '启用“固定分类位置”设置。' minimum_required_tags: '在一个主题中允许的最少标签数量:' parent: "上级分类" num_auto_bump_daily: '每天自动碰撞的主题的数量' + navigate_to_first_post_after_read: '阅读主题后导航到第一个帖子' notifications: watching: title: "监看" @@ -2084,6 +2250,7 @@ zh_CN: this_week: "周" today: "今天" other_periods: "查看热门" + browser_update: '抱歉,你的浏览器版本太低,无法正常访问该站点。请升级你的浏览器。' permission_types: full: "创建 / 回复 / 阅读" create_post: "回复 / 阅读" @@ -2103,6 +2270,7 @@ zh_CN: bookmarks: 'g, b 书签' profile: 'g 然后 p 个人页面' messages: 'g, m 私信' + drafts: 'g草稿' navigation: title: '导航' jump: '# 前往帖子 #' @@ -2122,6 +2290,10 @@ zh_CN: dismiss_new_posts: 'x 然后 r 解除新/帖子提示' dismiss_topics: 'x 然后 t 解除主题提示' log_out: 'shift+z shift+z 退出' + composing: + title: '编辑' + return: 'shift+c返回编辑器' + fullscreen: 'shift+F11全屏编辑器' actions: title: '动作' bookmark_topic: 'f 切换主题收藏状态' @@ -2157,6 +2329,8 @@ zh_CN: granted: other: "%{count} 授予" select_badge_for_title: 选择一个徽章作为你的头衔使用 + none: "(无)" + successfully_granted: "成功将%{badge}授予%{username}" badge_grouping: getting_started: name: 开始 @@ -2179,6 +2353,7 @@ zh_CN:

    tagging: all_tags: "所有标签" + other_tags: "其他标签" selector_all_tags: "所有标签" selector_no_tags: "无标签" changed: "标签被修改:" @@ -2195,6 +2370,10 @@ zh_CN: sort_by_name: "名称" manage_groups: "管理标签组" manage_groups_description: "管理标签的群组" + upload: "上传标签" + upload_description: "上传文本文件以批量创建标签" + upload_instructions: "每行一个,可选带有'tag_name,tag_group'格式的标签组。" + upload_successful: "标签上传成功" filters: without_category: "%{tag}的%{filter}主题" with_category: "%{filter} %{tag}主题在%{category}" @@ -2203,15 +2382,19 @@ zh_CN: notifications: watching: title: "监看" + description: "你将自动监看该标签中的所有主题。你将收到所有新帖子和主题的通知,此外,主题旁边还会显示未读和新帖子的数量。" watching_first_post: title: "监看头一帖" + description: "你只会收到此标签中每个新主题中第一篇帖子的通知。" tracking: title: "跟踪" + description: "你将使用此标签自动跟踪所有主题。未读和新帖计数将显示在主题旁。" regular: title: "普通" description: "如果有人@你或回复你的帖子,将通知你。" muted: title: "静音" + description: "你不会收到有关此标签的新主题的任何通知,也不会显示在未读标签上。" groups: title: "标签组" about: "将标签分组以便管理。" @@ -2225,6 +2408,9 @@ zh_CN: save: "保存" delete: "删除" confirm_delete: "确定要删除此标签组吗?" + everyone_can_use: "每个人都可以使用标签" + usable_only_by_staff: "标签对所有人可见,但只有管理人员可以使用它们" + visible_only_to_staff: "标签仅对管理人员可见" topics: none: unread: "你没有未读主题。" @@ -2247,9 +2433,11 @@ zh_CN: bookmarks: "没有更多收藏的主题了。" search: "没有更多搜索结果了。" invite: + custom_message: "通过编写自定义消息,使你的邀请更个性化。" custom_message_placeholder: "输入留言" custom_message_template_forum: "你好,你应该来我们这个论坛!" custom_message_template_topic: "你好,我觉得你可能会喜欢这个主题!" + forced_anonymous: "由于极端负载,暂时向所有人显示,已注销用户会看到它。" safe_mode: enabled: "安全模式已经开启,关闭该浏览器窗口以退出安全模式" admin_js: @@ -2257,9 +2445,13 @@ zh_CN: admin: title: 'Discourse 管理员' moderator: '版主' + reports: + title: "可用报告列表" dashboard: title: "仪表盘" last_updated: "最近更新于:" + find_old: "寻找旧的仪表板?" + old_link: "在这里访问" version: "安装的版本" up_to_date: "你正在运行最新的论坛版本。" critical_available: "有一个关键更新可用。" @@ -2284,14 +2476,29 @@ zh_CN: space_free: "{{size}} 空闲" uploads: "上传" backups: "备份" + lastest_backup: "最新:%{date}" traffic_short: "流量" traffic: "应用 web 请求" page_views: "浏览量" page_views_short: "浏览量" show_traffic_report: "显示详细的流量报告" + community_health: 社区健康 + moderators_activity: 版主活跃度 + whats_new_in_discourse: Discourse中有什么新东西? + activity_metrics: 活跃指标 + all_reports: "所有报告" + general_tab: "常规" + moderation_tab: "适度" + disabled: 停用 + timeout_error: 对不起,查询时间太长,请选择较短的间隔 + exception_error: 抱歉,执行查询时发生错误 + too_many_requests: 你执行这个操作太多次了,请稍后再试。 reports: + trend_title: "%{percent}更改。目前%{current},在前一期间是%{prev}。" today: "今天" yesterday: "昨天" + last_7_days: "过去7天" + last_30_days: "过去30天" all_time: "所有时间内" 7_days_ago: "7天之前" 30_days_ago: "30天之前" @@ -2302,6 +2509,13 @@ zh_CN: start_date: "开始日期" end_date: "结束日期" groups: "所有群组" + disabled: "此报告已禁用" + totals_for_sample: "总计样本" + total: "所有时间总计" + no_data: "没有数据显示。" + trending_search: + more: '搜索日志' + disabled: '趋势搜索报告已停用。启用记录搜索查询以收集数据。' commits: latest_changes: "最近的更新:请经常升级!" by: "来自" @@ -2324,6 +2538,7 @@ zh_CN: agree_flag: "保持帖子" agree_flag_title: "确认标记并保持帖子不变。" ignore_flag: "忽略" + ignore_flag_title: "移除标记;这次不处理。" delete: "删除" delete_title: "删除标记指向的帖子。" delete_post_defer_flag: "删除帖子并忽略标记" @@ -2342,6 +2557,10 @@ zh_CN: more: "(更多回复…)" suspend_user: "禁用用户" suspend_user_title: "因该贴禁用用户" + replies: + other: "[%{count} 个回复]" + delete_replies: + other: "同时删除对此帖子的%{count}回复?" dispositions: agreed: "已确认" disagreed: "被否决" @@ -2359,12 +2578,22 @@ zh_CN: was_edited: "帖子在第一次标记后被编辑" previous_flags_count: "这篇帖子已经被标记了 {{count}} 次。" show_details: "显示标记细节" + user_percentage: + summary: + other: "{{agreed}},{{disagreed}},{{ignored}},({{count}}标记总数)" + agreed: + other: "{{count}}%同意" + disagreed: + other: "{{count}}%不同意" + ignored: + other: "{{count}}%忽略" details: "详情" flagged_topics: topic: "主题" type: "类型" users: "用户" last_flagged: "最后标记" + no_results: "没有被标记的主题。" short_names: off_topic: "离题" inappropriate: "不恰当" @@ -2375,11 +2604,40 @@ zh_CN: new: title: "新建群组" create: "创建" + name: + too_short: "群组名太短" + too_long: "群组名太长" + checking: "查看群组名是否可用……" + available: "群组名可用" + not_available: "群组名不可用" + blank: "群组名不能为空" + bulk_add: + title: "批量添加到群组" + complete_users_not_added: "这些用户还没有被添加(请确保他们已经有账户了):" + paste: "粘贴用户名邮件列表,一行一个:" + add_members: + as_owner: "设置用户为此群组拥有者" manage: interaction: email: 邮箱 + incoming_email: "自定义进站电子邮件地址" + incoming_email_placeholder: "输入邮箱地址" + visibility: 可见 + visibility_levels: + title: "此群组对谁可见?" + public: "所有人" + members: "群组拥有者,成员和管理员" + staff: "群组拥有者及管理人员" + owners: "群组拥有者及管理员" membership: + automatic: 自动 + trust_level: 信任等级 + trust_levels_title: "这些用户加入时,将自动赋予信任等级:" trust_levels_none: "无" + automatic_membership_email_domains: "用户注册时邮箱域名若与列表完全匹配则自动添加至这个群组:" + automatic_membership_retroactive: "应用同样的邮件域名规则添加已经注册的用户" + primary_group: "自动设置为主要群组" + name_placeholder: "群组名,没有空格,和用户名一样的规则" primary: "主要群组" no_primary: "(无主要群组)" title: "群组" @@ -2394,10 +2652,12 @@ zh_CN: add: "添加" custom: "定制" automatic: "自动" + default_title: "默认标题" + default_title_description: "将会应用给此群组所有用户" group_owners: 所有者 add_owners: 添加所有者 - none_selected: "选择一个小组以开始" - no_custom_groups: "创建一个自定义小组" + none_selected: "选择一个群组以开始" + no_custom_groups: "创建一个自定义群组" api: generate_master: "生成主 API 密钥" none: "当前没有可用的 API 密钥。" @@ -2452,8 +2712,21 @@ zh_CN: details: "当有新回复、编辑、帖子被删除或者恢复时。" user_event: name: "用户事件" + details: "当用户登陆,退出,创建,批准或更新时。" + group_event: + name: "群组事件" + details: "当群组创建,更新或销毁时。" + category_event: + name: "分类事件" + details: "当分类创建,更新或销毁时。" + tag_event: + name: "标签事件" + details: "当标签创建,更新或销毁时。" flag_event: name: "标记事件" + details: "当标签创建,审批通过,审批不通过或忽略时。" + queued_post_event: + name: "帖子审批事件" delivery_status: title: "分发状态" inactive: "不活跃" @@ -2493,6 +2766,7 @@ zh_CN: change_settings: "更改设置" change_settings_short: "设置" howto: "如何安装插件?" + official: "官方插件" backups: title: "备份" menu: @@ -2516,6 +2790,8 @@ zh_CN: label: "上传" title: "上传备份至实例" uploading: "上传中…" + uploading_progress: "上传中……{{progress}}%" + success: "'{{filename}}'已成功上传。文件正在被处理将在几分钟后显示在列表中。" error: "上传“{{filename}}”时出错:{{message}}" operations: is_running: "已有操作正在执行" @@ -2528,6 +2804,7 @@ zh_CN: label: "备份" title: "建立一个备份" confirm: "你确定要开始建立一个备份吗?" + without_uploads: "是(不包括上传)" download: label: "下载" title: "发送含下载链接的邮件" @@ -2544,6 +2821,9 @@ zh_CN: label: "回滚" title: "将数据库回滚到之前的工作状态" confirm: "你确定要将数据库回滚到之前的工作状态吗?" + location: + local: "本地" + s3: "Amazon S3" export_csv: success: "导出开始,完成后你将被通过私信通知。" failed: "导出失败。请检查日志。" @@ -2586,9 +2866,22 @@ zh_CN: revert: "撤销更变" revert_confirm: "你确定要撤销你的更变吗?" theme: + theme: "主题" + component: "组件" + components: "组件" + theme_name: "主题名称" + component_name: "组件名称" + themes_intro: "选择已存在的风格主题或创建一个新以开始。" + beginners_guide_title: "使用Discourse风格主题的初学者指导" + developers_guide_title: "Discourse风格主题的开发者指导" + browse_themes: "浏览社区风格主题" import_theme: "导入主题" customize_desc: "定制:" title: "主题" + create: "创建" + create_type: "类型:" + create_name: "名称:" + name_too_short: "名称至少4字节。" long_title: "修改你站点的色彩、CSS 和 HTML" edit: "编辑" edit_confirm: "这是一个远程主题。如果你编辑了 CSS/HTML,在下一次更新该主题后这些自定义项目将会被删除。" @@ -2603,6 +2896,9 @@ zh_CN: color_scheme_select: "选择主题使用的颜色" custom_sections: "自定义段落:" theme_components: "主题组件" + convert: "转换" + convert_component_alert: "你确定转换此组件到风格主题吗?将会从组件%{relatives}中移除。" + convert_component_tooltip: "转换此组件到风格主题" uploads: "上传" no_uploads: "你可以上传与主题相关的组件,例如字体和图片" add_upload: "添加上传" @@ -3334,3 +3630,6 @@ zh_CN: admin: "管理员" moderator: "版主" regular: "普通用户" + previews: + share_button: "分享" + reply_button: "回复" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 188206e903..3857cbdae4 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -25,20 +25,20 @@ zh_TW: thousands: "{{number}} 千" millions: "{{number}} 百萬" dates: - time: "h:mm" - timeline_date: "MMM YYYY" - long_no_year: "MMM D h:mm a" - long_no_year_no_time: "MMM D" - full_no_year_no_time: "MMMM Do" - long_with_year: "YYYY MMM D h:mm a" - long_with_year_no_time: "YYYY MMM D" - full_with_year_no_time: "YYYY MMMM Do" - long_date_with_year: "'YY MMM D LT" - long_date_without_year: "MMM D, LT" - long_date_with_year_without_time: "'YY MMM D" - long_date_without_year_with_linebreak: "MMM D
    LT" - long_date_with_year_with_linebreak: "'YY MMM D
    LT" - wrap_ago: "%{date}以前" + time: "h:mm a" + timeline_date: "YYYY年 M月" + long_no_year: "M月 D日 h:mm a" + long_no_year_no_time: "M月 D日" + full_no_year_no_time: "M月 D日" + long_with_year: "YYYY年 M月 D日 h:mm a" + long_with_year_no_time: "YYYY年 M月 D日" + full_with_year_no_time: "YYYY年 M月 D日" + long_date_with_year: "YYYY年 M月 D日 LT" + long_date_without_year: "M月 D日 LT" + long_date_with_year_without_time: "YYYY年 M月 D日" + long_date_without_year_with_linebreak: "M月 D日
    LT" + long_date_with_year_with_linebreak: "YYYY年 M月 D日
    LT" + wrap_ago: "%{date}前" tiny: half_a_minute: "< 1 分鐘" less_than_x_seconds: @@ -54,15 +54,15 @@ zh_TW: x_days: other: "%{count} 天" x_months: - other: "%{count} 月" + other: "%{count} 個月" about_x_years: other: "%{count} 年" over_x_years: other: "> %{count} 年" almost_x_years: other: "%{count} 年" - date_month: "MMM D" - date_year: "MMM 'YY" + date_month: "M月 D日" + date_year: "YYYY年 M月" medium: x_minutes: other: "%{count} 分鐘" @@ -70,7 +70,7 @@ zh_TW: other: "%{count} 小時" x_days: other: "%{count} 天" - date_year: "'YY MMM D" + date_year: "YYYY年 M月 D日" medium_with_ago: x_minutes: other: "%{count} 分鐘前" @@ -97,11 +97,13 @@ zh_TW: google+: '在 Google+ 分享此連結' email: '以電子郵件分享此連結' action_codes: - public_topic: "%{when}時讓大家看到這個主題" + public_topic: "於 %{when}發佈這個話題" + private_topic: "於 %{when} 私訊這個話題" split_topic: "於 %{when} 分割此討論話題" - invited_user: "邀請 %{who} 於 %{when} " - invited_group: "邀請 %{who} %{when}" - removed_user: "刪除 %{who} 於 %{when} " + invited_user: "於 %{when} 邀請 %{who}" + invited_group: "於 %{when} 邀請 %{who} " + user_left: "%{who} 已於 %{when} 將自己從此訊息中移除" + removed_user: "已於 %{when} 刪除 %{who}" removed_group: "刪除 %{who} %{when}" autoclosed: enabled: '於 %{when} 關閉' @@ -121,8 +123,14 @@ zh_TW: visible: enabled: '於 %{when} 列出' disabled: '於 %{when} 除名' + banner: + enabled: '已於 %{when} 將其作為橫幅主題。它將一直顯示直至使用者關閉它。' + disabled: '已於 %{when} 移除該橫幅主題。將不再出現於任何頁面。' topic_admin_menu: "版區管理員操作" - emails_are_disabled: "管理員已經停用了所有外寄郵件功能。通知信件都不會寄出。" + wizard_required: "歡迎來到您的新 Discourse!執行設定精靈開始吧 ✨" + emails_are_disabled: "管理員已停用了全域的外部信件功能。任何類型的電子郵件都將不再寄出。" + bootstrap_mode_enabled: "為了讓您更輕鬆地建設網站,您正處於初始模式。將會自動授予所有新註冊的使用者信任等級 1,並自動啟用每日摘要電子郵件。該功能將於 %{min_users} 個使用者註冊後自動關閉。" + bootstrap_mode_disabled: "初始模式將會在 24 小時後自動禁用。" themes: default_description: "預設" s3: @@ -133,9 +141,11 @@ zh_TW: ap_southeast_1: "亞太地區 (新加坡)" ap_southeast_2: "亞太地區 (悉尼)" cn_north_1: "中國 (北京)" + cn_northwest_1: "中國 (寧夏)" eu_central_1: "歐洲 (法蘭克福)" eu_west_1: "歐洲 (愛爾蘭)" eu_west_2: "歐洲 (倫敦)" + eu_west_3: "歐洲 (巴黎)" sa_east_1: "南美洲 (聖保羅)" us_east_1: "美國東部 (北維珍尼亞州)" us_east_2: "美國東部 (俄亥俄州)" @@ -539,7 +549,6 @@ zh_TW: watched_first_post_tags: "關注第一則帖子" watched_first_post_tags_instructions: "在有了這些標籤的每一個新主題,第一帖會通知你。" muted_categories: "靜音" - muted_categories_instructions: "在這些分類裡面,你將不會收到新主題任何通知,它們也不會出現在“最新”主題列表。" delete_account: "刪除我的帳號" delete_account_confirm: "你真的要刪除帳號嗎?此動作不能被還原!" deleted_yourself: "你的帳號已成功刪除" @@ -595,7 +604,6 @@ zh_TW: second_factor: title: "兩步驟驗證" disable: "停用兩步驟驗證" - enable: "啟用兩步驟驗證來增加帳號安全性" disable_description: "請從您的應用程式輸入驗證碼" show_key_description: "手動輸入" change_about: @@ -680,7 +688,6 @@ zh_TW: always: "總是" never: "永不" email_digests: - title: "長期未訪問時發送熱門主題和回覆的摘要郵件" every_30_minutes: "每 30 分鐘" every_hour: "每小時" daily: "每天" @@ -1552,7 +1559,6 @@ zh_TW: upload: "抱歉,上傳你的檔案時發生錯誤,請再試一次。" file_too_large: "檔案過大(最大 {{max_size_kb}}KB)。為什麼不就大檔案上傳至雲存儲服務後再分享連結呢?" too_many_uploads: "抱歉,一次只能上傳一個檔案。" - too_many_dragged_and_dropped_files: "抱歉,一次只能上傳 10 個檔案。" upload_not_authorized: "抱歉,你沒有上傳檔案的權限 (驗證擴展:{{authorized_extensions}})。" image_upload_not_allowed_for_new_user: "抱歉,新用戶不可上傳圖片。" attachment_upload_not_allowed_for_new_user: "抱歉,新用戶不可上傳附件。" @@ -1679,7 +1685,7 @@ zh_TW: tags: "標籤" tags_placeholder: "(可選)允許使用的標籤列表" tag_groups_placeholder: "(可選)允許使用的標籤組列表" - topic_featured_link_allowed: "運行在該分類中發佈特色連結" + topic_featured_link_allowed: "允許在該分類中發布精選的連結標題" delete: '刪除分類' create: '新分類' create_long: '建立新的分類' diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 4194fffac6..c5dc3737dc 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -626,8 +626,6 @@ ar: email_body: "%{link}\n\n%{message}" inappropriate: title: 'غير مناسب' - description: 'محتوي هذا المنشور أي شخص عاقل سيعتبره عدواني، أو مسيئ، أو انتهاك لـمبادئ مجتمعنا التوجيهية.' - short_description: 'انتهاك للقواعد العامة للمجتمع' long_form: 'ترفع علم هذا عن صورة غير ملائمة' notify_user: title: 'أرسل رسالة إلى @{{username}}' @@ -666,11 +664,9 @@ ar: long_form: 'ترفع علم هذا كدعاية' inappropriate: title: 'غير مناسب' - description: 'هذا الموضوع يشمل محتوى أي شخص عاقل سيعتبره عدواني، أو مسيئ، أو انتهاك لـمبادئ مجتمعنا التوجيهية.' long_form: 'ترفع علم هذا عن صورة غير ملائمة' notify_moderators: title: "شيء آخر " - description: 'هذا الموضوع يتطلب اهتمام الطاقم العام معتمداً على التوجيهات, شروط الخدمة, أو لسبب آخر لم يذكر أعلاه.' long_form: 'علم هذا لتنبيه المراقب' email_title: 'الموضوع "%{title}" يتطلب موافقة المشرف' email_body: "%{link}\n\n%{message}" @@ -852,11 +848,6 @@ ar: sidekiq_warning: "\"Sidekiq\" لا يعمل! \nالعديد من المهام, كإرسال البريدوغيرها, يتم تنفيذها بشكل غير متزامن من قبل \"sidekiq\". الرجاء التحقيق من عمل احدى وضائف الـ\"Sidekiq\". Learn about Sidekiq here." queue_size_warning: 'عدد المهام في الطّابور هو %{queue_size}، وهذا رقم كبير. قد يكون هذا مؤشّرًا لوجود مشكلة بعمليّات Sidekiq، أو قد تحتاج إضافة عاملي Sidekiq أخرى.' memory_warning: 'خادمك يعمل بأقل من 1 جيجا بايت من الذاكرة الإجمالية. يتطلب على الأقل 1 جيجا بايت من الذاكرة.' - google_oauth2_config_warning: 'تم تكوين الخادم للسماح بالتسجيل وتسجيل الدخول مع Google OAuth2 (enable_google_oauth2_logins)، لكن معرف العميل وعميل القيم السرية لم يعين. أذهب إلى إعدادات الموقعوتحديث الإعدادات. أنظر لهذا الدليل لمزيد من المعلومات.' - facebook_config_warning: 'تم تكوين الخادم للسماح بالتسجيل وتسجيل الدخول مع Facebook (enable_facebook_logins), لكن معرف العميل وعميل القيم السرية لم يعين. أذهب إلى إعدادات الموقعوتحديث الإعدادات. أنظر لهذا الدليل لمزيد من المعلومات.' - twitter_config_warning: 'تم تكوين الخادم للسماح بالتسجيل وتسجيل الدخول مع Twitter (enable_twitter_logins), لكن معرف العميل وعميل القيم السرية لم يعين. أذهب إلى إعدادات الموقع وتحديث الإعدادات. أنظر لهذا الدليل لمزيد من المعلومات.' - github_config_warning: 'تم تكوين الخادم للسماح بالتسجيل وتسجيل الدخول مع GitHub (enable_github_logins), لكن معرف العميل وعميل القيم السرية لم يعين. أذهب إلى إعدادات الموقع وتحديث الإعدادات. أنظر لهذا الدليل لمزيد من المعلومات.' - failing_emails_warning: 'يوجد %{num_failed_jobs} مهام بريد إلكتروني فشلت. تحقق من app.yml الخاص بك وتأكد من إعدادات خادم البريد أنها صحيحة. أنظر للمهام الفاشلة في Sidekiq.' subfolder_ends_in_slash: "إعدادات المجلدات الداخلية خاطئ;ال DISCOURSE_RELATIVE_URL_ROOT يجب ان تنتهي ب سلاش." site_settings: censored_words: "الكلمات التي ستُستبدل آليًّا ب‍ ■■■■" @@ -962,13 +953,11 @@ ar: min_admin_password_length: "أدنى طول لكلمة سرّ الإداريّ." block_common_passwords: "لا تسمح لكلمات المرور المسجلة في قائمة كلمات المرور الشائعةز" enable_sso: " اسمح تسجيل دخول واحد عبر موقع خارجي(تحذير :عنوان بريد المستخدم *يجب* ان يتم التحقق من صحته عبر الموقع الخارجي)" - enable_sso_provider: "تنفيذ مزود بروتوكول Discourse SSO على نقطة نهاية /session/sso_provider, يتطلب sso_secret لتعينها" sso_secret: "السلسلة السرية استخدمت لتشفير مصادقة معلومات SSO, كن متأكداً أنها 10 حروف أو أكثر" sso_overrides_bio: "تخطى التعريف المبسط بالعضو، ولا تسمح له بتغييره." sso_overrides_email: "يتجاوز البريد الاكتوني المحلي مع بريد موقع خارجي من حمولة SSO عند كل تسجيل دخول, ويمنع التتغيرات المحلية. (تحذير: ممكن يحدث تعارض بسبب الاختلاف طول اسم المستخدم/المتطلبات)" sso_overrides_username: "يتجاوز \"اسم المستخدم المحلي\" مع \"اسم مستخدم موقع خارجي\" من حمولة SSO عند كل تسجيل دخول, ويمنع التغيرات المحلية. (تحذير: ممكن يحدث تعارض بسبب الاختلاف طول اسم المستخدم/المتطلبات)" sso_overrides_name: "يتجاوز الاسم المحلي مع اسم موقع خارجي من حمولة SSO عند كل تسجيل دخول, ويمنع التغيرات المحلية. " - sso_overrides_avatar: "تجاوز صوره المستخدم الرمزيه بواسطه موقع ور رميه خارجي من SSO المحمول. اذا فعلت, الغي allow_uploaded_avatarsعاليه مطلوبه " sso_not_approved_url: "إعادة التوجيه لم توافق على حسابات SSO لهذا URL" allow_new_registrations: "السماح بتسجيل حسابات جديدة. عطل هذه الخاصية إذا كنت تريد منع الكل من عمل حساب جديد علي الموقع." enable_signup_cta: "اعرض تنبية للزوار المجهولين المترددين يحثهم علي تسجيل حساب جديد." @@ -982,7 +971,6 @@ ar: allow_restore: "تسمح استعادة، والتي يمكن أن تحل محل ALL بيانات الموقع! ترك كاذبة إلا إذا كنت تخطط لاستعادة نسخة احتياطية" maximum_backups: "أكبر قدر ممكن من النسخ الاحتياطي للحفاظ على القرص. يتم حذف النسخ الاحتياطية القديمة تلقائيا" automatic_backups_enabled: "فعل النسخ الإحتياطي التلقائي بشكل متكرر كما هو محدد." - enable_s3_backups: "تحميل النسخ الاحتياطي لS3 عند اكتماله. هام: يتطلب اعتماد S3 صالحة دخلت في إعدادات الملفات." s3_backup_bucket: "الرفع عن بعد لإجراء نسخ إحتياطية. تحذير : تأكد من أنه رفع خاص." s3_disable_cleanup: "عطل النسخ الاحتياطيه المحذوفه من S3 عندما يتم حذفها محلياً" backup_time_of_day: "الوقت الذي يجب ان تأخد النسخ الاحتياطية فيه." @@ -1488,7 +1476,6 @@ ar: من فضلك قم بتسجيل الدخول او انشاء حساب جديد للمتابعة. terms_of_service: title: "شروط الخدمة" - signup_form_message: 'لقد قرأت و أوافق على بنود الخدمة.' deleted: 'حذف' upload: edit_reason: "تحميل نسخ محلية للصور" diff --git a/config/locales/server.bg.yml b/config/locales/server.bg.yml index a3c64d16a5..f5f0701ede 100644 --- a/config/locales/server.bg.yml +++ b/config/locales/server.bg.yml @@ -400,7 +400,6 @@ bg: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Неприлично' - description: 'Този пост има съдържание, което всеки разумен човек би сметнал за обидно и оскърбително of Правилата на Общността.' long_form: 'Маркирахте това като неприлично' notify_user: title: 'Изпрати съобщение до @{{username}}' @@ -427,7 +426,6 @@ bg: long_form: 'маркирахте това като спам' inappropriate: title: 'Неприлично' - description: 'Този пост има съдържание, което всеки разумен човек би сметнал за обидно и оскърбително , или нарушава of виж Правилата на Общността.' long_form: 'Маркирано като неприлично' notify_moderators: title: "Нещо друго" @@ -574,10 +572,6 @@ bg: host_names_warning: "Вашият config/database.yml файл използва по подразбиране localhost за hostname. Обновете, за да използва името на вашия сайт за hostname. " sidekiq_warning: 'Sidekiq не работи. Много задачи, като изпращане на имейли, се изпълняват несинхронизирано от sidekiq. Моля убедете се, че поне един процес sidekiq работи. Научете повече за Sidekiq тук.' memory_warning: 'Вашият сървър използва по-малко от 1 ГБ от общата памет. Препоръчва се най-малко 1 ГБ.' - google_oauth2_config_warning: 'Сървърът е конфигуриран да позволи регистрация и вписване с Google OAuth2 (enable_google_oauth2_logins), но client id и client secret values не са определени. Отидете на Настройките на сайта и актуализирайте настройките. За повече информация вижте тук.' - facebook_config_warning: 'Сървърът е конфигуриран да позволи регистрация и вписване с Facebook (enable_facebook_logins), app id и app secret values не са изпратени. Отидете на Настройките на сайта и обновете настройките.За повече информация вижте тук.' - twitter_config_warning: 'Сървърът е конфигуриран да позволи регистрация и вписване със Twitter (enable_twitter_logins),но the key and secret values не са изпратени. Отидете на Настройките на сайта и обновете настройките. За повече информация вижте тук.' - github_config_warning: 'Сървърът е конфигуриран да позволи регистрация и вписване с GitHub (enable_github_logins), но client id и secret values не са изпратени. Отидете на Настройките на сайта и обновете настройкитеЗа повече информация вижте тук.' subfolder_ends_in_slash: "Вашата настройка за подпапка е неправилна; DISCOURSE_RELATIVE_URL_ROOT завършва с наклонена черта." site_settings: censored_words: "Думите които ще бъдат автоматично заменени с ■■■■" @@ -659,12 +653,10 @@ bg: min_admin_password_length: "Минимална дължина на парола за Админ." block_common_passwords: "Не са разрешени паролите, които са в топ 10,000 от най-често използваните." enable_sso: "Активиране на SSO чрез външен сайт (ВНИМАНИЕ: Имейл адресите на потребителите трябва да бъдат потвърдени от външния сайт!)" - enable_sso_provider: "Имплементиране на Discourse SSO протокол в /session/sso_provider крайна точка, изисква sso_secret да бъде настроен" sso_secret: "Таен стринг използван за криптографско удостоверяване на SSO информацията, уверете се, че е минимум 10 символа" sso_overrides_email: "Заместване на локалния имейл с друг от външен сайт от SSO доставчика при всяко влизане, и предотвратяване на локални промени. (ВНИМАНИЕ: могат да настъпят несъответствия поради нормализирането на локалните имейли)" sso_overrides_username: "Заместване на локалното потребителско име с друго от външен сайт от SSO доставчика при всяко влизане, и предотвратяване на локални промени. (ВНИМАНИЕ: могат да настъпят несъответствия поради нормализирането на потребителското име с дължината/изискванията)" sso_overrides_name: "Сменете потребителското име с това, което е предоставено от SSO доставчика." - sso_overrides_avatar: "Заместване на потребителския аватар с друг от външен сайт от SSO payload. Ако е разрешено деактивирането на allow_uploaded_avatars е препоръчително" sso_not_approved_url: "Пренасочване на неодобрените SSO акаунти към този URL" allow_new_registrations: "Позволи регистрация на нови потребители. Махни отметката, за да не може никой да създаде нов акаунт." enable_yahoo_logins: "Активиране на Yahoo удостоверяване" @@ -672,7 +664,6 @@ bg: google_oauth2_client_secret: "Client secret за Google приложение." allow_restore: "Позволи възстановяване, което може да замени ВСИЧКИ данни на сайта! Оставете го погрешно освен ако не планирате да възстановите бекъпа" maximum_backups: "Максимално количество бекъпи, които могат да бъдат съхранени на диска. По-старите бекъпи са автоматично изтрити" - enable_s3_backups: "Качи бекъпи на S3 когато е пълен. ВАЖНО: изисква валидни S3 правомощия въведени в настройките на файловете." s3_backup_bucket: "Remote bucket за съхранение на бекъпи. ВНИМАНИЕ: Бъдете сигурни, че това е remote bucket." active_user_rate_limit_secs: "Колко често обновяваме полето the 'last_seen_at', в секунди " verbose_localization: "Покажи разширени локализационни съвети в потребителския интерфейс " @@ -1062,7 +1053,6 @@ bg: search_title: "Търси този сайт" terms_of_service: title: "Правила за ползване" - signup_form_message: 'Аз прочетох и приемам правилата за ползване.' deleted: 'изтрит' upload: edit_reason: "изтеглено локално копие на изображенията" diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index d6bafa8e62..9c654f1b30 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -223,7 +223,6 @@ bs_BA: long_form: 'opomeni kao spam' inappropriate: title: 'Neprikladno' - description: 'Ovaj post sadrži tekst koji bi razumna osoba smatrala uvredljivim, pogrdnim i neprikladan prema pravilima naše zajednice.' long_form: 'opomeni kao neprikladno' notify_user: long_form: 'korisnik obavješten' @@ -247,7 +246,6 @@ bs_BA: long_form: 'opomenuo kao spam' inappropriate: title: 'Inappropriate' - description: 'Ova tema sadrži tekst koji bi razumna osoba smatrala uvredljivim, pogrdnim i neprikladan prema pravilima naše zajednice.' long_form: 'opomenuto kao neprikladno' notify_moderators: title: "Obavijesti Moderatore" @@ -345,10 +343,6 @@ bs_BA: host_names_warning: "Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname." sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by sidekiq. Please ensure at least one sidekiq process is running. Learn about Sidekiq here.' memory_warning: 'Your server is running with less than 1 GB of total memory. At least 1 GB of memory is recommended.' - google_oauth2_config_warning: 'The server is configured to allow signup and log in with Google OAuth2 (enable_google_oauth2_logins), but the client id and client secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - facebook_config_warning: 'The server is configured to allow signup and log in with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' site_settings: censored_words: "Words that will be automatically replaced with ■■■■" delete_old_hidden_posts: "Auto-delete any hidden posts that stay hidden for more than 30 days." @@ -417,14 +411,12 @@ bs_BA: min_password_length: "Minimum password length." block_common_passwords: "Don't allow passwords that are in the 10,000 most common passwords." sso_secret: "Secret string used to encrypt/decrypt SSO information, be sure it is 10 chars or longer" - sso_overrides_avatar: "Overrides user avatar with external site avatar from SSO payload. If enabled, disabling allow_uploaded_avatars is highly recommended" allow_new_registrations: "Allow new user registrations. Uncheck this to prevent anyone from creating a new account." enable_yahoo_logins: "Enable Yahoo authentication" google_oauth2_client_id: "Client ID of your Google application." google_oauth2_client_secret: "Client secret of your Google application." allow_restore: "Allow restore, which can replace ALL site data! Leave false unless you plan to restore a backup" maximum_backups: "The maximum amount of backups to keep on disk. Older backups are automatically deleted" - enable_s3_backups: "Upload backups to S3 when complete. IMPORTANT: requires valid S3 credentials entered in Files settings." s3_backup_bucket: "The remote bucket to hold backups. WARNING: Make sure it is a private bucket." active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds" verbose_localization: "Show extended localization tips in the UI" @@ -664,7 +656,6 @@ bs_BA: search_title: "Pretraži stranicu" terms_of_service: title: "Terms of Service" - signup_form_message: 'I have read and accept the Terms of Service.' deleted: 'obrisano' upload: edit_reason: "downloaded local copies of images" diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml index 3cf8345d0a..21b74b3a38 100644 --- a/config/locales/server.ca.yml +++ b/config/locales/server.ca.yml @@ -491,7 +491,6 @@ ca: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Inapropiat' - description: 'Aquesta publicació té contingut que una persona raonable consideraria ofensiu, abusiu o una violació de les nostres normes de la comunitat.' long_form: 'marcar amb bandera d''inapropiat' notify_user: title: 'Envia un missatge a @{{username}}' @@ -525,11 +524,9 @@ ca: long_form: 'marcat amb bandera de brossa' inappropriate: title: 'Inapropiat' - description: 'Aquest tema té contingut que una persona raonable consideraria ofensiu, abusiu o una violació de les nostres normes de la comunitat.' long_form: 'marcat amb bandera d''inapropiat' notify_moderators: title: "Alguna altra cosa" - description: 'Aquest tema necessita la supervisió general de l''equip basada en les normes, TOS o per un altre motiu abans no esmentat.' long_form: 'marcat per a la supervisió moderadora' email_title: 'El tema "%{title}" necessita la supervisió moderadora' email_body: "%{link}\n\n%{message}" @@ -717,11 +714,6 @@ ca: sidekiq_warning: 'No s''està executant Sidekiq. Hi ha moltes tasques asincrònicament en execució per Sidekiq, com ara enviament de correus. Si us plau, assegura''t que com a mínim hi ha un procés Sidekiq en marxa. Aprèn més aquí sobre Sidekiq.' queue_size_warning: 'La quantitat de tasques en cua és de %{queue_size}, que és elevada. Això podria indicar un problema amb un o més procesos o potser et cal afegir-hi més treballadors de Sidekiq.' memory_warning: 'El teu servidor s''està executant amb menys d''1 GB de memòria total. Se''n recomana com a mínim un 1 GB de memòria.' - google_oauth2_config_warning: 'El servidor està configurat per permetre el registre i inici de sessió amb Google OAuth2 (enable_google_oauth2_logins), però l''id de client i els valors secrets de client no estan configurats. Vés a la Configuració del Lloc i actualitza les configuracions. Mira aquesta guia per saber-ne més.' - facebook_config_warning: 'El servidor està configurat per permetre el registre i inici de sessió amb Facebook (enable_facebook_logins), però l''id de client i els valors secrets de client no estan configurats. Vés a la Configuració del Lloc i actualitza les configuracions. Mira aquesta guia per saber-ne més.' - twitter_config_warning: 'El servidor està configurat per permetre el registre i inici de sessió amb Twitter (enable_twitter_logins), però l''id de client i els valors secrets de client no estan configurats. Vés a la Configuració del Lloc i actualitza les configuracions. Mira aquesta guia per saber-ne més.' - github_config_warning: 'El servidor està configurat per permetre el registre i inici de sessió amb GitHub (enable_github_logins), però l''id de client i els valors secrets de client no estan configurats. Vés a la Configuració del Lloc i actualitza les configuracions. Mira aquesta guia per aprendre''n més.' - failing_emails_warning: 'Hi ha %{num_failed_jobs} tasques de correu fallides. Revisa el teu app.yml i assegura''t que la configuració del servidor de correu és correcta. Mira les tasques fallides a Sidekiq.' subfolder_ends_in_slash: "La teva configuració de subcarpeta no és correcta; DISCOURSE_RELATIVE_URL_ROOT acaba amb barra inclinada." email_polling_errored_recently: one: "L'enquesta per correu electrònic ha generat un error durant les darreres 24 hores. Fes un cop d'ull als registres per saber-ne més." @@ -836,14 +828,12 @@ ca: password_unique_characters: "Quantitat mínima de caràcters únics que ha de tenir una contrasenya." block_common_passwords: "No permetis contrasenyes que es trobin entre les 10000 més comuns." enable_sso: "Activa l'autenticació única des d'un lloc extern (ATENCIÓ: LES ADRECES DE CORREU *HAN* DE SER VALIDADES PER UN LLOC EXTERN!)" - enable_sso_provider: "Implementa el protocol de proveïdor de l'autenticació única del Discourse a /session/sso_provider endpoint, necessita sso_secret per ser activada" sso_url: "Adreça URL d'autenticació única a un punt final (ha dincloure http:// o https://)" sso_secret: "Cadena secreta emprada per autenticar criptogràficament la informació d'autenticació única, assegura't que té 10 caràcters o més" sso_overrides_bio: "Anul·la la biografia de la persona al perfil i evita que es pugui canviar" sso_overrides_email: "Anul·la el correu local amb correu extern per a l'autenticació única a cada connexió i evita els canvis en local. (ATENCIÓ: poden succeir incidències a causa de la normalització dels correus locals)" sso_overrides_username: "Anul·la el nom local de persona amb nom de lloc extern per a la càrrega útil d'autenticació única a cada connexió i evita els canvis en local. (ATENCIÓ: poden succeir incidències a causa de les diferències a la mida i a les condicions dels noms de persona usuària)" sso_overrides_name: "Anul·la el nom sencer local amb nom sencer extern per a la càrrega útil d'autenticació única a cada connexió i evita els canvis en local." - sso_overrides_avatar: "Anul·la l'avatar de la persona amb avatar de lloc extern de la càrrega útil d'autenticació única, es recomana molt la inhabilitació de allow_uploaded_avatars" sso_not_approved_url: "Redirecciona els comptes d'autenticació única a aquesta adreça URL" sso_allows_all_return_paths: "No restringeixis el domini per a return_paths facilitat per l'autenticació única (per defecte el camí de retorn ha de ser el lloc actual)" allow_new_registrations: "Permet registres de noves persones. Inhabilita-ho per tal d'evitar que qualsevol pugui crear un nou compte." @@ -858,7 +848,6 @@ ca: allow_restore: "Permet restablir, que pot reemplaçar TOTES les dades del lloc! Deixa-ho com a \"false\" si no pretens restablir una còpia de seguretat" maximum_backups: "Quantitat màxima de còpies de seguretat per mantenir al disc. Les còpies més antigues seran esborrades automàticament" automatic_backups_enabled: "Executa còpies de seguretat automàtiques tal com estan definides a la freqüències de la còpia de seguretat" - enable_s3_backups: "Carrega còpies de seguretat a S3 quan es completi. IMPORTANT: necessita credencials vàlides de S3 introduïdes a la configuració de fitxer." s3_backup_bucket: "Sistema remot de càrrega de còpies de seguretat. ATENCIÓ: Assegura't que és una càrrega privada." s3_disable_cleanup: "Inhabilita l'eliminació de còpies de seguretat de S3 quan s'eliminin localment." backup_time_of_day: "Temps del dia UTC quan la còpia de seguretat hauria d'executar-se." @@ -1668,7 +1657,6 @@ ca: search_title: "Busca aquest lloc" terms_of_service: title: "Condicions d'ús" - signup_form_message: 'He llegit i accepto les Condicions del servei.' deleted: 'esborrat' upload: edit_reason: "còpia local d'imatges descarregada" @@ -1994,6 +1982,3 @@ ca: title: "Convida equip" finished: title: "El teu Discourse està llest!" - description: | -

    Si mai vols canviar aquesta configuració, visita la teva secció d'administració; troba-ho al costat de la icona de clau anglesa al menú del lloc.

    -

    Passa-t'ho bé i molta sort amb la construcció de la teva nova comunitat!

    diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index 4123e84421..dbd7b98266 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -606,7 +606,6 @@ cs: long_form: 'označeno jako spam' inappropriate: title: 'Nevhodné' - description: 'Obsah tohoto příspěvku by rozumný člověk shledal urážlivý, hrubý nebo v rozporu s pravidly komunity.' long_form: 'nahlášeno jako nevhodné' notify_moderators: title: "Něco jiného" @@ -761,9 +760,6 @@ cs: host_names_warning: "Vaše konfigurace v souboru config/database.yml používá 'localhost' jako jméno hostitele. Změňte toto nastavení na doménu vašeho webu." sidekiq_warning: 'Sidekiq neběží. Řada úloh, jako je zasílání emailů, se provádí asynchronně na pozadí přes sidekiq. Prosím, ujistěte se, že alespoň jeden process sidekiq běží. Více o Sidekiq.' memory_warning: 'Váš server má méně než 1 GB paměti. Doporučená konfigurace je alespoň 1 GB paměti.' - facebook_config_warning: 'Server je nakonfigurován, aby povoloval registraci a přihlášení přes Facebook (enable_facebook_logins), ale nejsou nastaveny hodnoty "app id" a "app secret". Navštivte nastevení webu a opravte nastavení. Pro více informací si přečtěte tento návod.' - twitter_config_warning: 'Server je nakonfigurován, aby povoloval registraci a přihlášení přes Twitter (enable_twitter_logins), ale nejsou nastaveny hodnoty "key" a "secret". Navštivte nastevení webu a opravte nastavení. Pro více informací si přečtěte tento návod.' - github_config_warning: 'Server je nakonfigurován, aby povoloval registraci a přihlášení přes GitHub (enable_github_logins), ale nejsou nastaveny hodnoty "client id" a "secret". Navštivte nastevení webu a opravte nastavení. Pro více informací si přečtěte tento návod.' site_settings: allow_user_locale: "Povolit uživatelům nastavit si jazyk fóra" min_search_term_length: "Minimální délka hledaného výrazu ve znacích" @@ -916,7 +912,6 @@ cs: see_more: "Více" terms_of_service: title: "Podmínky používání" - signup_form_message: 'I have read and accept the Terms of Service.' deleted: 'smazáno' upload: unauthorized: "Bohužel, soubor, který se snažíte nahrát, není povolený (povolené přípony: %{authorized_extensions})." diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index 7011b5a672..43d739423f 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -516,7 +516,6 @@ da: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Upassende' - description: 'Dette indlæg har indhold som en rimelig person ville opfatte som stødende, udtryk for misbrug eller som et brud på vores sammenholds retningslinjer.' long_form: 'markerede dette som stødende' notify_user: title: 'Send @{{username}} en besked' @@ -550,11 +549,9 @@ da: long_form: 'markerede dette som spam' inappropriate: title: 'Upassende' - description: 'Dette emne har indlæg som en fornuftig person finder stødende, krænkende, eller som er i modstrid med Den Gode Tone.' long_form: 'markeret upassende' notify_moderators: title: "Noget andet" - description: 'Dette emne kræver moderatorernes opmærksomhed baseret på Den Gode Tone, Brugerbetingelser eller en anden grund, som ikke er nævnt ovenfor.' long_form: 'markerede dette til gennemsyn' email_title: 'Emnet "%{title}" kræver moderator-opmærksomhed' email_body: "%{link}\n\n%{message}" @@ -736,11 +733,6 @@ da: sidekiq_warning: 'Sidekiq kører ikke. Mange opgaver, som f.eks. udsendelse af e-mail, afvikles asynkront af sidekiq. Du skal sikre dig at der kører mindst én sidekiq-proces. Læs mere om Sidekiq her.' queue_size_warning: 'Antallet af job i kø er %{queue_size}, hvilket er højt. Dette kunne indikere at der er et problem med Sidekiq processerne, eller du behøver at tilføre flere Sidekiq resurser.' memory_warning: 'Din server kører med mindre end 1 GB samlet hukommelse. Det anbefales at have mindst 1 GB hukommelse.' - google_oauth2_config_warning: 'Serveren er konfigureret til at tillade signup med Google OAuth2 (enable_google_oauth2_logins),men klient id og klientens skjulte værdier er ikke indstillet. Gå tilthe Site Settings og opdater! Se denne guide for at lære mere.' - facebook_config_warning: 'Serveren er konfigureret til at tillade tilmelding og login med Facebook (enable_facebook_logins), men app id og app secret værdierne er ikke angivet. Gå til indstillinger og opdatér indstillingerne. Se denne vejledning for mere information.' - twitter_config_warning: 'Serveren er konfigureret til at tillade tilmelding og login med Twitter (enable_twitter_logins), men key og secret værdierne er ikke angivet. Gå til indstillinger og opdatér indstillingerne. Se denne vejledning for mere information.' - github_config_warning: 'Serveren er konfigureret til at tillade tilmelding og login med GitHub (enable_github_logins), men client id og secret værdierne er ikke angivet. Gå til indstillinger og opdatér indstillingerne. Se denne vejledning for mere information.' - failing_emails_warning: 'Der er %{num_failed_jobs} email operationer der mislykkede. Tjek din app.yml for at sikre at din mail server indstillinger er korrekte. Se mislykkede operationer i Sidekiq.' subfolder_ends_in_slash: "Din undermappe er ikke opsat korrekt; DISCOURSE_RELATIVE_URL_ROOT ender med en slash." email_polling_errored_recently: one: "Email afstemning har afstedkommet en fejl i de seneste 24 timer. Venligst se the logs for detaljer." @@ -1344,7 +1336,6 @@ da: search_title: "Søg på denne side" terms_of_service: title: "Vilkår" - signup_form_message: 'Jeg har læst og accepterer vilkårene.' deleted: 'slettet' upload: unauthorized: "Beklager, filen, som du forsøger at uploade er ikke autoriseret (autoriserede filendelser: %{authorized_extensions})." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index a086195c4d..4dabf4cc5e 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -144,6 +144,7 @@ de: other: 'Du hast die ungültige Auswahl %{name} getroffen' default_categories_already_selected: "Du kannst keine Kategorie auswählen, welche bereits in einer anderen Liste benutzt wird. " s3_upload_bucket_is_required: "Uploads auf Amazon S3 können nicht aktiviert werden, bevor der 's3_upload_bucket' eingetragen wurde." + s3_backup_requires_s3_settings: "Du kannst S3 nicht als Backup-Ort verwenden, solange du die Einstellung '%{setting_name}' nicht angegeben hast." conflicting_google_user_id: 'Die Google-Konto-ID für dieses Konto hat sich geändert; das Einschreiten des Teams ist aus Sicherheitsgründen erforderlich. Bitte kontaktiere das Team und verweise es auf
    https://meta.discourse.org/t/76575' activemodel: errors: @@ -167,6 +168,10 @@ de: backup_file_should_be_tar_gz: "Die Sicherungsdatei sollte ein .tar.gz-Archiv sein." not_enough_space_on_disk: "Es gibt nicht genügend freien Festplattenspeicher, um dieses Backup hochzuladen." invalid_filename: "Der Dateiname für das Backup enthält ungültige Zeichen. Gültig sind: a-z 0-9 . - _." + file_exists: "Die Datei, die du versucht hast hochzuladen, existiert bereits." + location: + local: "Lokal" + s3: "Amazon S3" invalid_params: "Du hast bei der Anfrage ungültige Parameter angegeben: %{message}" not_logged_in: "Dazu musst du angemeldet sein." not_found: "Die angeforderte URL oder Ressource konnte nicht gefunden werden." @@ -457,6 +462,7 @@ de: Du solltest dieses Thema eventuell schließen über die Administration :wrench: (oben rechts oder unten), damit sich an einer Ankündigung wie dieser nicht die Antworten aufstapeln. lounge_welcome: title: "Willkommen in der Lounge" + body: "\nGratuliere! :confetti_ball:\n\nWenn du dieses Thema sehen kannst, wurdest du vor Kurzem zum **Stammgast** (Vertrauensstufe 3) befördert.\n \nDu kannst nun …\n\n* den Titel eines jeden Themas ändern\n* Themen in andere Kategorien verschieben\n* Links veröffentlichen, die von Suchmaschinen weiterverfolgt werden (das automatische [nofollow](https://de.wikipedia.org/wiki/Nofollow) wird entfernt)\n* auf die private Lounge-Kategorie zugreifen, die für Benutzer mit Vertrauensstufe 3 oder höher sichtbar ist\n* Spam durch eine einzige Meldung ausblenden\n\nHier ist die [aktuelle Liste aller Stammgäste](/badges/3/regular). Vergiss nicht, hallo zu sagen!\n\nVielen Dank dafür, dass du ein wichtiger Teil dieser Community bist!\n\n(Wenn du mehr über Vertrauensstufen wissen möchtest, kannst du [dieses Thema lesen][trust]. Beachte bitte, dass nur jene Mitglieder Stammgäste bleiben, die auch im Laufe der Zeit die Anforderungen erfüllen.)\n\n[trust]: https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/\n" category: topic_prefix: "Über die Kategorie %{category}" replace_paragraph: "(Ersetze diesen ersten Absatz mit einer kurzen Beschreibung deiner neuen Kategorie. Diese Richtlinie wird in der Kategorienauswahl angezeigt, versuche also weniger als 200 Zeichen zu benutzen. **Bis du diese Beschreibung geändert oder Themen angelegt hast, wird diese Kategorie nicht auf der Kategorie-Seite angezeigt.**)" @@ -619,7 +625,6 @@ de: ipod: "iPod" mobile: "Mobilgerät" mac: "Mac" - linux: "Linux-Computer" windows: "Windows-Computer" unknown: "unbekanntes Gerät" os: @@ -670,8 +675,6 @@ de: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Unangemessen' - description: 'Dieser Beitrag enthält Inhalte, die eine vernünftige Person als anstößig, beleidigend oder unsere Richtlinien verletzend auffassen würde.' - short_description: 'Ein Verstoß gegen unsere Community-Richtlinien' long_form: 'dies als unangemessen gemeldet' notify_user: title: 'Schreibe @{{username}} eine Nachricht' @@ -720,12 +723,9 @@ de: short_description: 'Dies ist ist Werbung' inappropriate: title: 'Unangemessen' - description: 'Dieses Thema enthält Inhalte, die eine vernünftige Person als anstößig, beleidigend oder unsere Richtlinien verletzend auffassen würde.' long_form: 'als unangemessen gemeldet' - short_description: 'Ein Verstoß gegen unsere Community-Richtlinien' notify_moderators: title: "Irgendetwas anderes" - description: 'Dieser Beitrag erfordert die allgemeine Aufmerksamkeit des Teams, da er entweder nicht mit den Richtlinien oder den Nutzungsbedingungen in Einklang zu bringen ist, oder aus anderen Gründen.' long_form: ' hast dies den Moderatoren gemeldet' short_description: 'Erfordert aus einem anderen Grund die Aufmerksamkeit des Teams' email_title: 'Das Thema "%{title}" benötigt die Aufmerksamkeit eines Moderators' @@ -1004,14 +1004,11 @@ de: dashboard: rails_env_warning: "Dein Server läuft im %{env}-Modus." host_names_warning: "Deine config/database.yml-Datei verwendet localhost als Hostname. Trage hier den Hostnamen deiner Website ein." + gc_warning: 'Dein Server verwendet die Standardparameter für Rubys Garbage-Collector, die nicht optimal sind. Lese dieses Thema über Performanzeinstellungen (en): Tuning Ruby and Rails for Discourse.' sidekiq_warning: 'Sidekiq läuft nicht. Viele Aufgaben, wie zum Beispiel das Versenden von E-Mails, werden asynchron durch Sidekiq ausgeführt. Bitte stell sicher, dass mindestens ein Sidekiq-Prozess läuft. Mehr über Sidekiq erfährst du hier (en).' queue_size_warning: 'Eine hohe Anzahl an Aufgaben (%{queue_size}) befindet sich in der Warteschlange. Dies könnte auf ein Problem mit Sidekiq hinweisen oder du musst zusätzliche Sidekiq Worker starten.' memory_warning: 'Dein Server läuft mit weniger als 1 GB Hauptspeicher. Mindestens 1 GB Hauptspeicher werden empfohlen.' - google_oauth2_config_warning: 'Der Server ist für Anmeldung und Login mit Google OAuth2 (enable_google_oauth2_logins) konfiguriert, aber die Client-ID und das Client-Gemeheimnis sind nicht gesetzt. Trage diese in den Einstellung ein. Eine Anleitung zu diesem Thema findest du hier.' - facebook_config_warning: 'Der Server erlaubt die Anmeldung mit Facebook (enable_facebook_logins), aber die App ID und der Geheimcode sind nicht gesetzt. Besuche die Einstellungen um die fehlenden Einträge hinzuzufügen. Besuche den Leitfaden um mehr zu erfahren.' - twitter_config_warning: 'Der Server erlaubt die Anmeldung mit Twitter (enable_twitter_logins), aber der Schlüssel und der Geheimcode sind nicht gesetzt. Besuche die Einstellungen um die fehlenden Einträge hinzuzufügen. Besuche den Leitfaden um mehr zu erfahren.' - github_config_warning: 'Der Server erlaubt die Anmeldung mit Facebook GitHub (enable_github_logins), aber die Kunden-ID und der Geheimcode sind nicht gesetzt. Besuche die Einstellungen um die fehlenden Einträge hinzuzufügen. Besuche den Leitfaden um mehr zu erfahren.' - failing_emails_warning: "%{num_failed_jobs} E-Mails konnten nicht versendet werden. Überprüfe deine app.yml und stelle sicher, dass die E-Mail Servereinstellungen korrekt gesetzt sind. \nSieh dir hier die nicht versendeten E-Mails an." + image_magick_warning: 'Der Server wurde konfiguriert um Vorschaubilder von grossen Bildern zu erstellen, aber ImageMagick ist nicht installiertd. Installiere ImageMagick mit deinem bevorzugten Packetmanager oder besuche um das aktuelle Paket herunterzuladen.' subfolder_ends_in_slash: "Deine Installation in einem Pfad ist nicht korrekt, DISCOURSE_RELATIVE_URL_ROOT endet mit einem Schrägstrich." email_polling_errored_recently: one: "Beim Abrufen von E-Mails ist in den letzten 24 Stunden ein Fehler aufgetreten. Weitere Informationen findest du im Fehlerprotokoll." @@ -1054,6 +1051,8 @@ de: educate_until_posts: "Zeige das Hilfe-Panel im Editor sobald ein Benutzer einen seiner ersten (n) Beiträge zu schreiben beginnt." title: "Der Name dieser Site, wird für das Title-Tag verwendet." site_description: "Beschreibe diese Site in einem Satz. Wird für das \"description\" Meta-Tag verwendet." + contact_email: "E-Mail-Adresse einer verantwortlichen Person für diese Community. Wird verwendet für kritische Benachrichtigungen sowie auf dem /about Kontaktformular für dringende Anfragen." + contact_url: "URL für Kontaktanfragen bezüglich dieser Site. Wird unter /about im Kontaktformular verwendet für dringende Angelegenheiten." crawl_images: "Lade Bilder von fremden URLs herunter, um ihre Höhe und Breite zu bestimmen." download_remote_images_to_local: "Lade eine Kopie von extern gehosteten Bildern herunter und ersetze Links in Beiträgen entsprechend; dies verhindert defekte Bilder." download_remote_images_threshold: "Minimal benötigter freier Festplattenspeicher um externe Bilder lokal herunterzuladen (in Prozent)" @@ -1081,10 +1080,16 @@ de: inline_onebox_domains_whitelist: "Eine Liste von Domains, die in verkleinerter Form in eine Onebox umgewandelt werden, wenn sie ohne Titel verlinkt werden" enable_inline_onebox_on_all_domains: "Ignoriere die `inline_onebox_domain_whitelist`-Seiteneinstellung und erlaube Inline-Oneboxen für alle Domains" max_oneboxes_per_post: "Maximale Anzahl von Oneboxes in einem Beitrag." + logo_url: "Das Logo oben links auf deiner Site sollte eine breite, rechteckige Form haben. Wenn du kein Logo auswählst, wird stattdessen `title` angezeigt. Verwende den Assistenten, um es zu ändern." digest_logo_url: "Das alternative Logo oben in den E-Mail-Zusammenfassungen deiner Site. Sollte eine breite, rechteckige Form haben. Sollte kein SVG-Bild sein. Wenn leer, wird `logo_url` verwendet." + logo_small_url: "Das kleine Logo oben links auf deiner Seite, sollte quadratisch sein, wird beim Herunterscrollen angezeigt. Wenn keines ausgewählt wird, du, wird stattdessen ein Haus-Symbol angezeigt. Verwende den Assistenten, um es zu ändern." + favicon_url: "Favicon für deine Seite, siehe https://de.wikipedia.org/wiki/Favicon. Damit es auch über ein CDN richtig funktioniert, muss es im PNG-Format vorliegen. Verwende den Assistenten, um es zu ändern." + mobile_logo_url: "Benutzerdefiniertes Logo für die mobile Seitenversion. Falls leer, wird `logo_url` verwendet. Beispiel: https://example.com/uploads/default/logo.png" + large_icon_url: "Bild wird als Logo/Startbild auf Android verwendet. Empfohlene Größe beträgt 512px mal 512px. Verwende den Assistenten, um dies zu ändern." apple_touch_icon_url: "Icon für berührungsempfindliche Apple-Geräte. Empfohlene Grösse ist 144px auf 144px." notification_email: "Die E-Mail-Adresse die als \"From:\" Absender aller wichtiger System-E-Mails benutzt wird. Die benutzte Domain sollte über korrekte SPF, DKIM und PTR Einträge verfügen, damit E-Mails sicher zugestellt werden können." email_custom_headers: "Eine durch senkrechte Striche getrennte Liste von eigenen E-Mail Headerzeilen" + email_subject: "Anpassbares Betreff-Format für Standard-E-Mails. Siehe https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" force_https: "Erzwinge HTTPS für deine Site. ACHTUNG: Aktiviere dies nicht, bevor HTTPS nicht vollständig eingerichtet ist und auf jeden Fall überall funktioniert! Hast du alle CDN-Netzwerke, alle Logins über Soziale Netzwerke, alle externe Logos / Abhängigkeiten geprüft, um sicherzustellen, dass sie auch alle HTTPS-kompatibel sind?" same_site_cookies: "Verwende Same-Site-Cookies, die alle Angriffsszenarien für Cross-Site-Request-Forgery in unterstützten Browsern verhindern. Warnung: Strikt wird nur auf Seiten funktionieren, die Login erzwingen und SSO verwenden." summary_score_threshold: "Mindestpunktzahl, die ein Beitrag benötigt, um in der \"Thema zusammenfassen\"-Ansicht zu erscheinen." @@ -1115,12 +1120,17 @@ de: flag_sockpuppets: "Wenn ein neuer Benutzer auf ein Thema antwortet, das von einem anderen neuen Benutzer aber mit der gleichen IP-Adresse begonnen wurde, markiere beide Beiträge als potenziellen Spam." traditional_markdown_linebreaks: "Traditionelle Zeilenumbrüche in Markdown, die zwei nachfolgende Leerzeichen für einen Zeilenumbruch benötigen." enable_markdown_typographer: "Verwende typographische Regeln, um die Lesbarkeit von Text zu erhöhen: ersetze einfache Anführungszeichen ' durch geneigte Anführungszeichen ’, (c) (tm) durch Symbole, -- durch Gedankenstriche –, usw." + enable_markdown_linkify: "Text, der nach einem Link aussieht, automatisch als Link behandeln: www.example.com und https://example.com werden automatisch verlinkt" markdown_linkify_tlds: "Liste der Top-Level-Domains, die automatisch als Links behandelt werden" post_undo_action_window_mins: "Minuten, die ein Benutzer hat, um Aktionen auf einen Beitrag rückgängig zu machen (Gefällt mir, Meldung, usw.)." must_approve_users: "Team-Mitglieder müssen alle neuen Benutzerkonten freischalten, bevor diese Zugriff auf die Website erhalten. ACHTUNG: Das Aktivieren dieser Option für eine Live-Site entfernt den Zugriff auch für alle existierenden Benutzer außer für Team-Mitglieder!" pending_users_reminder_delay: "Benachrichtige die Moderatoren, falls neue Benutzer mehr als so viele Stunden auf ihre Genehmigung gewartet haben. Stelle -1 ein, um diese Benachrichtigungen zu deaktivieren." maximum_session_age: "Benutzer bleiben (n) Stunden nach ihrem letzten Besuch angemeldet" + ga_universal_tracking_code: "Google Universal Analytics (analytics.js) Tracking-Code, z.B. UA-12345678-9; siehe https://google.com/analytics" + ga_universal_domain_name: "Google Universal Analytics (analytics.js) domain name, eg: mysite.com; Siehe https://google.com/analytics" + ga_universal_auto_link_domains: "Aktiviere Google Universal Analytics (analytics.js) Cross-Domain-Tracking. Ausgehenden Links zu diesen Domains wird eine Client-ID hinzugefügt. Siehe Googles Cross-Domain-Tracking-Anleitung." gtm_container_id: "Google Tag Manager Container-ID, z.B.: GTM-ABCDEF" + enable_escaped_fragments: "Als Fallback die Ajax-Crawling-API von Google verwenden, wenn keine Suchmaschine deaktiviert wurde. Siehe https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Erlaube Moderatoren neue Kategorien zu erstellen" cors_origins: "Erlaubte Adressen für Cross-Origin-Requests (CORS). Jede Adresse muss http:// oder https:// enthalten. Die Umgebungsvariable DISCOURSE_ENABLE_CORS muss gesetzt sein, um CORS zu aktivieren." use_admin_ip_whitelist: "Administratoren können sich nur anmelden, wenn sie von einer IP-Adresse aus zugreifen, welcher unter den vertrauenswürden IP-Adressen gelistet ist (Admin > Logs > Screened Ips)." @@ -1157,6 +1167,7 @@ de: email_domains_whitelist: "Eine durch senkrechte Striche getrennte Liste von E-Mail-Domains, die für die Registrierung neuer Konten verwendet werden können. ACHTUNG: Benutzer mit E-Mail-Adressen anderer Domains werden nicht zugelassen!" hide_email_address_taken: "Teile Benutzer während der Registrierung und beim „Passwort vergessen“-Formular nicht mit, wenn ein Konto mit der angegebenen E-Mail-Adresse existiert." log_out_strict: "Beim Abmelden ALLE Sitzungen des Benutzers auf allen Geräten beenden" + version_checks: "Kontaktiere den Discourse Hub zur Überprüfung auf neue Versionen und zeige Benachrichtigungen über neue Versionen im /admin-Dashboard an." new_version_emails: "Sende eine E-Mail an die contact_email Adresse wenn eine neue Version von Discourse verfügbar ist." invite_expiry_days: "Tage, die Benutzereinladungen gültig bleiben." invite_only: "Die öffentliche Registrierungen ist deaktiviert, neue Benutzer müssen explizit vom Team eingeladen werden." @@ -1169,15 +1180,15 @@ de: password_unique_characters: "Minimale Anzahl unterschiedlicher Zeichen, die ein Passwort enthalten muss." block_common_passwords: "Erlaube kein Passwort unter den 10000 meist verwendeten Passwörter." enable_sso: "Aktiviere Single Sign-on über eine externe Site (WARNUNG: DIE E-MAIL-ADRESSE DES BENUTZERS *MUSS* VON DER EXTERNEN SITE VERIFIZIERT WERDEN)." - enable_sso_provider: "Aktiviere das Discourse SSO Anbieter Protokoll unter /session/sso_provider; benötigt sso_secret." + verbose_sso_logging: "Protokolliere ausführliche SSO-bezogene Diagnose in /logs" sso_url: "URL des Single Sign-On-Endpunkts (muss http:// oder https:// enthalten)" sso_secret: "Geheimer Schlüssel für die Authentifizierung von SSO-Informationen. Sollte unbedingt 10 Zeichen oder länger sein." + sso_provider_secrets: "Eine Liste von Domain-Secret-Paaren, die Discourse als SSO-Provider verwenden. Stelle sicher, dass SSO-Secrets mindestens 10 Zeichen lang sind. Das Wildcard-Symbol * can verwndet werden, um jede Domain oder Teile davon auszuwählen (z.B. *.example.com)." sso_overrides_bio: "Überschreibt \"Über mich\" im Benutzerprofil und verhindert Änderungen durch den Benutzer" sso_overrides_groups: "Synchronisiert alle manuellen Gruppenmitgliedschaften mit den Gruppen, die im SSO-Attribut „groups“ angegeben ist (WARNUNG: wenn du keine Gruppen angibst, werden alle Gruppenmitgliedschaften des Benutzers aufgehoben)" sso_overrides_email: "Überschreibt lokale E-Mail mit E-Mail der externen Site aus dem SSO-Payload (WARNUNG: Diskrepanzen können aufgrund der Normalisierung lokaler E-Mail-Adressen auftreten.)" sso_overrides_username: "Überschreibt lokalen Benutzernamen mit dem Benutzernamen der externen Site aus dem SSO-Payload (WARNUNG: Diskrepanzen können aufgrund von Normalisierung von lokalen Benutzernamen auftreten)" sso_overrides_name: "Überschreibt den vollen Namen des Benutzers mit den Daten von der externen Site aus dem SSO-Payload bei jedem Login. Außerdem werden lokale Änderungen verhindert." - sso_overrides_avatar: "Überschreibt das Profilbild des Benutzers mit dem Profilbild aus dem SSO-Payload. Wenn aktiv, dann sollte allow_uploaded_avatars deaktiviert werden." sso_overrides_profile_background: "Ersetzt das Hintergrundbild des Benutzerprofils mit dem Avatar der externen Seite aus den SSO-Daten" sso_overrides_card_background: "Ersetzt das Hintergrundbild der Benutzerkarte mit dem Avatar der externen Seite aus den SSO-Daten" sso_not_approved_url: "Nicht genehmigte SSO-Konten zu dieser URL weiterleiten" @@ -1187,18 +1198,29 @@ de: allow_new_registrations: "Erlaube das Registrieren neuer Benutzerkonten. Wird dies deaktiviert, so kann niemand mehr ein neues Konto erstellen." enable_signup_cta: "Zeige wiederkehrenden Gästen einen Hinweis, dass diese sich Anmelden oder Registrieren sollen." enable_yahoo_logins: "Aktiviere Yahoo Authentifizierung." + enable_google_oauth2_logins: "Google OAuth2-Authentifizierung aktivieren. Dies ist der momentan von Google unterstützte Authentifizierungs-Mechanismus. Benötigt Client-ID und Secret. Siehe Configuring Google login for Discourse." google_oauth2_client_id: "Client-ID deiner Google-Anwendung." google_oauth2_client_secret: "Client-Secret deiner Google-Anwendung." + google_oauth2_prompt: "Eine optionale Leerzeichen getrennte Liste von Zeichen Werten die angeben ob der Berechtigungsserver den Benutzer zur Re-Authentifizierung auffordert. Siehe https://developers.google.com/identity/protocols/OpenIDConnect#prompt für mögliche Werte." + google_oauth2_hd: "Eine optionale Google Apps gehostete Domain zu welches das Anmelden limitiert sein wird. Siehe https://developers.google.com/identity/protocols/OpenIDConnect#hd-param für weitere Informationen." + enable_twitter_logins: "Twitter-Authentifizierung aktivieren, erfordert twitter_consumer_key und twitter_consumer_secret. Siehe Configuring Twitter login (and rich embeds) for Discourse." + twitter_consumer_key: "Consumer Key für Twitter-Authentifizierung, registriert unter https://apps.twitter.com/" + twitter_consumer_secret: "Consumer Secret für Twitter-Authentifizierung, registriert unter https://apps.twitter.com" enable_instagram_logins: "Aktiviere Instagram-Authentifizierung (benötigt instagram_consumer_key und instagram_consumer_secret)." instagram_consumer_key: "Consumer Key für Instagram-Authentifizierung" instagram_consumer_secret: "Consumer Secret für Instagram-Authentifizierung" + enable_facebook_logins: "Facebook-Authentifizierung aktivieren, erfordert facebook_api_id und facebook_app_secret. Siehe Configuring Facebook login for Discourse." + facebook_app_id: "App-ID für Facebook-Authentifizierung, registriert unter https://developers.facebook.com/apps" + facebook_app_secret: "App-Secret für Facebook-Authentifizierung, registriert unter https://developers.facebook.com/apps" + enable_github_logins: "GitHub-Authentifizierung aktivieren, erfordert github_client_id und github_client_secret. Siehe Configuring GitHub login for Discourse." + github_client_id: "Client-ID für GitHub-uthentifizierung, registriert auf https://github.com/settings/applications" + github_client_secret: "Client-Secret für GitHub-Authentifizierung, registriert auf https://github.com/settings/applications" readonly_mode_during_backup: "Nur-Lesen-Modus beim Erstellen einer Sicherung aktivieren" enable_backups: "Erlaube Administratoren, Backups des Forums zu erstellen" allow_restore: "Wiederherstellung von Sicherungen zulassen, die ALLE vorhandenen Daten überschreiben! Auf 'false' lassen, sofern Sie nicht planen, eine Sicherung wiederherzustellen." maximum_backups: "Die maximale Anzahl an Sicherungen, die auf dem Server gespeichert werden. Ältere Sicherungen werden automatisch gelöscht." automatic_backups_enabled: "Automatische Backups aktivieren. Die Backups werden im eingestellten Zeitintervall erstellt." backup_frequency: "Die Anzahl von Tagen zwischen Backups." - enable_s3_backups: "Lade fertige Backups zu S3 hoch. WICHTIG: In den Dateien-Einstellungen müssen gültige S3-Kontodaten eingegeben sein." s3_backup_bucket: "Der entfernte Speicherort für Ihre Sicherungen. WARNUNG: Stellen Sie sicher, dass es sich um einen privaten Speicherort handelt." s3_endpoint: "Dieser Endpunkt kann so angepasst werden, dass er die Sicherung an einen S3-kompatiblen Service wie DigitalOcean Spaces oder Minio übertragt. WARNUNG: Verwende den Standard bei Verwendung von AWS S3." s3_force_path_style: "Forciere Pfad-Adressierung deines benutzerdefinierten Endpunkts. WICHTIG: Erforderlich bei Verwendung von Minio für Uploads und Sicherungen." @@ -1206,6 +1228,7 @@ de: s3_disable_cleanup: "Deaktiviere das Löschen von Backups aus S3 wenn sie lokal entfernt werden" backup_time_of_day: "Uhrzeit in UTC, wenn Backups ausgeführt werden sollen." backup_with_uploads: "Hochgeladene Dateien bei geplanten Backups mit einbeziehen. Wenn diese Einstellung deaktiviert ist, wird nur die Datenbank gesichert." + backup_location: "Ort, an dem die Backups abgelegt werden. WICHTIG: S3 erfordert gültige S3-Zugangsdaten in den Datei-Einstellungen." active_user_rate_limit_secs: "Anzahl der Sekunden, nach denen das 'last_seen_at'-Feld aktualisiert wird." verbose_localization: "Erweiterte Lokalisierungstipps in der Benutzeroberfläche anzeigen" previous_visit_timeout_hours: "Anzahl der Stunden, die ein Besuch dauert, bevor er als 'früherer' Besuch gezählt wird." @@ -1314,6 +1337,7 @@ de: title_min_entropy: "Für Titel neuer Themen minimal erforderliche Entropie (einzigartige Zeichen)." body_min_entropy: "Für den Text neuer Beiträge minimal erforderliche Entropie (einzigartige Zeichen)." allow_uppercase_posts: "Beiträge oder Titel von Themen mit ausschließlich Großschreibweise erlauben." + title_fancy_entities: "Konvertiere HTML-Entitäten in Themen-Überschriften, nach dem Smarty-Pants-Schema https://daringfireball.net/projects/smartypants/" min_title_similar_length: "Minimale Länge eines Titels, bevor nach ähnlichen Titeln gesucht wird." min_body_similar_length: "Minimale Länge eines Beitragstextes, bevor nach ähnlichen Themen gesucht wird." desktop_category_page_style: "Visueller Stil für die /categories Seite" @@ -1402,6 +1426,7 @@ de: pop3_polling_username: "Benutzername für die POP3-Abfrage." pop3_polling_password: "Passwort für die POP3-Abfrage." pop3_polling_delete_from_server: "E-Mails vom Server löschen. ACHTUNG: Wenn du dies deaktivierst, solltest du deinen E-Mail-Posteingang manuell aufräumen" + log_mail_processing_failures: "Protokolliere alle Fehler bei der E-Mail-Verarbeitung unter /logs" email_in: "Erlaube Benutzern, neue Themen per E-Mail einzutragen (erfordert manuelles oder POP3-Polling). Konfiguriere die Adressen im „Einstellungen“-Reiter in jeder Kategorie." email_in_min_trust: "Minimale Vertrauensstufe um neue Themen per E-Mail erstellen zu können." email_in_spam_header: "Die E-Mail-Kopfzeile, um Spam zu erkennen." @@ -1414,6 +1439,7 @@ de: delete_all_posts_max: "Die maximale Anzahl von Beiträgen welche auf einmal gelöscht werden kann. Hat ein Benutzer mehr Beiträge, so können die Beiträge nicht auf einmal und der Benutzer nicht gelöscht werden." username_change_period: "Die maximale Anzahl von Tagen, nach der Konten ihren Benutzernamen ändern können (0 verbietet die Änderung des Benutzernamen)." email_editable: "Erlaube Benutzern ihre E-Mail-Adresse nach der Registrierung zu ändern." + logout_redirect: "Adresse, auf die der Browser nach dem Abmelden weitergeleitet wird (z.B. https://example.com/logout)" allow_uploaded_avatars: "Benutzer können eigene Profilbilder hochladen." allow_animated_avatars: "Benutzer können animierte Profilbilder (.gif) hochladen und benutzen. ACHTUNG: Rufe den Befehl `avatars:refresh rake` auf nachdem du diese Option verändert hast." allow_animated_thumbnails: "Generiert animierte Vorschaubilder aus animierten GIFs." @@ -1435,6 +1461,7 @@ de: staff_user_custom_fields: "Eine Liste von benutzerdefinierten Benutzerfeldern, die vom Team eingesehen werden können." enable_user_directory: "Aktiviert ein durchsuchbares Benutzerverzeichnis" enable_group_directory: "Aktiviert ein durchsuchbares Gruppenverzeichnis" + group_in_subject: "Setze %{optional_pm} im E-Mail-Betreff auf die erste Gruppe in der Nachricht, siehe: Customize subject format for standard emails" allow_anonymous_posting: "Benutzern erlauben, in den anonymen Modus zu wechseln" anonymous_posting_min_trust_level: "Vertrauensstufe, ab der das Schreiben anonymer Beiträge erlaubt ist" anonymous_account_duration_minutes: "Erzeuge alle (n) Minuten ein neues anonymes Konto je Benutzer, um die Anonymität der virtuellen anonymen Benutzer zu gewährleisten. Beispiel: Ein Wert von 600 sorgt dafür, dass ein neues anonymes Konto erzeugt wird, wenn ein Benutzer in den anonymen Modus wechselt UND mindestens 600 Minuten seit der letzten anonymen Nachricht dieses Benutzers vergangen sind." @@ -1469,6 +1496,7 @@ de: default_code_lang: "Standard Syntax Highlighting, dass auf GitHub Code Blöcke angewendet wird. (lang-auto, ruby, python etc.)" warn_reviving_old_topic_age: "Wenn jemand beginnt auf ein Thema zu antworten, dessen letzte Antwort älter als diese Anzahl an Tagen ist, wird eine Warnung angezeigt. Deaktiviere dies durch setzen auf 0." autohighlight_all_code: "Erzwinge Syntaxhervorhebung für alle Quellcode-Blöcke, auch dann wenn keine Sprache angeben wurde." + highlighted_languages: "Aktivierter Regeln für das Syntax-Hervorhebung. (Warnung: Das Aktivieren zu vieler Sprachen kann die Leistung beeinträchtigen) siehe: https://highlightjs.org/static/demo für eine Demonstration" embed_truncate: "Kürze die eingebetteten Beiträge" embed_support_markdown: "Unterstütze Markdown-Formatierung für eingebettete Beiträge" allowed_href_schemes: "URI-Schemas, die in Links zusätzlich zu http und https erlaubt sind." @@ -1534,6 +1562,7 @@ de: show_filter_by_tag: "Zeige eine Dropdown-Liste, um eine Themenliste nach Schlagwort zu filtern." max_tags_in_filter_list: "Maximale Anzahl von Schlagwörtern, die in einer Dropdown-Liste angezeigt werden. Es werden die am häufigsten verwendeten Schlagwörter angezeigt." tags_sort_alphabetically: "Zeige Schlagwörter in alphabetischer Reihenfolge. Standardmäßig werden sie nach Beliebtheit sortiert." + tags_listed_by_group: "Zeige Schlagwörter auf der Schlagwort-Seite geordnet nach Schlagwort-Gruppe an." tag_style: "Visueller Stil für Schlagwort-Abzeichen." allow_staff_to_tag_pms: "Erlaube Team-Mitgliedern, jede Nachricht mit einem Schlagwort zu markieren." min_trust_level_to_tag_topics: "Minimale Vertrauensstufe, um Schlagwörter zu Themen hinzuzufügen." @@ -1579,6 +1608,10 @@ de: min_username_length_range: "Sie können nicht das Minimum größer setzen als das Maximum." max_username_length_exists: "Sie können nicht die maximale Länge der Benutzernamen kleiner setzen als der längste Benutzername ist (%{username})." max_username_length_range: "Sie können nicht das Maximum kleiner setzen als das Minimum." + placeholder: + sso_provider_secrets: + key: "www.example.com" + value: "SSO-Secret" search: within_post: "#%{post_number} von %{username}" types: @@ -1859,12 +1892,79 @@ de: test_mailer: title: "Test-E-Mail" subject_template: "[%{email_prefix}] Test der E-Mail-Zustellbarkeit" + text_body_template: | + Dies ist eine Test-E-Mail von + + [**%{base_url}**][0] + + E-Mail-Zustellbarkeit ist eine komplizierte Sache. Hier sind ein paar wichtige Dinge aufgeführt, die du am Anfang prüfen solltest: + + - Stelle *sicher*, dass du die `notification_email` (Absender von Benachrichtigungen) in den Einstellungen richtig gesetzt hast. **Die Domain in der "From"-Adresse der E-Mails, die du schickst, ist die Domain, gegen die deine E-Mail validiert wird**. + + - Lerne, wie du den Quelltext der E-Mail in deinem E-Mail-Programm anzeigen lassen kannst, damit du die E-Mail-Kopfzeilen auf wichtige Anhaltspunkte hin überprüfen kannst. In Google Mail ist dies die "Zeige Original"-Option in der Auswahlliste oben rechts von jeder E-Mail. + + - **WICHTIG:** Hat dein Internetdienstanbieter (ISP) einen Reverse-DNS-Eintrag eingegeben, der die Domainnamen und die IP-Adressen verknüpft, von denen du E-Mails verschickst? [Überprüfe deinen Reverse PTR-Eintrag][2] hier. Wenn dein ISP den Reverse-DNS-Eintrag nicht richtig eingibt, ist es sehr unwahrscheinlich, dass eine deiner E-Mails zugestellt wird. + + - Ist der [SPF-Eintrag][8] deiner Domain richtig? [Überprüfe deinen SPF-Eintrag][1] hier. Beachte, dass TXT der richtige Eintragstyp für SPF ist. + + - Ist der [DKIM-Eintrag][3] deiner Domain richtig? Dies wird die E-Mai-lZustellbarkeit signifikant verbessern. [Überprüfe deinen DKIM-Eintrag][7] hier. + + - Wenn du deinen eigenen E-Mail-Server betreibst, stelle sicher, dass die IPs deiner E-Mail-Server [nicht auf irgendwelchen E-Mail-Blacklist][4] stehen. Überprüfe auch, dass es definitiv einen vollqualifizierten Hostnamen in der HELO-Nachricht schickt, der in DNS auflöst. Wenn nicht, wird dies dazu führen, dass deine E-Mail von vielen E-Mail-Diensten abgelehnt wird. + + - Wir empfehlen dir sehr, **eine Test-E-Mail an [mail-tester.com][mt]** zu schicken, um zu überprüfen, dass alles erwähnte korrekt funktioniert. + + (Der *einfache* Weg besteht darin, ein kostenloses Konto bei [SendGrid][sg], [SparkPost][sp], [Mailgun][mg] oder [Mailjet][mj] anzulegen, die großzügige kostenlose E-Mail-Tarife haben und für kleine Communitys ausreichen werden. Du wirst allerdings dennoch die SPF- und DKIM-Einträge in deinen DNS-Einstellungen eintragen müssen!) + + Wir hoffen, dass du diesen Test der E-Mail-Zustellbarkeit erfolgreich erhalten hast! + + Viel Erfolg, + + Deine Freunde von [Discourse](https://www.discourse.org) + + [0]: %{base_url} + [1]: https://www.kitterman.com/spf/validate.html + [2]: https://mxtoolbox.com/ReverseLookup.aspx + [3]: http://www.dkim.org/ + [4]: https://whatismyipaddress.com/blacklist-check + [7]: https://www.mail-tester.com/spf-dkim-check + [8]: http://www.openspf.org/SPF_Record_Syntax + [sg]: https://goo.gl/r1WMF6 + [sp]: https://www.sparkpost.com/ + [mg]: https://www.mailgun.com/ + [mj]: https://www.mailjet.com/pricing + [mt]: https://www.mail-tester.com/ new_version_mailer: title: "Neue Version" subject_template: "[%{email_prefix}] Neue Discourse-Version, Update verfügbar" + text_body_template: | + Hurra, eine neue Version von [Discourse](https://www.discourse.org) ist verfügbar! + + Deine Version: %{installed_version} + Neue Version: **%{new_version}** + + - Aktualisiere mit unserem einfachen **[Ein-Klick-Browser-Upgrade](%{base_url}/admin/upgrade)**. + + - Schaue dir in den [Releasenotes](https://meta.discourse.org/tags/release-notes) an, was neu ist, oder rufe das [technische GitHub-Changelog](https://github.com/discourse/discourse/commits/master) auf. + + - Besuche [meta.discourse.org](https://meta.discourse.org) für Neuigkeiten, Diskussionen und Support für Discourse. new_version_mailer_with_notes: title: "Neue Version mit Versionshinweisen" subject_template: "[%{email_prefix}] Update verfügbar" + text_body_template: | + Hurra, eine neue Version von [Discourse](https://www.discourse.org) ist verfügbar! + + Deine Version: %{installed_version} + Neue Version: **%{new_version}** + + - Aktualisiere mit unserem einfachen **[Ein-Klick-Browser-Upgrade](%{base_url}/admin/upgrade)**. + + - Schaue dir in den [Releasenotes](https://meta.discourse.org/tags/release-notes) an, was neu ist, oder rufe das [technische GitHub-Changelog](https://github.com/discourse/discourse/commits/master) auf. + + - Besuche [meta.discourse.org](https://meta.discourse.org) für Neuigkeiten, Diskussionen und Support für Discourse. + + ### Releasenotes + + %{notes} flag_reasons: off_topic: "Dein Beitrag wurde als **Thema verfehlt** gemeldet: Die Community glaubt, dass er nicht zum Thema passt, wie es durch den Titel und den ersten Beitrag definiert wurde." inappropriate: "Dein Beitrag wurde als **unangemessen** gemeldet: die Community glaubt, dass er anstößig oder beleidigend ist oder einen Verstoß gegen [die Community Richtlinien](/guidelines) darstellt." @@ -2033,6 +2133,14 @@ de: csv_export_succeeded: title: "CSV-Export erfolgreich" subject_template: "[%{export_title}] Datenexport abgeschlossen" + text_body_template: | + Deine Daten wurden erfolgreich exportiert! :dvd: + + %{file_name} (%{file_size}) + + Der obenstehende Download-Link wird 48 Stunden gültig sein. + + Die Daten sind als Gzip-Archiv komprimiert. Wenn sich das Archiv beim Öffnen nicht selbst extrahiert, verwende eines der hier empfohlenen Werkzeuge: https://www.gzip.org/#faq4 csv_export_failed: title: "CSV-Export fehlgeschlagen" subject_template: "Datenexport fehlgeschlagen" @@ -2277,14 +2385,6 @@ de: spam_post_blocked: title: "Spam-Beitrag ausgeblendet" subject_template: "Beiträge des neuen Benutzers %{username} wegen mehrfacher Verlinkung blockiert" - text_body_template: | - Dies ist eine automatisierte Nachricht. - - Der neue Benutzer [%{username}](%{user_url}) hat versucht, mehrere Beiträge mit Links zur Domain %{domains} zu erstellen, aber diese Beiträge wuden blockiert, um Spam zu vermeiden. Der Benutzer kann weiter neue Beiträge erstellen, die nicht auf %{domains} verlinken. - - Bitte [überprüfe den Benutzer](%{user_url}). - - Dies kann über die Einstellungen `newuser_spam_host_threshold` und `white_listed_spam_host_domains` geändert werden. unsilenced: title: "Stummschaltung aufgehoben" subject_template: "Konto nicht mehr gesperrt" @@ -2716,7 +2816,6 @@ de: Ein Konto ist erforderlich. Bitte erstelle ein Konto oder melde dich an. terms_of_service: title: "Nutzungsbedingungen" - signup_form_message: 'Ich habe die Nutzungsbedingungen gelesen und akzeptiert.' deleted: 'gelöscht' image: "Bild" upload: @@ -2890,6 +2989,151 @@ de: Ja, die rechtliche Seite ist langweilig, aber wir müssen uns selbst schützen – und im erweiterten Sinne auch dich und deine Daten – vor unfreundlichen Leuten. Wir haben [Nutzungsbedingungen](/tos), die deines (und unser) Verhalten und unsere Rechte bezüglich des Inhalts, der Privatsphäre und den Gesetzen beschreiben. Um diesen Dienst zu nutzen, musst du dich mit unseren [Nutzungsbedingungen](/tos) einverstanden erklären. tos_topic: title: "Nutzungsbedingungen" + body: | + Die folgenden Geschäftsbedingungen sind maßgebend für die gesamte Webseite und alle Inhalte, Dienstleistungen und Produkte, die auf oder über die Webseite zur Verfügung gestellt werden, einschließlich, aber nicht beschränkt auf die %{company_domain} Foren-Software, die %{company_domain} Support-Foren und den %{company_domain} Hosting-Dienst („Hosting”), (zusammengefasst: die „Webseite“). Die Webseite gehört und wird betrieben von %{company_full_name} („%{company_name}“). Die Website wird angeboten vorbehaltlich der uneingeschränkten Zustimmung aller hierin enthaltenen Bedingungen und aller anderen sonstigen betrieblichen Regeln, Richtlinien (einschließlich, ohne Einschränkung, der [Datenschutzrichtlinien](/privacy) and [Community-Richtlinien](/faq)) von %{company_domain} und Verfahren, die von Zeit zu Zeit auf dieser Webseite von veröffentlicht werden können (zusammen die „Vereinbarung“). + + Bitte lies diese Vereinbarung sorgfältig durch, bevor du die Webseite verwendest oder darauf zugreifst. Durch die Benutzung oder den Zugriff auf einen Teil der Webseite stimmst du den Geschäftsbedingungen dieser Vereinbarung zu. Wenn du nicht allen Geschäftsbedingungen in dieser Vereinbarung zustimmst, dann darfst du weder auf die Seite zugreifen noch irgendwelche Dienstleistungen in Anspruch nehmen. Wenn diese Geschäftsbedingungen als Angebot von %{company_name} erachtet werden, beschränkt sich die Zustimmung ausdrücklich auf diese Bedingungen. Die Website wird nur für Benutzer angeboten, die mindestens 13 Jahre alt sind. + + + + ## [1. Dein %{company_domain}-Konto](#1) + + Wenn du eine Konto auf der Webseite erstellst, bist du verantwortlich, die Sicherheit deines Kontos aufrecht zu erhalten und du bist verantwortlich für alle Aktivitäten, die mit deinem Konto durchgeführt werden. Du musst %{company_name} umgehend über eine unbefugte Benutzung deines Kontos oder über jeden anderen Sicherheitsverstoß informieren. %{company_name} haftet nicht für Handlungen oder Unterlassungen deinerseits, einschließlich Schäden jeglicher Art, die infolge solcher Handlungen oder Unterlassungen eingetreten sind. + + + + ## [2. Pflichten der Mitwirkenden](#2) + + Wenn du Material, oder Links auf der Webseite veröffentlichst, anderweitig Material über die Webseite zur Verfügung stellst (oder Dritte dazu berechtigst) (jegliches Material als „Inhalt“), bist du für dessen Inhalt und den aus diesem Inhalt resultierenden Schaden in vollem Umfang verantwortlich. Dies gilt unabhängig davon, in welcher Form der Inhalt präsentiert wird, einschließlich, aber nicht beschränkt auf Text, Foto, Video, Audio oder Computer-Software. Indem du Inhalt zur Verfügung stellst, versicherst und garantierst du, dass: + + * das Herunterladen, Kopieren und die Benutzung des Inhalts keine proprietären Rechte verletzt, einschließlich, aber nicht beschränkt auf Urheberrecht, Patente, Schutzmarken oder Betriebsgeheimnissen irgendeines Dritten; + * sofern dein Arbeitgeber die Rechte an geistigem Eigentum hat, die du erstellst, entweder (i) du die Erlaubnis von deinem Arbeitgeber hast, den Inhalt zu einzustellen bzw. verfügbar zu machen, einschließlich, aber nicht beschränkt auf Software, oder (ii) deinem Arbeitgeber dir einen Verzicht auf alle Rechte an dem Inhalt versichert hat; + * du Fremdlizenzen im Zusammenhang mit dem Inhalt vollständig nachgekommen bist und dass du alle notwendigen Dinge unternommen hast, um erforderliche Bedingungen erfolgreich an die Endbenutzer weiterzureichen; + * der Inhalt keine Viren, Würmer, Malware, Trojaner oder anderen schädliche oder zerstörenden Inhalt enthält oder installiert; + * der Inhalt kein Spam und nicht von einer Maschine oder zufallsgeneriert ist und dass er keine sittenwidrigen oder unerwünschte kommerzielle Inhalte enthält, die darauf ausgerichtet sind, Zugriffe auf dritte Seiten zu lenken oder die Suchmaschinenplatzierung fördern, oder sonstige unerlaubte Handlungen (wie etwa Phishing) darstellen oder Empfänger hinsichtlich der Quelle des Materials täuschen (wie etwa Spoofing); + * der Inhalt nicht pornografisch ist, keine Bedrohungen enthält oder zu Gewalt auffordert, und dass er nicht gegen die Privatsprähe oder Persönlichkeitsrechte Dritter verstößt; + * dein Inhalt nicht über unerwünschte elektronische Nachrichten wie Spam-Links in Newsgroups, E-Mail-Verteilern, Blogs und Webseiten oder ähnliche unaufgeforderte Werbemaßnahmen beworben wird; + * dein Inhalt in keiner Weise bezeichnet ist, die die Leser täuschen zu glauben, dass du eine andree Person oder Firma bist; und + * dass du, im Falle von Inhalt, der Quelltext enthält, deren Art, Natur, Nutzen und Auswirkungen genau kategorisiert und/oder beschreibst, egal ob du dazu von %{company_name} aufgefordert wirst oder nicht. + + + + ## [3. Lizenz von Benutzerinhalten](#3) + + Benutzerbeiträge werden unter eine [Creative Commons Namensnennung – Nicht-kommerziell – Weitergabe unter gleichen Bedingungen 3.0 Unported-Lizenz](https://creativecommons.org/licenses/by-nc-sa/3.0/deed.de) gestellt. Ohne Einschränkung dieser Darstellungen und Garantien, hat %{company_name} das Recht (jedoch nicht die Pflicht) dazu, nach dem Ermessen von %{company_name} (i) jeglichen Inhalt abzulehnen oder zu entfernen der, nach eigenem Ermessen von %{company_name}, gegen eine Richtlinie von %{company_name} verstößt oder in einer anderen Weise schädlich oder bedenklich ist, oder (ii) einer Person oder juristischen Person den Zugriff auf und die Benutzung der Seite zu beenden oder zu verweigern, gleich aus welchem Grund, nach eigenem Ermessen von %{company_name}. %{company_name} hat keine Verpflichtung, Beiträge zu erstatten, die vorher bezahlt wurden. + + + + + ## [4. Zahlung und Verlängerung](#4) + + ### Allgemeine Bedingungen + + Optionale, kostenpflichtige Dienstleistungen oder Upgrades können auf der Webseite zur Verfügung stehen. Durch die Wahl optionaler, kostenpflichtigen Dienstleistungen oder Upgrades stimmst du der monatlichen oder jährlichen Zahlung der ausgewiesenen Gebühren an %{company_name} zu. Die Zahlungen werden im Voraus am Tag der Anmeldung für die Dienstleistungen oder Upgrades fällig und decken die Benutzung dieser Dienstleistung wie angegeben für einen Monats- oder Jahreszeitraum ab. Diese Gebühren sind nicht erstattungsfähig. + + ### Automatische Verlängerung + + Sofern du %{company_name} nicht vor Ablauf des jeweiligen Abonnementzeitraums darüber informierst, dass du eine Dienstleistung oder ein Upgrade kündigen möchtest, wird dein Abonnement automatisch verlängert und du ermächtigst uns, die zu diesem Zeitpunkt geltenden Gebühren für ein Jahres- oder Monatsabonnement (einschließlich aller Steuern) für mittels einer Kreditkarte oder sonstiger Zahlungsmittel einzuziehen, die du bei der Anmeldung angegeben hast. Dienstleistungen und Upgrades können jederzeit gekündigt werden. + + + + ## [5. Dienstleistungen](#5) + + ### Hosting, Support-Dienstleistungen + + Optionales Hosting und Support-Dienstleistungen können gemäß den Geschäftsbedingungen von %{company_name} für einen solchen Dienstleistung zur Verfügung gestellt werden. Durch die Registrierung eines Kontos für Hosting oder Support-Dienstleistungen, stimmst du diesen Geschäftsbedingungen zu. + + ### HTTPS + + Wir bieten HTTPS-Verschlüsselung als bezahltes Add-on an. Indem du dich anmeldest und eine benutzerdefinierte Domain auf %{company_domain} verwendest, autorisierst du uns, im Namen des Domaininhabers aufzutreten (zum Beispiel durch Anforderung benötigter Zertifikate) ausschließlich für den Zweck, HTTPS auf deiner Seite anzubieten. + + ### Unternehmen + + Hosting-Dienstleistungen für Unternehmen werden angeboten von%{company_name} nach den maßgeblichen Geschäftsbedingungen für jeden dieser Dienste, die in einem Kunden-spezifischen Vertrag festgelegt sind. Indem du dich für ein Unternehmen-Hosting-Konto registrierst, akzeptierst du, diese Geschäftsbedigungen zu befolgen. + + + + ## [6. Pflichten der Website-Besucher](#6) + + %{company_name} hat und kann nicht alle Materialien, einschließlich Computer-Software, die auf der Webseite veröffentlicht werden, überprüft/überprüfen, und kann deshalb nicht für die Inhalte, Verwendung oder Auswirkung dieses Materials haftbar gemacht werden. Durch den Betrieb der Webseite versichert oder impliziert %{company_name} nicht, dass das dort veröffentlichte Material gutgeheißen wird oder dass ein solches Material für zutreffend, nützlich oder ungefährlich gehalten wird. Du bist dafür verantwortlich, die notwendigen Sicherheitsvorkehrungen zu treffen, um dich und dein Computersystem vor Viren, Würmern, Trojanern und anderen schädlichen oder zerstörerischen Inhalten zu schützen. Die Webseite kann Inhalte enthalten, die beleidigend, unanständig oder anderweitig anstößig sind, sowie Inhalte, die technische Ungenauigkeiten, Schreibfehler und sonstige Fehler aufweisen. Diese Webseite kann auch Material enthalten, das gegen Eigentums- oder Veröffentlichungsrechte verstößt oder die Rechte an geistigem Eigentum oder sonstige Eigentumsrechte Dritter verletzt. Das Herunterladen, Kopieren oder Nutzen des Materials kann außerdem zusätzlichen genannten oder ungenannten Geschäftsbedingungen unterliegen. %{company_name} schließt jegliche Verantwortung für einen Schaden infolge der Benutzung durch Benutzer der Webseite oder infolge des Herunterladens der dort veröffentlichten Inhalte durch solche Benutzer aus. + + + + ## [7. Inhalt, der auf anderen Websites veröffentlicht wird](#7) + + Wir haben nicht alle Materialien, einschließlich Computer-Software, die auf den Webseiten und Seiten zur Verfügung gestellt, auf die %{company_domain} verlinkt oder die auf %{company_domain} verlinken, überprüft und können dies auch nicht tun. %{company_name} hat keine Kontrolle über diese Nicht-%{company_domain}-Webseiten und -Seiten und ist nicht verantwortlich für deren Inhalte oder Benutzung. DDu bist dafür verantwortlich, die notwendigen Sicherheitsvorkehrungen zu treffen, um dich und dein Computersystem vor Viren, Würmern, Trojanern und anderen schädlichen oder zerstörerischen Inhalten zu schützen. %{company_name} schließt jegliche Verantwortung für einen Schaden infolge der Benutzung von Nicht-%{company_domain}-Webseiten und -Seiten aus. + + + + ## [8. Verstoß gegen das Urheberrecht und DMCA-Richtlinien](#8) + + Genauso wie %{company_name} andere auffordert, Rechte an geistigem Eigentum zu respektieren, so respektiert auch %{company_name} die Rechte an geistigem Eigentum anderer. Wenn du der Ansicht bist, dass Material, das sich auf %{company_domain} befindet oder das von %{company_domain} verlinkt wird, gegen dein Urheberrecht verstößt, wirst du gebeten, %{company_name} gemäß der Richtlinie zum [Digital Millennium Copyright Act ("DMCA")]https://de.wikipedia.org/wiki/Digital_Millennium_Copyright_Act) von %{company_name} zu benachrichtigen. %{company_name} wird alle derartigen Mitteilungen beantworten und das verletztende Material wie erforderlich oder angemessen löschen oder alle Links auf das verletztende Material deaktivieren. %{company_name} wird den Zugang eines Besuchers auf und die Benutzung der Webseite kündigen, wenn unter entsprechenden Umständen der Benutzer nachweislich wiederholt gegen das Urheberrecht oder andere Rechte an geistigem Eigentum von %{company_name} oder anderen verstößt. Im Falle einer solchen Kündigung ist %{company_name} nicht verpflichtet, jegliche zuvor an %{company_name} bezahlten Beträge zu erstatten. + + + + ## [9. Geistiges Eigentum](#9) + + Durch diese Vereinbarung wird kein geistiges Eigentum von %{company_name} oder Dritten von %{company_name} auf dich übertragen, und alle Rechte, Titel und Rechtsansprüche an einem solchen Eigentum bleiben bei %{company_name} (wie zwischen den Parteien). %{company_name}, %{company_domain}, das %{company_domain}-Logo und alle anderen Marken, Dienstleistungsmarken, Grafiken und Logos, die in Verbindung mit %{company_domain} oder der Website verwendet werden, sind Marken oder eingetragene Marken von %{company_name} oder %{company_name}-Lizenznehmern. Andere Marken, Dienstleistungsmarken, Grafiken und Logos, die in Verbindung mit der Website verwendet werden, können die Marken anderer Dritter sein. Durch deine Benutzung der Website werden dir keine Rechte oder Lizenzen zur Vervielfältigung oder anderweitigen Benutzung jeglicher Marken von %{company_name} oder Dritten gewährt. + + + + ## [10. Werbeanzeigen](#10) + + %{company_name} behält sich das Recht vor, Werbeanzeigen auf deinem Inhalt anzuzeigen, sofern du kein „Keine Werbeanzeigen“-Upgrade erworben haben. + + + + ## [11. Namensnennung](#11) + + %{company_name} behält sich das Recht vor, Namensnennung-Links, wie z. B. „Powered by %{company_domain}“, Theme-Autor und Schriftartnennung in der Fußzeile oder der Werkzeugleiste anzuzeigen. Informationen in der Fußzeile und in der %{company_domain}-Werkzeugleiste dürfen ungeachtet der gekauften Upgrades nicht geändert oder gelöscht werden. + + + + ## [12. Änderungen](#12) + + %{company_name} behält sich das Recht vor, jeden Teil dieser Bestimmungen nach eigenem Ermessen zu verändern oder zu ersetzen. Du bist dafür verantwortlich, diese Bestimmungen regelmäßig auf Änderungen zu überprüfen. Deine weitere Benutzung or dein Zugang zur Webseite nach einer Änderung dieser Bestimmungen stellt eine Zustimmung dieser Änderungen dar. %{company_name} kann in der Zukunft auch neue Dienste und Funktionen über die Webseite (einschließlich der Veröffentlichung neuer Werkzeuge und Ressourcen) anbieten. Solche neuen Funktionen und Dienste unterliegen den Bedingungen dieser Geschäftsbedingungen. + + + + ## [13. Kündigung](#13) + + %{company_name} kann deinen Zugang zur gesamten Website oder einem Teil hiervon jederzeit und mit oder ohne Angabe eines Grundes, mit oder ohne Kündigungsfrist und mit sofortiger Wirkung kündigen. Wenn du diese Vereinbarung oder dein Konto auf %{company_domain} (falls du eines besitzt) kündigen möchtest, kannst du auch einfach die Benutzung der Webseite einstellen. Alle Bestimmungen dieser Vereinbarung, die gemäß ihrer Art die Kündigung überdauern sollten, überdauern die Kündigung, einschließlich, aber nicht beschränkt auf Eigentumsbestimmungen, Haftungsausschlüsse, Entschädigungen und Haftungsbeschränkungen. + + + + + ## [14. Haftungsausschluss](#14) + + Die Webseite wird „so wie sie ist“ zur Verfügung gestellt. %{company_name}, Vertragspartner und Lizenznehmer schließen hiermit sämtliche Gewährleistungen, ausdrücklich oder implizit, aus, einschließlich, aber nicht beschränkt auf die Gewährleistungen der Marktgängigkeit, der Eignung für einen bestimmten Zweck oder der Nichtverletzung von Rechten. Weder %{company_name} noch Vertragspartner und Lizenznehmer übernehmen jedwede Gewährleistung, dass die Website fehlerfrei ist oder dass der Zugang fortlaufend oder unterbrechungsfrei erfolgt. Wenn du dies tatsächlich liest, findest du hier [eine Belohnung](https://www.newyorker.com/online/blogs/shouts/2012/12/the-hundred-best-lists-of-all-time.html). Du verstehst, dass du nach eigenem Ermessen und auf eigene Gefahr Inhalte oder Leistungen von der Website herunterlädst oder über die Webseite einholst. + + + + + ## [15. Haftungsbeschränkung](#15) + + In keinem Fall haften %{company_name}, Vertragspartner oder Lizenznehmer in Bezug auf einen Gegenstand dieser Vereinbarung aufgrund eines Vertrages, aufgrund von Fahrlässigkeit, Gefährdungshaftung oder einer sonstigen Billigkeitstheorie für: (i) jegliche beiläufig entstandene Schäden, Folgeschäden oder sonstige Schäden; (ii) die Kosten für die Beschaffung von Ersatzprodukten oder Leistungen; (iii) die Benutzungsunterbrechung oder den Verlust oder die Beschädigung von Daten; oder (iv) für jegliche Beträge, die Gebühren übersteigen, die von dir an %{company_name} gemäß dieser Vereinbarung während eines Zeitraums von zwölf (12) Monaten vor dem Klageanspruch bezahlt wurden. %{company_name} übernimmt keine Haftung für einen Ausfall oder eine Verzögerung aufgrund von Tatsachen, die nach vernünftigem Ermessen über seine Kontrolle hinausgehen. Das Vorstehende gilt nicht, soweit nach anwendbarem Recht unzulässig. + + + + ## [16. Allgemeine Zusicherung und Gewährleistung](#16) + + Du versicherst und garantierst, dass (i) deine Benutzung der Webseite gemäß der [Datenschutzrichtlinie](/privacy) von %{company_name}, der [Community-Richtlinie](/guidelines), dieser Vereinbarung und allen geltenden Gesetzen und Bestimmungen erfolgt (einschließlich, aber nicht beschränkt auf jegliche lokale Gesetze oder Bestimmungen in Ihrem Land, Bundesland, Ihrer Stadt oder einem anderen Regierungsgebiet, hinsichtlich des Online-Verhaltens und zulässiger Inhalte, und einschließlich aller geltender Gesetze hinsichtlich der Übertragung technischer Daten, die aus den USA oder dieinem Wohnsitzland exportiert wurden) und (ii) deine Benutzung der Website nicht gegen die Rechte an geistigem Eigentum Dritter verstößt oder diese falsch anwendet. + + + + ## [17. Schadloshaltung](#17) + + Du stimmst zu, %{company_name}, Vertragspartnern und Lizenznehmern, und jeweiligen Vorstandsmitglieder, Amtsträger, Team-Mitglieder und Vertreter gegen jegliche Haftungsfälle, Ansprüche und Ausgaben schadlos zu halten, einschließlich angemessener Anwaltskosten, die den schadlos gehaltenen Parteien im Zusammenhang mit jeglichen Ansprüchen entstehen, die sich aus jeglicher Verletzung dieser Vereinbarung ergeben. + + + + ## [18. Verschiedenes](#18) + + Diese Vereinbarung stellt die gesamte Vereinbarung zwischen %{company_name} und dir bezüglich des Gegenstands dieser Geschäftsbedingungen dar, und sie kann nur durch eine schriftliche, durch einen befugten Vertreter von %{company_name} unterzeichnet oder durch die Veröffentlichung einer überarbeiteten Version durch %{company_name} geändert werden. Vorbehaltlich eventuell anwendbarer anders lautender gesetzlicher Bestimmungen, unterliegt diese Vereinbarung, der Zugang zu und die Benutzung der Website den Gesetzen des Staates Kalifornien, USA unter Ausschluss des Kollisionsrechts. Gerichtsstand für alle Streitigkeiten aus oder im Zusammenhang mit derselben ist der Staat und die Bundesgerichte in San Francisco County, Kalifornien. Mit Ausnahme der Ansprüche auf jede Unterlassung oder jeden Rechtsbehelf oder Ansprüche hinsichtlich der Rechte an geistigem Eigentum (die vor ein ordentliches Gericht ohne die Hinterlegung einer Einlage gebracht werden können) wird jeder Rechtsstreit, der gemäß dieser Vereinbarung entsteht, endgültig durch umfassende Schiedsregelungen des Judicial Arbitration and Mediation Service, Inc. („JAMS“) von drei Schlichtern, die gemäß solcher Regeln ernannt werden, beigelegt werden. Das Schiedsverfahren findet in San Francisco, Kalifornien in englischer Sprache statt, und die Schiedsentscheidung kann vor jedem Gericht durchgesetzt werden. Die obsiegende Partei im Rahmen einer Verhandlung oder eines Verfahrens zur Durchsetzung dieser Vereinbarung ist berechtigt, Unkosten und Anwaltsgebühren in Rechnung zu stellen. Wenn ein Teil dieser Vereinbarung für ungültig oder nicht einklagbar erachtet wird, wird dieser Teil so ausgelegt, dass die ursprüngliche Absicht der Parteien widergespiegelt wird, und die restlichen Abschnitte vollständig gültig bleiben. Eine Verzichtserklärung einer der Parteien hinsichtlich einer Bedingung oder Kondition dieser Vereinbarung oder ein Verstoß gegen eine solche Bedingung in einer anderen Instanz stellt keinen Verzicht auf eine solche Bestimmung oder eine nachfolgende Nichterfüllung einer solchen Bestimmung dar. Du kannst deine Rechte gemäß dieser Vereinbarung auf eine Partei übertragen, die sich mit folgenden Benutzungsbedingungen einverstanden erklärt und bestätigt, dass sie sich an die Geschäftsbedingungen halten wird; %{company_name} kann seine Rechte gemäß dieser Vereinbarung bedingungslos übertragen. Diese Vereinbarung ist bindend und wird zugunsten der Parteien und ihrer entsprechenden Erben und Rechtsnachfolger wirksam. + + Dieses Dokument ist als CC-BY-SA lizensiert. Es wurde zuletzt aktualisiert am 1. Januar 2018. + + Basiert ursprünglich auf den [WordPress-Geschäftsbedingungen](https://de.wordpress.com/tos/). privacy_topic: title: "Datenschutzrichtlinie" body: | @@ -3226,6 +3470,7 @@ de: staff_tag_disallowed: "Das Schlagwort \"%{tag}\" darf nur vom Team verwendet werden." staff_tag_remove_disallowed: "Das Schlagwort \"%{tag}\" darf nur vom Team entfernt werden." minimum_required_tags: "Du musst mindestens %{count} Schlagwörter auswählen." + upload_row_too_long: "Die CSV-Datei sollte ein Schlagwort pro Zeile haben. Optional kann das Schlagwort von einem Komma und dem Namen einer Schlagwortgruppe gefolgt sein." rss_by_tag: "Themen mit dem Schlagwort %{tag}" finish_installation: congratulations: "Glückwunsch, du hast Discourse installiert!" @@ -3294,6 +3539,7 @@ de: description: "E-Mail-Adresse der verantwortlichen Person oder Gruppe für diese Community. Wird verwendet für kritische Benachrichtigungen wie unbehandelte Meldungen, Sicherheitsaktualisierungen sowie auf eurer „Über uns“-Seite für dringenden Community-Kontakt." contact_url: label: "Webseite" + placeholder: "https://www.example.com/kontaktiere-uns" description: "Allgemeines Kontaktformular für euch oder eure Organisation. Wird angezeigt auf eurer „Über uns“-Seite." site_contact: label: "Automatische Nachrichten" @@ -3363,9 +3609,6 @@ de: description: "Du hast es fast geschafft! Lass’ uns ein paar Leute einladen, die dabei helfen eure Diskussionen anzuregen mit interessanten Themen und Beiträgen, um deine Community zu starten." finished: title: "Dein Discourse ist bereit!" - description: | -

    Wenn du diese Einstellungen jemals ändern möchtest, besuche deinen Administrationsbereich; finde ihn neben dem Schraubenschüssel-Symbol im Seitenmenü..

    -

    Viel Spaß, und viel Glück beim Aufbauen deiner neuen Community!

    search_logs: graph_title: "Anzahl Suchen" joined: "Beigetreten" diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index ea73e51ac4..354eb4a314 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -548,8 +548,6 @@ el: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Ανάρμοστο' - description: 'Το περιεχόμενο αυτής της δημοσίευσης θα θεωρούνταν από κάθε λογικό άνθρωπο, προσβλητικό, καταχρηστικό ή αντίθετο με τις οδηγίες χρήσης της κοινότητάς μας' - short_description: 'Παράβαση των Οδηγιών της Κοινότητας' long_form: 'το επισήμαναν ως ανάρμοστο' notify_user: title: 'Αποστολή μηνύματος στον @{{username}} ' @@ -592,11 +590,9 @@ el: long_form: 'το επισήμαναν ως ανεπιθύμητο' inappropriate: title: 'Ανάρμοστο' - description: 'Αυτό το θέμα έχει περιεχόμενο το οποίο θα θεωρούνταν από κάθε λογικό άνθρωπο προσβλητικό, καταχρηστικό ή αντίθετο με τις οδηγίες χρήσης της κοινότητας μας.' long_form: 'το επισήμαναν ως ανάρμοστο' notify_moderators: title: "Κάτι άλλο" - description: 'Το θέμα απαιτεί την γενική προσοχή του προσωπικού, βασισμένη στις κατευθυντήριες γραμμές, τουςόρους χρήσης ή για άλλο λόγο που δεν αναφέρεται πιο πάνω.' long_form: 'το επισήμαναν για έλεγχο από συντονιστή' email_title: 'Το θέμα "%{title}" χρειάζεται έλεγχο από συντονιστή' email_body: "%{link}\n\n%{message}" @@ -782,11 +778,6 @@ el: Περισσότερα για το Sidekiq εδώ.' queue_size_warning: 'Ο αριθμός των εργασιών που βρίσκονται σε λίστα αναμονής είναι %{queue_size}, το οποίο είναι υψηλό. Αυτό θα μπορούσε να ενδείξει πρόβλημα με τις λειτουργία(ες) του Sidekiq ή θα πρέπει να προσθέσετε κι άλλους Sidekiq εργάτες. ' memory_warning: 'Ο server σας έχει λιγότερο από 1 GB διαθέσιμης μνήμης. Προτείνεται η χρήση τουλάχιστον 1 GB μνήμης.' - google_oauth2_config_warning: 'Ο server είναι ρυθμισμένος να επιτρέπει την εγγραφή και την είσοδο με Google OAuth2 (enable_google_oauth2_logins), αλλά η ταυτότητα του πελάτη και οι μυστικές τιμές δεν έχουν οριστεί. Πηγαίνετε στις Ρυθμίσεις Σελίδας και ανανεώστε τις ρυθμίσεις. Δείτε τον οδηγό για περισσότερα.' - facebook_config_warning: 'Ο server είναι ρυθμισμένος να επιτρέπει την εγγραφή και την είσοδο με Facebook (enable_facebook_logins), αλλά η ταυτότητα του πελάτη και οι μυστικές τιμές δεν έχουν οριστεί. Πηγαίνετε στις Ρυθμίσεις Σελίδας και ανανεώστε τις ρυθμίσεις. Δείτε τον οδηγό για περισσότερα.' - twitter_config_warning: 'Ο server είναι ρυθμισμένος να επιτρέπει την εγγραφή και την είσοδο με Twitter (enable_twitter_logins), αλλά η ταυτότητα του πελάτη και οι μυστικές τιμές δεν έχουν οριστεί. Πηγαίνετε στις Ρυθμίσεις Σελίδας και ανανεώστε τις ρυθμίσεις. Δείτε τον οδηγό για περισσότερα.' - github_config_warning: 'Ο server είναι ρυθμισμένος να επιτρέπει την εγγραφή και την είσοδο με GitHub (enable_github_logins), αλλά η ταυτότητα του πελάτη και οι μυστικές τιμές δεν έχουν οριστεί. Πηγαίνετε στις Ρυθμίσεις Σελίδας και ανανεώστε τις ρυθμίσεις. Δείτε τον οδηγό για περισσότερα.' - failing_emails_warning: 'Υπάρχουν %{num_failed_jobs} εργασίες email οι οποίες απέτυχαν. Ελέγξτε το app.yml και βεβαιωθείτε πως οι ρυθμίσεις του mail server σας είναι σωστές. Δείτε τις αποτυχημένες εργασίες στο Sidekiq.' subfolder_ends_in_slash: "Οι ρυθμίσεις του υποφακέλου σας δεν είναι σωστές. Το DISCOURSE_RELATIVE_URL_ROOT τελειώνει με κάθετη. " email_polling_errored_recently: one: "To Email polling έχει παράγει ένα σφάλμα τις τελευταίες 24 ώρες . Δείτε τα αρχεία καταγραφής για περισσότερες λεπτομέρειες. " @@ -918,14 +909,12 @@ el: password_unique_characters: "Ελάχιστος αριθμός μοναδικών χαρακτήρων που πρέπει να έχει ένας κωδικός." block_common_passwords: "Μην επιτρέπεις τους κωδικούς πρόσβασης που είναι ανάμεσα στους 10,000 πιο κοινούς κωδικούς πρόσβασης." enable_sso: "Ενεργοποίηση καθολικής σύνδεσης μέσω ενός εξωτερικού συνδέσμου (ΠΡΟΕΙΔΟΠΟΙΗΣΗ: ΟΙ ΔΙΕΥΘΥΝΣΕΙΣ EMAiL ΤΩΝ ΧΡΗΣΤΩΝ ΘΑ ΠΡΕΠΕΙ ΝΑ ΕΠΙΚΥΡΩΘΟΥΝ ΑΠΟ ΤΟΝ ΕΞΩΤΕΡΙΚΟ ΣΥΝΔΕΣΜΟ!)" - enable_sso_provider: "Εφαρμογή πρωτοκόλλου παροχέα Discourse SSO στο τελικό σημείο του /session/sso_provider, απαιτεί να τεθει το sso_secret." sso_url: "URL του single sign on endpoint (πρέπει να περιέχει http:// or https://)" sso_secret: "Η μυστική συμβολοσειρά που χρησιμοποιείται για να πιστοποιήσει κρυπτογραφικά πληροφορίες SSO, σιγουρευτείτε οτι έχει 10 χαρακτήρες ή περισσότερους." sso_overrides_bio: "Παρακάμπτει την βιογραφία του χρήστη στο προφίλ του χρήστη και τον αποτρέπει να την αλλάξει. " sso_overrides_email: "Παράκαμψη τοπικής διεύθυνσης email με τη διεύθυνση email από τον εξωτερικό ιστότοπο με φορτίο SSO σε κάθε σύνδεση, και αποτροπή τοπικών αλλαγών. (ΠΡΟΕΙΔΟΠΟΙΗΣΗ: μπορεί να προκληθούν ασυμφωνίες λόγω της ομαλοποίησης των τοπικών διευθύνσεων email)" sso_overrides_username: "Παράκαμψη τοπικού ονόματος χρήστη με το όνομα χρήστη από τον εξωτερικό ιστότοπο SSO σε κάθε σύνδεση και αποτροπή τοπικών αλλαγών. (ΠΡΟΕΙΔΟΠΟΙΗΣΗ: μπορεί να προκληθούν ασυμφωνίες λόγω των διαφορών μεταξύ των ονομάτων χρηστων όσο αναφορά το μάρκος/απαιτήσεις)" sso_overrides_name: "Παράκαμψη τοπικού ονόματεπώνυμου με το όνομα χρήστη από τον εξωτερικό ιστότοπο SSO σε κάθε σύνδεση και αποτροπή τοπικών αλλαγών. " - sso_overrides_avatar: "Παράκαμψη τοπικού άβαταρ χρήστη με το άβαταρ από τον εξωτερικό ιστότοπο SSO Εάν ενεργοποιημένο, η αποσύνδεση του allow_uploaded_avatars συνιστάται." sso_not_approved_url: "Ανακατευθύνετε μη εγκεκριμένους λογαριασμούς SSO σε αυτό το URL" sso_allows_all_return_paths: "Do not restrict the domain for return_paths provided by SSO (by default return path must be on current site)" allow_new_registrations: "Επιτρέψτε τις νέες εγγραφές χρηστών. Απενεργοποιήστε την επιλογή αυτή για να αποτρέψετε σε οποιονδήποτε να δημιουργήσει ένα νέο λογαριασμό" @@ -940,7 +929,6 @@ el: allow_restore: "Επίτρεψε επαναφορά, η οποία μπορεί να αντικαταστήσει ΟΛΑ τα δεδομένα του ιστότοπου! Αφήστε απενεργοποιημένο, εκτός αν σχεδιάζετε να επαναφέρετε αντίγραφο ασφαλείας" maximum_backups: "Το μέγιστο ποσό αντιγράφων ασφαλείας το οποία θα διατηρηθούν στο δίσκο. Παλαιότερα αντίγραφα ασφαλείας διαγράφονται αυτόματα" automatic_backups_enabled: "Πάρε αυτόματα αντίγραφα ασφαλείας όπως ορίζεται στη συχνότητα αντιγράφων ασφαλείας" - enable_s3_backups: "Ανέβασε τα αντίγραφα αφαλείας στο S3 όταν ολοκληρωθούν. ΣΗΜΑΝΤΙΚΟ: απαιτεί έγκυρα διαπιστευτήρια S3 καταχωρημένα στις ρυθμίσεις αρχείων." s3_backup_bucket: "Απομακρυσμένος αποθηκευτικός χώρος για τα αντίγραφα ασφαλείας. ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Βεβαιωθείτε οτι ο χώρος είναι προσωπικός." s3_disable_cleanup: "Απενεργοποιήστε την αφαίρεση των αντιγράφων ασφαλείας από το S3 όταν αφαιρεθούν τοπικά" backup_time_of_day: "Ώρα σε ζώνη UTC που το αντίγραφο ασφαλείας πρέπει να πραγματοποιηθεί." @@ -2174,7 +2162,6 @@ el: Παρακαλούμε δημιουργήστε έναν λογαριασμό ή συνδεθείτε για να συνεχίσετε.. terms_of_service: title: "Όροι Χρήσης" - signup_form_message: 'Έχω διαβάσει και αποδέχομαι τους Όρους Χρήσης.' deleted: 'διεγράφη' image: "εικόνα" upload: @@ -2714,6 +2701,3 @@ el: description: "Έχεις σχεδόν τελειώσει! Ας προσκαλέσουμε μερικούς χρήστες να συμμετάσχουν στις συζητήσεις με ενδιαφέροντα νήματα και αναρτήσεις ώστε να ξεκινήσει να λειτουργεί η κοινότητα. " finished: title: "Το Discourse σου είναι έτοιμο!" - description: | -

    Αν ποτέ θελήσεις να αλλάξεις αυτές τις ρυθμίσεις, επισκέψου την σελίδα διαχείρισης; θα τη βρεις δίπλα στο εικονίδιο του εργαλείου στο μενού ιστοτόπου.

    -

    Καλή διασκέδαση και καλή επιτυχία στην κατασκευή της νέας σου κοινότητας!

    diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 070de08714..361436cdef 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -67,6 +67,7 @@ es: maximum_staged_user_per_email_reached: "Alcanzado el número máximo de usuarios provisionales creados por email." no_subject: "(sin título)" no_body: "(sin mensaje)" + missing_attachment: '(falta archivo adjunto %{filename})' errors: empty_email_error: "Sucede cuando el texto en bruto del email que recibimos está en blanco." no_message_id_error: "Sucede cuando el email no tiene Id del mensaje en el encabezado." @@ -116,6 +117,7 @@ es: odd: debe ser impar record_invalid: 'Error en la validación: %{errors}' max_emojis: "no puede tener más de %{max_emojis_count} emoji" + emojis_disabled: "no puede tener emoji" ip_address_already_screened: "ya se está incluido en una regla existente" restrict_dependent_destroy: one: "No se pudo eliminar el registro porque existe otro %{record} dependiente" @@ -143,6 +145,7 @@ es: other: 'Has especificado las opciones inválidas %{name}' default_categories_already_selected: "No se puede seleccionar una categoría ya utilizada en otra lista." s3_upload_bucket_is_required: "No se pueden activar las subidas a S3 a menos que se haya proporcionado un valor a 's3_upload_bucket'." + s3_backup_requires_s3_settings: "No puedes usar S3 como método de copia de seguridad salvo que hayas rellenado '%{setting_name}'." conflicting_google_user_id: 'El ID de la cuenta Google para esta cuenta ha cambiado; el staff debe intervenir por razones de seguridad. Por favor, póngase en contacto con miembros del staff y envíe esta referencia
    https://meta.discourse.org/t/76575' activemodel: errors: @@ -166,6 +169,10 @@ es: backup_file_should_be_tar_gz: "El archivo de la copia de seguridad debería ser del tipo .tar.gz" not_enough_space_on_disk: "No hay espacio suficiente en el disco para subir esta copia de seguridad." invalid_filename: "El nombre de archivo del backup contiene caracteres no válidos. Los válidos son a-z 0-9 . - _." + file_exists: "El archivo que estás intentando subir ya existe." + location: + local: "Local" + s3: "Amazon S3" invalid_params: "Proporcionó parámetros no válidos para la solicitud: %{message}" not_logged_in: "Tienes que iniciar sesión para hacer eso." not_found: "No se ha podido encontrar la URL o recurso solicitado." @@ -261,6 +268,7 @@ es: tag: "Temas etiquetados" badge: "%{display_name} distintivo en %{site_title}" too_late_to_edit: "Ese post fue publicado hace demasiado tiempo. No puede ser editado ni eliminado." + edit_conflict: "La publicación ha sido editada por otro usuario y tus cambios no pueden ser guardados." revert_version_same: "La versión actual es la misma que la versión a la que intentas volver." excerpt_image: "imagen" queue: @@ -603,6 +611,32 @@ es: email_login: invalid_token: "Lo sentimos, ese enlace de inicio de sesión de correo electrónico es demasiado viejo. Seleccione el botón Iniciar sesión y use 'Olvidé mi contraseña' para obtener un nuevo enlace." title: "Email login" + user_auth_tokens: + browser: + chrome: "Google Chrome" + safari: "Safari" + firefox: "Firefox" + opera: "Opera" + ie: "Internet Explorer" + edge: "Microsoft Edge" + unknown: "navegador desconocido" + device: + android: "Dispositivo Android" + ipad: "iPad" + iphone: "iPhone" + ipod: "iPod" + mobile: "Dispositivo móvil" + mac: "Mac" + linux: "Ordenador GNU/Linux" + windows: "Ordenador Windows" + unknown: "dispositivo desconocido" + os: + android: "Android" + ios: "iOS" + macos: "macOS" + linux: "Linux" + windows: "Microsoft Windows" + unknown: "sistema operativo desconocido" change_email: confirmed: "Tu email ha sido actualizado." please_continue: "Continuar a %{site_name}" @@ -644,8 +678,8 @@ es: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Inapropiado' - description: 'Este post contiene contenido que una persona sensata podría considerar ofensivo, abusivo o que viola nuestras directrices de comunidad.' - short_description: 'Una violación a las guías de nuestra comunidad' + description: 'Esta publicación tiene contenido que una persona razonable podría considerar ofensivo, abusivo o una violación de nuestras directrices de la comunidad.' + short_description: 'Infringe nuestras directrices de la comunidad' long_form: 'reportado como inapropiado' notify_user: title: 'Enviar un mensaje a @{{username}}' @@ -694,12 +728,9 @@ es: short_description: 'Esto es un anuncio' inappropriate: title: 'Inapropiado' - description: 'Este tema contiene material que una persona sensata podría considerar ofensivo, abusivo, o que viola las directrices de nuestra comunidad.' long_form: 'marcado como inapropiado' - short_description: 'Incumple nuestras directrices de la comunidad' notify_moderators: title: "Notificar a los moderadores" - description: 'Este tema requiere atención del equipo de moderación basándose en las pautas de la comunidad o los Términos y condiciones, o por otra razón no especificada arriba.' long_form: 'reportado para atención de los moderadores' short_description: 'Requiere atención del staff por otro motivo' email_title: 'El tema "%{title}" requiere la atención de un moderador' @@ -746,6 +777,9 @@ es: session_info: "Leer información de la sesión del usuario" read: "Leer todo" write: "Escribir todo" + flags: + errors: + already_handled: "El reporte ya ha sido atendido" reports: default: labels: @@ -759,6 +793,13 @@ es: editor: Editor author: Autor edit_reason: Motivo + most_disagreed_flaggers: + labels: + user: Usuari + agreed_flags: Reportes de acuerdo + disagreed_flags: Reportes en desacuerdo + ignored_flags: Reportes ignorados + score: Puntuación moderators_activity: title: "Actividad de moderadores" labels: @@ -972,17 +1013,22 @@ es: labels: user_agent: "Agente de usuario" page_views: "Páginas vistas" + suspicious_logins: + title: "Inicios de sesión sospechosos" + labels: + user: Usuario + client_ip: IP del cliente + location: Ubicación + browser: Navegador + device: Dispositivo + os: Sistema operativo + login_time: Fecha y hora de inicio de sesión dashboard: rails_env_warning: "Tu servidor está funcionando en modo de %{env}." host_names_warning: "Tu archivo 'config/database.yml' está utilizando un hostname predeterminado de 'localhost'. Actualízalo para usar el hostname de tu sitio." sidekiq_warning: 'Sidekiq no está funcionando. Muchas tareas, por ejemplo el envío de correos, están realizadas desincronizadamente por sidekiq. Por favor asegúrate de que por lo menos un proceso de sidekiq está funcionando. Learn about Sidekiq here.' queue_size_warning: 'El número de jobs en cola es %{queue_size}, se trata de una cifra alta. Esto podría indicar un problema en el proceso (o procesos) de Sidekiq, o quizá se deba añadir más Sidekiq workers.' memory_warning: 'Tu servidor está funcionando con menos de 1 GB de memoria total. Por lo menos 1 GB de memoria es recomendado.' - google_oauth2_config_warning: 'El servidor está configurado para permitir el registro e inicio de sesión con Google OAuth2 (enable_google_oauth2_logins), pero los valores client id y client secret están vacíos. Ve a Ajustes del sitio y actualiza la configuración. Mira esta guía para saber más.' - facebook_config_warning: 'El servidor está configurado para permitir crear una cuenta e ingresar utilizando Facebook (enable_facebook_logins), pero los valores id y secreto de la app no están configurados. Ingresa a la Configuración del Sitio y actualiza la configuración. Revisa la guía para aprender más.' - twitter_config_warning: 'El servidor está configurado para permitir crear una cuenta e ingresar utilizando Twitter (enable_twitter_logins), pero los valores clave y secreto de la app no están configurados. Ingresa a la Configuración del Sitio y actualiza la configuración. Revisa la guía para aprender más.' - github_config_warning: 'El servidor está configurado para permitir crear una cuenta e ingresar utilizando GitHub (enable_github_logins), pero los valores de cliente clave y cliente secreto no están configurados. Ingresa a la Configuración del Sitio y actualiza la configuración. Revisa la guía para aprender más.' - failing_emails_warning: 'Hay %{num_failed_jobs} jobs de email que fallaron. Revisa tu app.yml y asegúrate que la configuración del servidor de mail es correcta. Mira los jobs fallados en Sidekiq.' subfolder_ends_in_slash: "La configuación del subdirectorio no es correcta; el campo DISCOURSE_RELATIVE_URL_ROOT termina con una barra." email_polling_errored_recently: one: "El email polling ha generado un error en las pasadas 24 horas. Mira en los logs para más detalles." @@ -1136,7 +1182,6 @@ es: password_unique_characters: "Número mínimo de caracteres únicos que la contraseña debe tener." block_common_passwords: "No permitir contraseñas que están entre las 10.000 más comunes." enable_sso: "Activar single sign on a través de un sitio externo (AVISO: ¡LAS DIRECCIONES DE EMAIL SERÁN VALIDADAS POR EL SITIO EXTERNO!)" - enable_sso_provider: "Implementar el protocolo del proveedor de SSO de Discourse en el endpoint /session/sso_provider requiere establecer un sso_secret" sso_url: "URL del endpoint para el single sign on (debe incluir http:// o https://)" sso_secret: "Cadena secreta utilizada para autenticar criptográficamente la información del SSO, asegúrate de que es de 10 caracteres o más" sso_overrides_bio: "Sobreescribe la bio del usuario en su perfil y deshabilita que pueda cambiarla" @@ -1144,7 +1189,6 @@ es: sso_overrides_email: "Sobreescribe el email local con el email que provea sitio externo desde el payload del SSO en cada registro, y previene cambios locales. (AVISO: pueden ocasionarse discrepancias debido a la normalización de los emails locales)" sso_overrides_username: "Sobreescribe el usuario local con el usuario que provea el payload del SSO en cada registro, y previene cambios locales. (AVISO: pueden ocasionarse discrepancias debido a las diferencias en los requirimientos/longitud del usuario)" sso_overrides_name: "Sobreescribe el nombre completo con el que provee el payload del SSO en cada registro, y previene cambios locales." - sso_overrides_avatar: "Sustituye el avatar de usuario con el avatar proveniente del sitio externo configurado para el SSO. Si se activa esta opción, se recomienda encarecidamente deshabilitar allow_uploaded_avatars" sso_overrides_profile_background: "Sobrescribe el fondo del perfil del usuario con el avatar del sitio externo desde SSO payload." sso_overrides_card_background: "Sobrescribe el fondo de la tarjeta de usuario con el avatar de sitio externo desde SSO payload." sso_not_approved_url: "Redireccionar cuentas SSO sin aprobar a esta URL" @@ -1165,7 +1209,6 @@ es: maximum_backups: "La cantidad máxima de copias de seguridad a tener en el disco. Las copias de seguridad más antiguas se eliminan automáticamente" automatic_backups_enabled: "Ejecutar backups automáticos definidos por la opción de frecuencia de backups" backup_frequency: "El número de días entre backups." - enable_s3_backups: "Sube copias de seguridad a S3 cuando complete. IMPORTANTE: requiere credenciales validas de S3 puestas Archivos configuración." s3_backup_bucket: "El bucket remoto para mantener copias de seguridad. AVISO: Asegúrate de que es un bucket privado." s3_endpoint: "El punto final se puede modificar para realizar una copia de seguridad en un servicio compatible con S3 como DigitalOcean Spaces o Minio. ADVERTENCIA: utilice el valor predeterminado si usa AWS S3" s3_force_path_style: "Haga cumplir el direccionamiento de estilo de ruta para su punto final personalizado. IMPORTANTE: se requiere para usar cargas y copias de seguridad desde Minio." @@ -1544,7 +1587,7 @@ es: user: 'Usuarios' results_page: "Resultados de búsqueda para '%{term}'" sso: - login_error: "Error en Ingreso" + login_error: "Error al iniciar sesión" not_found: "No se pudo encontrar tu cuenta. Por favor contacta al administrador del sitio." account_not_approved: "Tu cuenta está pendiente de aprobación. Recibirás una notificación por email cuando se apruebe." unknown_error: "Hay un problema con tu cuenta. Por favor contacta al administrador del sitio." @@ -2204,14 +2247,6 @@ es: spam_post_blocked: title: "Post con Spam Bloqueado" subject_template: "Nuevo usuario %{username} posts bloqueados debido a repetición de enlaces." - text_body_template: | - Este es un mensaje automático. - - El usuario nuevo [%{username}](%{user_url}) ha intentado crear múltiples temas con enlaces a %{domains}, pero han sido bloqueados para evitar spam. El usuario todavía puede crear temas, siempre y cuando no contengan enlaces a %{domains}. - - Por favor, [echa un vistazo al usuario](%{user_url}). - - Esto puede ser modificado con los ajustes `newuser_spam_host_threshold` y `white_listed_spam_host_domains`. unsilenced: title: "Dejar de silenciar" subject_template: "Cuenta no retenida" @@ -2537,8 +2572,8 @@ es: Haz clic en el siguiente enlace para cambiar tu contraseña: %{base_url}/u/password-reset/%{email_token} email_login: - title: "Ingreso vía enlace" - subject_template: "[%{email_prefix}] Ingreso vía enlace" + title: "Iniciar sesión con un enlace" + subject_template: "[%{email_prefix}] Iniciar sesión con un enlace" text_body_template: | Aquí tienes tu enlace para ingresar a [%{site_name}](%{base_url}). @@ -2560,7 +2595,7 @@ es: title: "Admin Login" subject_template: "[%{email_prefix}] Inicio de sesión" text_body_template: | - Alguien pidió iniciar sesión con tu cuenta en [%{site_name}](%{base_url}). + Alguien ha pedido iniciar sesión con tu cuenta en [%{site_name}](%{base_url}). Si tú no realizaste esta petición, puedes ignorar este email. @@ -2640,10 +2675,9 @@ es: login_required: welcome_message: | ## [Bienvenido a %{title}](#welcome) - Una cuenta es requerida. Por favor cree una cuenta o inicie sesión para continuar. + Hace falta una cuenta para usar el sitio. Por favor crea una o inicia sesión para continuar. terms_of_service: title: "Condiciones Generales de Uso" - signup_form_message: 'He leído y acepto las Condiciones de Servicio.' deleted: 'borrado' image: "imagen" upload: @@ -3233,9 +3267,6 @@ es: description: "¡Ya casi estás listo! Vamos a invitar otras personas para que ayuden a iniciar discusiones con temas interesantes y respuestas para que comience tu comunidad." finished: title: "Tu foro de Discourse está listo!" - description: | -

    Si alguna vez quieres cambiar alguno de estos ajustes, visita tu sección de administrador; la puedes encontrar el lado del icono de una llave inglesa en el menú de la página..

    -

    Diviértete, y ¡buena suerte construyendo tu nueva comunidad!

    search_logs: graph_title: "Cuenta de Búsquedas" joined: "Registrado" diff --git a/config/locales/server.et.yml b/config/locales/server.et.yml index f913853dcb..2810e15e28 100644 --- a/config/locales/server.et.yml +++ b/config/locales/server.et.yml @@ -113,6 +113,9 @@ et: backup_file_should_be_tar_gz: "Varukoopia peab olema .tar.gz arhiivifail." not_enough_space_on_disk: "Varukoopia üleslaadimiseks pole piisavalt kettaruumi." invalid_filename: "Varundusfaili nimi sisaldab keelatud sümboleid. Lubatud sümbolid on a-z 0-9 . - _." + location: + local: "Kohalik" + s3: "Amazon S3" not_logged_in: "Selleks peate olema sisse logitud." not_found: "Päritud URLi või ressurssi ei leitud." invalid_access: "Päritud ressursi vaatamiseks puuduvad õigused." @@ -307,6 +310,7 @@ et: replace_paragraph: "(Asenda see paragrahv oma uues kategoorias kategooria lühitutvustusega. See juhis ilmub siis kategooriate vaates. Seega, katsu hoida kirjeldus alla 200 tähemärgi. **Kuniks sa pole muutnud seda kirjeldust või kuni sa pole loonud teemasid, ei ole seda kategooriat kategooriate lehel näha.**)" post_template: "%{replace_paragraph}\n\nKasuta järgnevaid lõikusid pikema kirjelduse jaoks või foorumi juhendi või reeglite jaoks:\n\n- Miks peaksid inimesed seda foorumit kasutama? Mille jaoks see on?\n\n- Mille poolest erineb see meie teistest foorumitest?\n\n- Mis infot peaksid selle foorumi teemad sisaldama?\n\n- Kas meil on seda foorumit vaja? Kas me saame seda foorumit ühendada teise foorumiga või alamfoorumiga?\n" errors: + not_found: "Kategooriat ei leitud!" uncategorized_parent: "'Liigitamata' ei saa omada vanem-liiki" self_parent: "Alamliigi vanemaks ei tohi olla alamliik ise" depth: "Alamliiki ei saa paigutada teise alamliigi alla" @@ -330,6 +334,9 @@ et: leader: title: "juht" change_failed_explanation: "Üritasid alandada %{user_name} tasemele '%{new_trust_level}'. Samas on tal juba tase'%{current_trust_level}'. %{user_name} jääb tasemele '%{current_trust_level}' - kui soovid alandada kasutaja taset, siis lukusta kõigepealt usaldustase." + post: + image_placeholder: + broken: "See pilt on katki" rate_limiter: hours: one: "1 tund" @@ -420,6 +427,32 @@ et: title: 'Uuenda Parool' success: "Sinu parooli muutmine õnnestus ja oled nüüd sisse logitud." success_unapproved: "Sinu parooli muutmine õnnestus." + user_auth_tokens: + browser: + chrome: "Google Chrome" + safari: "Safari" + firefox: "Firefox" + opera: "Opera" + ie: "Internet Explorer" + edge: "Microsoft Edge" + unknown: "tundmatu brauser" + device: + android: "Androidi seade" + ipad: "iPad" + iphone: "iPhone" + ipod: "iPod" + mobile: "Mobiilne seade" + mac: "Mac" + linux: "GNU/Linuxiga arvuti" + windows: "Windowsi arvuti" + unknown: "tundmatu seade" + os: + android: "Android" + ios: "iOS" + macos: "macOS" + linux: "Linux" + windows: "Microsoft Windows" + unknown: "tundmatu operatsioonisüsteem" change_email: confirmed: "Sinu meiliaadress on uuendatud." please_continue: "Edasi saidile %{site_name}" @@ -449,7 +482,6 @@ et: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Ebasünnis' - description: 'Selle postituse sisu on iga mõistliku inimese hinnangul solvav, ahistav või rikub meie kommuuni reegleid.' long_form: 'tähistasin selle kui sobimatu' notify_user: title: 'Saada kasutajale @{{username}} sõnum' @@ -465,29 +497,33 @@ et: bookmark: title: 'Järjehoidja' description: 'Lisa sellele postitusele järjehoidja' + short_description: 'Lisa sellele postitusele järjehoidja' long_form: 'lisasin sellele postitusele järjehoidja' like: title: 'Laigi' description: 'Laigi seda postitust' long_form: 'laikis seda' user_activity: + no_default: + others: "Tegevusi pole." no_bookmarks: others: "Järjehoidjad puuduvad." no_likes_given: self: "Sa pole oma meeldimist ühelegi postitusele andnud." others: "Meeldimisega postitusi veel pole." + no_replies: + others: "Vastuseid pole." topic_flag_types: spam: title: 'Spämm' description: 'See teema on reklaam. See ei ole ei kasulik ega teemakohane.' long_form: 'tähistasin selle kui spämmi' + short_description: 'See on reklaam' inappropriate: title: 'Ebasobiv' - description: 'Selle teema sisu on iga mõistliku inimese hinnangu järgi solvav, ahistav või rikub meie kommuuni reegleid.' long_form: 'tähistasin selle kui sobimatu' notify_moderators: title: "Miski muu" - description: 'See teema vajab saidi meeskonna tähelepanu tulenevalt juhisest, kasutustingimustest või mõnest muust põhjusest, mida pole siin mainitud.' long_form: 'tähistasin selle kui moderaatori tähelepanu vajava' email_title: 'Teema "%{title}" nõuab moderaatori tähelepanu' email_body: "%{link}\n\n%{message}" @@ -507,13 +543,49 @@ et: authorize: "Autoriseeri" read: "lugemine" read_write: "lugemine ja kirjutamine" + scopes: + read: "Loe kõiki" + write: "Kirjuta kõikidesse" reports: + default: + labels: + count: Arv + percent: Protsent + day: Päev + post_edits: + title: "Postituse muutmised" + labels: + post: Postitus + author: Autor + edit_reason: Põhjus + moderators_activity: + title: "Modereerimise tegevus" + labels: + moderator: Moderaator + time_read: Lugemise aeg + topic_count: Teemade loomise aeg + post_count: Postitusi loodud + flags_status: + values: + agreed: Nõus + disagreed: Polnud nõus + no_action: Tegevust pole + labels: + flag: Liik + assigned: Määratud + poster: Postitaja visits: title: "Kasutajakülastused" xaxis: "Päev" yaxis: "Külastajate arv" signups: xaxis: "Päev" + new_contributors: + xaxis: "Päev" + dau_by_mau: + xaxis: "Päev" + daily_engaged_users: + xaxis: "Päev" profile_views: title: "Kasutaja profiili vaatamisi" xaxis: "Päev" @@ -543,6 +615,20 @@ et: users_by_trust_level: xaxis: "Usaldustase" yaxis: "Kasutajate arv" + labels: + level: Tase + users_by_type: + xaxis: "Liik" + yaxis: "Kasutajate arv" + labels: + type: Liik + xaxis_labels: + admin: Admin + moderator: Moderaator + silenced: Vaigistatud + trending_search: + labels: + searches: Otsingud emails: title: "Saadetud kirjad" xaxis: "Päev" @@ -550,6 +636,9 @@ et: user_to_user_private_messages: xaxis: "Päev" yaxis: "Sõnumite arv" + user_to_user_private_messages_with_replies: + xaxis: "Päev" + yaxis: "Sõnumite arv" system_private_messages: title: "Süsteem" xaxis: "Päev" @@ -577,8 +666,15 @@ et: num_clicks: "Klikid" num_topics: "Teemad" num_users: "Kasutajad" + labels: + domain: Domeen + num_clicks: Klikke + num_topics: Teemasid top_referred_topics: title: "Parimad viidatud teemad" + labels: + num_clicks: "Klikke" + topic: "Teema" page_view_anon_reqs: title: "Anonüümne" xaxis: "Päev" @@ -619,6 +715,9 @@ et: mobile_visits: xaxis: "Päev" yaxis: "Külastuste arv" + web_crawlers: + labels: + page_views: "Lehekülje vaatamisi" dashboard: host_names_warning: "Sinu config/database.yml fail kasutab vaikimisi hostinime localhost. Kirjuta sinna oma saidi hostinimi." site_settings: @@ -714,8 +813,11 @@ et: pasted_image_filename: "Asetatud pilt" color_schemes: dark: "Tume skeem" + default_theme_name: "Hele" light_theme_name: "Hele" dark_theme_name: "Tume" + neutral_theme_name: "Neutraalne" + summer_theme_name: "Suvi" about: "Teave" guidelines: "Juhendid" privacy: "Privaatsus" @@ -952,3 +1054,6 @@ et: title: "Kutsu meeskonda" finished: title: "Sinu Discourse on valmis!" + staff_action_logs: + not_found: "ei leitud" + unknown: "tundmatu" diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 413ea5d2a2..6f7c40eb4f 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -533,8 +533,6 @@ fa_IR: email_body: "%{link}\n\n%{message}" inappropriate: title: 'نامناسب' - description: 'محتوای این موضوع شامل مواردی میشود که یک شخص می تونه آن را توهین آمیز، آزار دهنده، یا نقض کننده قوانین باشد راهنمای انجمن.' - short_description: 'نقض راهنمای انجمن' long_form: 'به عنوان نامناسب علامت گذاری شده' notify_user: title: 'فرستادن پیام به @{{username}}' @@ -574,11 +572,9 @@ fa_IR: long_form: 'پرچم‌گذاری به عنوان یک هرزنامه' inappropriate: title: 'نامناسب' - description: 'محتوای این موضوع شامل مواردی میشود که یک شخص می‌تواند آن را توهین آمیز، آزار دهنده، یا نقض قوانین انجمن باشد. راهنمای انجمن.' long_form: 'پرچم گذاری شده به عنوان نامناسب' notify_moderators: title: "یک چیز دیگر" - description: 'این موضوع نیاز به توجه همکاران بر اساس guidelines, شرایط استفاده از خدمات، دارد یا یک دلیل دیگر که در بالا ذکر نشده.' long_form: 'این را برای توجه مدیر پرچم گذاری کن' email_title: 'موضوع "%{title}" نیاز به توجه مدیر دارد. ' email_body: "%{link}\n\n%{message}" @@ -761,11 +757,6 @@ fa_IR: sidekiq_warning: 'Sidekiq کار نمی کند. کار‌های زیادی مثل ارسال ایمیل ها، برای اجرا نیازمند sidekiq هستند. لطفا مطمئن شوید که حداقل یک پروسه sidekiq کار می‌کند. درباره Sidekiq بیشتر بدانید.' queue_size_warning: 'تعداد کار های زمانبندی شده %{queue_size}، که مقدار بالاییست. این مسئله می‌تواند باعث بروز مشکل در پردازش(های) Sidekiq شود، یا می‌توانید عامل‌های Sidekiq بیشتری اضافه کنید.' memory_warning: 'سرور شما با کمتر از 1 گیگ رم راه اندازی شده است، برای دیسکورس حداقل 1 گیگ رم لازم است تا به درستی کار کند.' - google_oauth2_config_warning: 'سرور تنظیم شده تا اجازه دهد برای ثبت نام و وارد شدن با Google OAuth2 (enable_google_oauth2_logins), ولی clientid و client secret تنظیم نشده است. به تنظیمات سایت بروید و تنظیمات را به‌روز کنید. اطلاعات بیشتر.' - facebook_config_warning: 'سرور تنظیم شده تا اجازه ثبت نام و وارد شدن یا فیس بوک را بدهد. (enable_facebook_logins)، ولی app id و app secret تنظیم نشده است. به بروید و و تنظیمات سایت را تغییر دهید. اطلاعات بیشتر.' - twitter_config_warning: 'سرور تنظیم شده تا اجازه ثبت نام و وارد شدن با توییتر را بدهد (enable_twitter_logins), مقدار key و secret تنظیم نشده است. به تنظیمات سایت رفته و مقادیر را بروز کنید. اطلاعات بیشتر.' - github_config_warning: 'سرور تنظیم شده تا اجازه ثبت نام و وارد شدن با GitHub را بدهد (enable_github_logins), ولی مقادیر client id و secret وارد نشده اند. به تنظیمات سایت رفته و مقادیر را تنظیم کنید. توضیحات بیشتر.' - failing_emails_warning: '%{num_failed_jobs} ایمیل زمانبندی شده نا‌موفق وجود دارد. فایل app.yml را بررسی کنید و مطمئن شوید که تنظیمات سرور ایمیل درست است. نمایش زمابندی‌های ناموفق Sidekiq .' subfolder_ends_in_slash: "تنظیمات زیرپوشه نادرست است، مقدار DISCOURSE_RELATIVE_URL_ROOT با نویسه‌ی slash تمام می‌شود." email_polling_errored_recently: one: "رای‌گیری ایمیلی %{count} خطا در 24 ساعت گذشته ایجاد کرده. گزارشات را ببینید." @@ -885,14 +876,12 @@ fa_IR: password_unique_characters: "حداقل تعداد نویسه یکتا برای یک رمز‌عبور" block_common_passwords: "رمز عبوری که جزء 10,000 رمز عبور رایج است را قبول نکنید. " enable_sso: "اجازه single sign on از طریق سایت‌های دیگر داده شود (هشدار: آدرس ایمیل کاربر *باید* توسط سایت دیگر معتبر تشخیص داده شود!)" - enable_sso_provider: "پیاده سازی پروتکل ارائه دهنده SSO Discourse در /session/sso_provider، به تنظیم sso_secret نیاز دارد" sso_url: "لینک ورود یکپارچه (باید شامل http:// یا https://) باشد" sso_secret: "مطمئن شوید که secret string استفاده شده در تصدیق رمزنویسی اطلاعات SSO ٬ 10 نویسه یا بیشتر است." sso_overrides_bio: "بیوگرافی کاربر در پروفایل را از بین می‌برد و به کاربر اجازه تغییر نمی‌دهد." sso_overrides_email: "آدرس ایمیل دریافت شده از طریق SSO را در هر بار ورود جایگزین آدرس ایمیل محلی کاربر می‌کند و از تغییر محلی آن نیز جلوگیری می‌نماید. (هشدار: این کار ممکن است به خاطر تغییر در ایمیل‌های محلی باعث بروز اختلاف و ناهماهنگی شود)" sso_overrides_username: "نام کاربری دریافت شده از طریق SSO را در هر بار ورود جایگزین نام کاربری محلی کاربر کرده و از تغییر محلی آن نیز جلوگیری می‌نماید. (هشدار: این کار ممکن است به خاطر تغییر در طول یا سایر شرایط نام کاربری باعث بروز خطا و ناهماهنگی شود)" sso_overrides_name: "نام کامل دریافت شده از طریق SSO را در هر بار ورود جایگزین نام کامل محلی کاربر می‌کند و از تغییر محلی آن نیز جلوگیری می‌نماید." - sso_overrides_avatar: "لغو آواتار کاربر با آواتار سایت خارجی از محموله SSO. اگر فعال باشد، غیر فعال کردن allow_uploaded_avatars به شدت توصیه می شود" sso_not_approved_url: "حساب‌های تأیید نشده SSO را به این URL بفرست" sso_allows_all_return_paths: "از دامنه برای return_paths که توسط SSO فراهم شده جلوگیری نکن (به صورت پیشفرض مسیر برگشتی باید در همین سایت باشد)" allow_new_registrations: "به کاربر جدید اجازه ثبت‌نام بده. لغو این انتخاب برای جلوگیری از ساخت حساب‌کاربری جدید. " @@ -907,7 +896,6 @@ fa_IR: allow_restore: "اجازه بازیابی بده، که می‌تواند تمام اطلاعات سایت را جایگزین کند! نادرست را رها کن مگر اینکه قصد بازیابی نسخه پشتیبان را داری " maximum_backups: "حداکثر تعداد پشتیبان برای نگه داری بر روی دیسک. نسخه های پشتیبان قدیمی بطور خودکار پاک شده است" automatic_backups_enabled: "فعالسازی پشتیبان‌گیری خودکار در بازه زمانی مشخص" - enable_s3_backups: "نسخه پشتیبان را وقتی کامل شد به S3 بارگزاری کن. مهم: نیازمند اطلاعات ورود صحیح به S3 در تنظیمات فایل می‌باشد." s3_backup_bucket: "میزبان راه‌دور برای نگهداری از نسخه‌های پشتیبان. اخطار: مطمئن شوید که میزبان خصوصی است. " s3_disable_cleanup: "غیر‌فعال‌سازی حذف نسخه پشتیبان از S3 وقتی در هاست حذف می‌شود." backup_time_of_day: "زمان UTC روز برای رخ دادن نسخه پشتیبان." @@ -1944,7 +1932,6 @@ fa_IR: داشتن حساب کاربری الزامی است. لطفا یک حساب کاربری بسازید یا وارد شوید. terms_of_service: title: "شرایط استفاده از خدمات" - signup_form_message: 'من شرایط استفاده از خدمات را خواندم و قبول می‌کنم.' deleted: 'حذف شد' image: "تصویر" upload: @@ -2293,6 +2280,3 @@ fa_IR: title: "دعوت همکاران" finished: title: "Discourse شما آماده‌ست!" - description: | -

    برای تغییر تنظیمات وارد بخش مدیریت; کنار آیکن آچار شوید.

    -

    برای ساخت انجمن خود! موفق باشید!

    diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index e3c171177c..d476f2e949 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -246,7 +246,7 @@ fi: rss_description: latest: "Tuoreimmat viestiketjut" hot: "Kuumat ketjut" - top: "Huippuketjut" + top: "Kuumat ketjut" top_all: "Kaikkien aikojen parhaat ketjut" top_yearly: "Vuoden parhaat ketjut" top_quarterly: "Vuosineljänneksen parhaat ketjut" @@ -641,7 +641,6 @@ fi: ipod: "iPod" mobile: "mobiililaite" mac: "Mac" - linux: "Linux-tietokone" windows: "Windows-tietokone" unknown: "tuntematon laite" os: @@ -692,8 +691,6 @@ fi: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Sopimaton' - description: 'Viestin sisältö on loukkaava, herjaava tai ristiriidassa palstan sääntöjen kanssa.' - short_description: 'Palstan sääntöjen vastainen' long_form: 'liputti tämän sopimattomaksi' notify_user: title: 'Lähetä käyttäjälle @{{username}} viesti.' @@ -742,12 +739,9 @@ fi: short_description: 'Tämä on mainos' inappropriate: title: 'Sopimaton' - description: 'Ketjussa on loukkaavaa, herjaavaa tai palstan sääntöjen kanssa ristiriitaista sisältöä.' long_form: 'liputti tämän sopimattomaksi' - short_description: 'Palstan sääntöjen vastainen' notify_moderators: title: "Jotain muuta" - description: 'Valvojan tulee huomioida tämä ketju palstan sääntöjen, palveluehtojen tai jonkun muun syyn vuoksi.' long_form: 'liputit tämän valvojille tiedoksi' short_description: 'Henkilökunnan tulisi huomioida muusta syystä' email_title: 'Ketju"%{title}" kaipaa valvojan huomiota' @@ -1029,11 +1023,6 @@ fi: sidekiq_warning: 'Sidekiq ei ole käynnissä. Monet tehtävät, kuten sähköpostien lähettäminen, suoritetaan asynkronisesti sidekiqin avulla. Varmista, että vähintään yksi sidekiq prosessi on käynnissä. Opiskele lisää Sidekiqista täältä.' queue_size_warning: 'Jonossa olevien tehtävien määrä on %{queue_size}, joka on korkea. Tämä voi olla merkki ongelmista Sidekiq prosess(e)issa tai sinun voi täytyä lisätä Sidekiq workerien määrää.' memory_warning: 'Palvelimella on alle 1GB muistia. Vähintään 1 GB muistia on suositeltavaa.' - google_oauth2_config_warning: 'Palvelin on konfiguroitu hyväksymään liittyminen Google OAuth2:n kautta (enable_google_oauth2_logins), mutta client id ja client secret arvoja ei ole asetettu. Päivitä arvot sivuston asetuksissa.Voit lukea lisätietoja tästä oppaasta.' - facebook_config_warning: 'Palvelin on konfiguroitu hyväksymään liittyminen Facebookin kautta (enable_facebook_logins), mutta app id ja app secret arvoja ei ole asetettu. Päivitä arvot sivuston asetuksissa.Voit lukea lisätietoja tästä oppaasta.' - twitter_config_warning: 'Palvelin on konfiguroitu hyväksymään liittyminen Twitterin kautta (enable_twitter_logins), mutta salaisia arvoja ei ole asetettu. Päivitä arvot sivuston asetuksissa. Voit lukea lisätietoja tästä oppaasta.' - github_config_warning: 'Palvelin on konfiguroitu hyväksymään liittyminen Githubin kautta (enable_github_logins), mutta client id ja app secret arvoja ei ole asetettu. Päivitä arvot sivuston asetuksissa.Voit lukea lisätietoja tästä oppaasta.' - failing_emails_warning: 'Epäonnistuneiden sähköpostitehtävien määrä on %{num_failed_jobs}. Tarkista app.yml ja varmista, että palvelimen asetukset ovat kunnossa. Katsele epäonnistuneita tehtäviä Sidekiqissa.' subfolder_ends_in_slash: "Alihakemiston asetuksesi ei kelpaa; DISCOURSE_RELATIVE_URL_ROOT päättyy vinoviivaan." email_polling_errored_recently: one: "Sähköpostin pollaus on aiheuttanut virheen edellisen 24 tunnin aikana. Tarkastele lokeja saadaksesi lisätietoja." @@ -1161,10 +1150,10 @@ fi: suppress_reply_directly_above: "Älä näytä vastauksena-painiketta viestin yläreunassa, jos viestissä on vastattu vain edelliseen viestiin." suppress_reply_when_quoting: "Älä näytä vastauksena-painiketta viestin yläreunassa, kun viestissä on lainaus." max_reply_history: "Maksimimäärä vastauksia, jotka avataan klikattaessa 'vastauksena' painiketta" - topics_per_period_in_top_summary: "Ketjujen lukumäärä, joka näytetään oletuksena Huiput-listauksissa." - topics_per_period_in_top_page: "Ketjujen lukumäärä, joka näytetään laajennetussa Huiput-listauksessa." - redirect_users_to_top_page: "Ohjaa uudet ja kauan poissa olleet käyttäjät automaattisesti huiput-sivulle." - top_page_default_timeframe: "Huiput-sivun oletusaikajakso." + topics_per_period_in_top_summary: "Ketjujen lukumäärä, joka näytetään oletuksena Kuumat-listauksissa." + topics_per_period_in_top_page: "Ketjujen lukumäärä, joka näytetään laajennetussa Kuumat-listauksessa." + redirect_users_to_top_page: "Ohjaa uudet ja kauan poissa olleet käyttäjät automaattisesti kuumat-sivulle." + top_page_default_timeframe: "Kuumat-sivun oletusaikajakso." show_email_on_profile: "Näytä käyttäjän sähköpostiosoite profiilissa (näkyy vain käyttäjälle itselleen ja henkilökunnalle)" prioritize_username_in_ux: "Näytä käyttäjänimi ensimmäisenä käyttäjäsivulla, -kortissa ja viesteissä (jos poistetaan käytöstä, nimi näytetään ensin)" enable_rich_text_paste: "Ota käyttöön automaattinen muunnos HTML:stä Markdowniin, kun tekstiä liitetään kirjoitusalueelle (kokeellinen)" @@ -1188,14 +1177,12 @@ fi: password_unique_characters: "Vähimmäismäärä eri merkkejä salasanassa." block_common_passwords: "Älä salli salasanoja, jotka ovat 10 000 yleisimmän salasanan joukossa." enable_sso: "Ota käyttöön single sign on ulkopuolisen sivuston kautta (VAROITUS: ULKOPUOLISEN SIVUSTON TÄYTYY VALIDOIDA SÄHKÖPOSTIOSOITTEET!)" - enable_sso_provider: "Ota käyttöön Discourse SSO provider -protokolla /session/sso_provider päätepisteessä, vaatii asetuksen sso_secret asettamista." sso_url: "Kertakirjautumisen päätepisteen URL (täytyy sisältää http:// tai https://)" sso_secret: "Salausavain, jolla todennetaan SSO tiedot, varmista että se on vähintään 10 merkkiä pitkä" sso_overrides_bio: "Syrjäyttää käyttäjän kuvauksen itsestään käyttäjäprofiilissa ja estää sen muokkaamisen" sso_overrides_email: "Ohittaa paikallisen sähköpostiosoitteen SSO:n kautta saatavalla ulkopuolisella osoitteella ja estää paikalliset muutokset (VAROITUS: eroavuuksia saattaa syntyä johtuen paikallisten sähköpostiosoitteiden normalisoinnista)" sso_overrides_username: "Ohittaa paikallisen käyttäjänimen SSO:n kautta saatavalla ulkopuolisella nimellä ja estää paikalliset muutokset (VAROITUS: eroavuuksia saattaa syntyä johtuen erilaisista vaatimuksista ja pituudesta)" sso_overrides_name: "Ohittaa paikallisen koko nimen SSO:n kautta saatavalla ulkopuolisella nimellä jokaisella kirjautumiskerralla ja estää paikalliset muutokset" - sso_overrides_avatar: "Syrjäyttää käyttäjän avatarin ulkopuolisella kertakirjautumisen kautta haetulla avatarilla. Jos tämä otetaan käyttöön, suositellaan samalla allow_uploaded_avatars -asetuksen poistamista käytöstä." sso_not_approved_url: "Uudelleenohjaa hyväksymättömät SSO-tilit tähän osoitteeseen" sso_allows_all_return_paths: "Älä rajoita SSO:n antamien palautuspolkujen verkkotunnusta (oletuksena palautuspolun on oltava nykyisellä sivustolla)" enable_local_logins: "Salli kirjautuminen paikallisesti käyttäjänimen ja salasanan avulla. Tämä tulee olla päällä, jotta kutsuminen voi toimia. VAROITUS: jos ei ole käytössä, voi sinun olla mahdotonta kirjautua sisään, jollet ole aiemmin määritellyt ainakin yhtä muuta kirjautumistapaa." @@ -1214,7 +1201,6 @@ fi: maximum_backups: "Tallennettuna pidettävien varmuuskopioiden maksimimäärä. Vanhemmat varmuuskopiot poistetaan automaattisesti" automatic_backups_enabled: "Tee automaattinen varmuuskopiointi, kuten tiheysasetus on määritelty" backup_frequency: "Kuinka monen päivän välein otetaan varmuuskopio." - enable_s3_backups: "Lataa varmuuskopiot S3:een niiden valmistuttua. TÄRKEÄÄ: edellyttää, että toimivat S3 kirjautumistiedot on syötetty asetuksiin." s3_backup_bucket: "Amazon S3 bucket johon varmuuskopiot ladataan. VAROITUS: Varmista, että se on yksityinen." s3_endpoint: "Varmuuskopioinnin päätepisteeksi voi asettaa minkä tahansa S3-yhetensopivan palvelun kuten DigitalOcean Spaces tai Minio. VAROITUS: Valitse oletus jos käytät AWS S3:a" s3_disable_cleanup: "Älä poista varmuuskopiota S3:sta, kun se poistetaan paikallisesti." @@ -1223,9 +1209,9 @@ fi: active_user_rate_limit_secs: "Kuinka usein 'last_seen_at' kenttä päivitetään, sekunneissa" verbose_localization: "Näytä laajennetut lokalisointitiedot käyttöliittymässä" previous_visit_timeout_hours: "Kuinka kauan vierailun on täytynyt kestää, jotta se lasketaan 'edelliseksi' vierailuksi, tunneissa" - top_topics_formula_log_views_multiplier: "katselukertojen logaritmin kerroin (n) Huiput-listauksen kaavassa: `log(katselut) * (n) + avausviestin tykkäykset * 0.5 + PIENEMPI(tykkäysten määrä / viestien määrä, 3) + 10 + log(viestien määrä)`" - top_topics_formula_first_post_likes_multiplier: "avausviestin tykkäysmäärän kerroin (n) Huiput-listauksen kaavassa: `log(katselut) * 2 + avausviestin tykkäykset * (n) + PIENEMPI(tykkäysten määrä / viestien määrä, 3) + 10 + log(viestien määrä)`" - top_topics_formula_least_likes_per_post_multiplier: "tykkäykset/viestit -suhteen enimmäisarvo (n) Huiput-listauksen kaavassa: `log(katselut) * 2 + avausviestin tykkäykset * 0.5 + PIENEMPI(tykkäysten määrä / viestien määrä, (n)) + 10 + log(viestien määrä)`" + top_topics_formula_log_views_multiplier: "katselukertojen logaritmin kerroin (n) Kuumat-listauksen kaavassa: `log(katselut) * (n) + avausviestin tykkäykset * 0.5 + PIENEMPI(tykkäysten määrä / viestien määrä, 3) + 10 + log(viestien määrä)`" + top_topics_formula_first_post_likes_multiplier: "avausviestin tykkäysmäärän kerroin (n) Kuumat-listauksen kaavassa: `log(katselut) * 2 + avausviestin tykkäykset * (n) + PIENEMPI(tykkäysten määrä / viestien määrä, 3) + 10 + log(viestien määrä)`" + top_topics_formula_least_likes_per_post_multiplier: "tykkäykset/viestit -suhteen enimmäisarvo (n) Kuumat-listauksen kaavassa: `log(katselut) * 2 + avausviestin tykkäykset * 0.5 + PIENEMPI(tykkäysten määrä / viestien määrä, (n)) + 10 + log(viestien määrä)`" rebake_old_posts_count: "Kuinka monta vanhaa viestiä rakennetaan uudelleen 15 minuutissa." enable_safe_mode: "Salli käyttäjän mennä vikasietotilaan, jotta hän voi rajata pois ongelmanaiheuttajista lisäosat." rate_limit_create_topic: "Ketjun aloittamisen jälkeen käyttäjän täytyy odottaa (n) sekuntia voidakseen aloittaa toisen ketjun." @@ -2271,14 +2257,6 @@ fi: spam_post_blocked: title: "Roskapostiviesti estettiin" subject_template: "Uuden käyttäjän %{username} viestit estettiin toistuvien linkkien vuoksi" - text_body_template: | - Tämä on automaattinen viesti. - - Uusi käyttäjä [%{username}](%{user_url}) yritti lähettää useita viestejä, joissa oli linkkejä kohteeseen %{domains}, mutta viestit estettiin roskapostin ehkäisemiseksi. - - [Arvioi käyttäjä]%{user_url}. - - Tätä voi muuttaa sivustoasetuksilla `newuser_spam_host_threshold` ja `white_listed_spam_host_domains`. unsilenced: title: "Hiljennys kumottu" subject_template: "Käyttäjätili ei ole enää jäädytetty" @@ -2711,7 +2689,6 @@ fi: Käyttäjätili tarvitaan. Luo tili tai kirjaudu sisään. terms_of_service: title: "Käyttöehdot" - signup_form_message: 'Olen lukenut ja ymmärtänyt Käyttöehdot.' deleted: 'poistettu' image: "kuva" upload: @@ -3350,7 +3327,7 @@ fi: categories_and_latest_topics: label: "Alueet ja tuoreimmat ketjut" categories_and_top_topics: - label: "Alueet ja huiput ketjut" + label: "Alueet ja kuumat ketjut" emoji: title: "Emojit" description: "Minkä emojityylin haluat yhteisöösi? Voit milloin vain lisätä mukautettuja emojeita navigoimalla Ylläpito --> Mukauta --> Emoji." @@ -3359,9 +3336,6 @@ fi: description: "Melkein valmista! Kutsutaan ihmisiä pohjustamaan keskusteluita kiintoisilla ketjunavauksilla ja vastauksilla, jotta yhteisö pääsee alkuun." finished: title: "Discoursesi on valmis!" - description: | -

    Jos milloin tahansa haluat muuttaa asetuksia, käy ylläpito-osiossa; löydät sen jakoavainkuvakkeen vierestä sivuston valikosta.

    -

    Onnea ja menestystä uuden yhteisön rakentamiseen!

    search_logs: graph_title: "Hakujen määrä" joined: "Liittyi" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index b2e680c52d..397a13b652 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -644,8 +644,6 @@ fr: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Inapproprié' - description: 'Ce message contient du contenu qu''une personne raisonnable jugerait offensant, abusif ou en violation de la charte de notre communauté.' - short_description: 'Une violation de la charte de notre communauté' long_form: 'signalé comme inapproprié' notify_user: title: 'Envoyer un message à @{{username}} ' @@ -694,12 +692,9 @@ fr: short_description: 'Ceci est une publicité' inappropriate: title: 'Inapproprié' - description: 'Ce message contient du contenu qu''une personne raisonnable jugerait offensant, abusif ou en violation de la charte de notre communauté.' long_form: 'signalé comme inapproprié' - short_description: 'Une transgression de notre charte communautaire' notify_moderators: title: "Autre chose" - description: 'Ce sujet demande l''attention des responsables d''après la charte de la communauté, les conditions générales de service ou pour une autre raison.' long_form: 'signalé pour modération' short_description: 'Nécessite l''attention du staff pour une autre raison' email_title: 'Le sujet « %{title} » nécessite l''attention d''un modérateur' @@ -978,11 +973,6 @@ fr: sidekiq_warning: 'Sidekiq n''est pas lancé. De nombreuses tâches, comme l''envoi des courriels, sont exécutées de manière asynchrone par sidekiq. Assurez-vous d''avoir au moins un processus sidekiq de lancé. En savoir plus sur sidekiq.' queue_size_warning: 'Le nombre de jobs dans la file d''attente est de %{queue_size}, ce qui est assez élevé. Cela peut indiquer un problème avec le(s) process Sidekiq, ou la nécessité d''ajouter davantage de workers.' memory_warning: 'Votre serveur dispose de moins de 1 Go de mémoire vive. Au moins 1 Go de RAM est recommandé.' - google_oauth2_config_warning: 'Le serveur est configuré pour permettre l''authentification via Google Oauth2 (enable_google_oauth2_logins), mais les paramètres client id et client secret ne sont pas renseignés. Allez dans les Paramètres du Site et mettez les à jour. Voir le guide pour en savoir plus.' - facebook_config_warning: 'Le serveur est configuré pour permettre l''authentification par Facebook (enable_facebook_logins), mais les paramètres facebook_app_id et facebook_app_secret ne sont pas renseignés. Allez dans les Paramètres et mettez les à jour. Voir le guide pour en savoir plus.' - twitter_config_warning: 'Le serveur est configuré pour permettre l''authentification par Twitter (enable_twitter_logins), mais les paramètres key et secret ne sont pas renseignés. Allez dans les Paramétres et mettez les à jour. Voir le guide pour en savoir plus.' - github_config_warning: 'Le serveur est configuré pour permettre l''authentification par GitHub (enable_github_logins), mais les paramètres github_client_id et github_client_secret ne sont pas renseignés. Allez dans les Paramètres et mettez les à jour. Voir le guide pour en savoir plus.' - failing_emails_warning: 'Il y a %{num_failed_jobs} tâches d''envois de courriel en erreur. Vérifiez votre fichier app.yml et assurez-vous de la conformité des paramètres du serveur de courriel. Voir aussi les processus en échec dans Sidekiq.' subfolder_ends_in_slash: "Votre configuration de sous-répertoire est erronée; DISCOURSE_RELATIVE_URL_ROOT se termine avec une barre oblique ." email_polling_errored_recently: one: "La vérification des courriels a généré une erreur au cours des 24 dernières heures. Vérifiez le journal pour plus de détails." @@ -1136,7 +1126,6 @@ fr: password_unique_characters: "Nombre minimum de caractères uniques qu'un mot de passe doit avoir." block_common_passwords: "Ne pas autoriser les mots de passe qui font partie des 10 000 les plus utilisés." enable_sso: "Activer l'authentification unique via un site externe (ATTENTION : LES ADRESSES COURRIEL *DOIVENT* ÊTRE VALIDÉES PAR LE SITE EXTERNE !)" - enable_sso_provider: "Implémenter le procotole Discourse de provider SSO à /session/sso_provider, requiert sso_secret" sso_url: "URL de l'authentification unique SSO (doit inclure http:// ou http://)" sso_secret: "Chaîne de caractères secrète utilisée pour authentifier les informations SSO par cryptographie, assurez-vous qu'elle est de 10 caractères ou plus" sso_overrides_bio: "Écrase la biographie du profil utilisateur et empêche l'utilisateur de le modifier" @@ -1144,7 +1133,6 @@ fr: sso_overrides_email: "Surcharger les emails locaux avec les emails externes d'un SSO à chaque connexion, et prévenir les modifications locales. (ATTENTION : Des écarts peuvent se produire dus aux règles locales sur les emails)" sso_overrides_username: "Surcharger les pseudos locaux avec les pseudos externes d'un SSO à chaque connexion, et prévenir les modifications locales. (ATTENTION : des écarts peuvent se produire dûes aux différences de longueur et d'exigences sur les pseudos)" sso_overrides_name: "Surcharger les noms complets locaux avec les noms complets externes d'un SSO à chaque connexion, et prévenir les modifications locales." - sso_overrides_avatar: "Surcharge les avatars des utilisateurs avec les avatars d'un SSO. Si activé, il est fortement recommandé de désactiver allow_uploaded_avatars." sso_overrides_profile_background: "Remplacer arrière-plan du profil utilisateur avec un avatar d'un site externe à travers SSO payload." sso_overrides_card_background: "Remplacer arrière-plan de la carte de l'utilisateur avec un avatar d'un site externe à travers SSO payload." sso_not_approved_url: "Rediriger les comptes SSO non validés vers cette URL" @@ -1165,7 +1153,6 @@ fr: maximum_backups: "Nombre maximum de sauvegardes à conserver sur le disque. Les anciennes sauvegardes seront automatiquement supprimées" automatic_backups_enabled: "Activer les sauvegardes automatiques tels que définis dans les fréquences de sauvegarde" backup_frequency: "Nombre de jours entre sauvegardes" - enable_s3_backups: "Envoyer vos sauvegardes à S3 lorsqu'elles sont terminées. IMPORTANT : Vous devez avoir renseigné vos identifiants S3 dans les paramètres de fichiers." s3_backup_bucket: "Bucket distant qui contiendra les sauvegardes. ATTENTION: Vérifiez que c'est un bucket privé" s3_endpoint: "Le terminal peut être modifié pour être sauvegardé sur un service compatible S3 comme DigitalOcean Spaces ou Minio. ATTENTION: Utiliser défaut si vous utilisez AWS S3" s3_force_path_style: "Appliquez l'adressage de type chemin pour votre terminal personnalisé. IMPORTANT : Nécessaire pour utiliser les téléchargements et sauvegardes Minio." @@ -2227,14 +2214,6 @@ fr: spam_post_blocked: title: "Message spam bloqué" subject_template: "Les messages du nouvel utilisateur %{username} sont bloqués pour des liens répétés" - text_body_template: | - Ceci est un message automatique. - - Le nouvel utilisateur [%{username}](%{user_url}) tente de créer de multiples messages avec des liens vers %{domains}, mais que ces messages ont été bloqués pour éviter le spam. Cet utilisateur est toujours capable de créer des messages qui ne contiennent pas de liens vers %{domains}. - - Merci de [review the user](%{user_url}). - - Ceci peut être modifier via les options `newuser_spam_host_threshold` et white_listed_spam_host_domains`. unsilenced: title: "Mise sous silence annulée" subject_template: "Compte débloqué" @@ -2668,7 +2647,6 @@ fr: Un compte est nécessaire. Veuillez en créer un ou connectez-vous pour continuer. terms_of_service: title: "Conditions générales d'utilisation" - signup_form_message: 'J''ai lu et j''accepte les conditions générales d''utilisation.' deleted: 'supprimé' image: "image" upload: @@ -3311,9 +3289,6 @@ fr: description: "Vous avez presque terminé ! Invitons quelques personnes pour commencer des discussions avec des sujets et réponses intéressantes pour faire démarrer votre communauté." finished: title: "Votre Discourse est prêt !" - description: | -

    Si vous souhaitez modifier ces paramètres, rendez-vous dans la section Administration ; trouvez la dans le menu du site.

    -

    Amusez-vous et bonne chance pour construire votre nouvelle communauté !

    search_logs: graph_title: "Nombre de recherches" joined: "Inscrit" diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 1714c45680..f0e0631069 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -613,8 +613,6 @@ he: email_body: "%{link}\n\n%{message}" inappropriate: title: 'לא ראוי' - description: 'פוסט זה מכיל תוכן שאדם סביר היה רואה כפוגעני, מתעלל או הפרה של כללי הקהילה.' - short_description: 'הפרה של כללי הקהילה שלנו' long_form: 'דוגלל כלא ראוי' notify_user: title: 'שלחו הודעה ל @{{username}}' @@ -656,11 +654,9 @@ he: long_form: 'דיגלתם זאת כספאם' inappropriate: title: 'לא ראוי' - description: 'נושא זה מכיל תוכן שהאדם הסביר היה מחשיב פוגעני, מתעלל או הפרה של כללי ההתנהלות בקהילה שלנו.' long_form: 'דוגלל כלא ראוי' notify_moderators: title: "משהו אחר" - description: 'נושא זה דורש תשומת לב של הצוות בהתאם להנחיות הקהילה, תנאי השירות, או מסיבה אחרת שאינה רשומה למעלה.' long_form: 'זה סומן לתשומת הלב של מנחה' email_title: 'הנושא "%{title}" דורש תשומת לב של מנחה' email_body: "%{link}\n\n%{message}" @@ -842,11 +838,6 @@ he: sidekiq_warning: 'Sidekiq לא רץ. משימות רבות, כמו שליחת מיילים, מבוצעות אסינכרונית באמצעות Sidekiq. אנא וודאו שלפחות תהליך אחד של Sidekiq רץ. לימדו על Sidekiq כאן.' queue_size_warning: 'מספר העבודות בתור הוא %{queue_size}, שהוא גבוה. זה עלול להצביע על בעיה עם תהליך(י) Sidekiq, או שייתכן שאתם צריכים יותר Sidekiq workers.' memory_warning: 'בשרת שלכם יש פחות מ 1 גיגה זיכרון בסך הכל. מומלץ לפחות 1 גיגה זיכרון.' - google_oauth2_config_warning: 'השרת מכוון לאפשר הרשמות והתחברות עם OAuth2 של גוגל (enable_google_oauth2_logins), אבל ערכי זהות הלקוח (client id) וסיסמת הלקוח (client secret) אינם מוגדרים. לכו ל הגדרות האתר ועדכנו את הגדרות האתר. ראו מדריך זה כדי ללמוד עוד.' - facebook_config_warning: 'השרת מכוון לאפשר הרשמה והתחברות עם פייסבוק (enable_facebook_logins), אבל ערכי מזהה האפליקציה וסוד האפליקציה אינם קבועים. לכו ל הגדרות האתר ועדכנו את ההגדרות האלו. ראו מדריך זה כדי ללמוד עוד.' - twitter_config_warning: 'השרת מכוון לאפשר הרשמה והתחברות עם טוויטר (enable_twitter_logins), אבל ערכי המפתח והסוד אינם קבועים. לכו ל הגדרות האתר ועדכנו את ההגדרות האלו. ראו מדריך זה כדי ללמוד עוד.' - github_config_warning: 'השרת מכוון לאפשר הרשמה והתחברות עם גיטהאב (enable_github_logins), אבל ערכי מזהה הלקוח והסוד אינם קבועים. לכו ל הגדרות האתר ועדכנו את ההגדרות האלו. ראו מדריך זה כדי ללמוד עוד.' - failing_emails_warning: 'יש %{num_failed_jobs} עבודות מייל שנכשלו. בידקו את app.yml שלכם כדי לוודא ששרת המייל מוגדר כיאות. ראו את העבודות שנכשלו ב Sidekiq.' subfolder_ends_in_slash: "הגדרות תיקיית המשנה שלכם לא נכונות, הנתיב DISCOURSE_RELATIVE_URL_ROOT צריך להסתיים בלוכסן." email_polling_errored_recently: one: "ניסיונות שליחת מיילים יצרו תקלה ב 24 השעות האחרונות. צפו ביומנים לפרטים נוספים." @@ -969,14 +960,12 @@ he: password_unique_characters: "מספר מינימלי של תווים ייחודיים שחייבים להיות בסיסמאות." block_common_passwords: "אל תאפשרו סיסמאות מתוך 10,000 הסיסמאות הנפוצות ביותר." enable_sso: "הפעלת התחברות יחידה (Single Sign On) באמצעות אתר חיצוני (א-ז-ה-ר-ה: כתובות המייל של משתמשים *חייבות* לעבור אימות על ידי אתר חיצוני!)" - enable_sso_provider: "הטמיעו את פרוטוקול הספק Discourse SSO בנקודת הקצה /session/sso_provider, דורש כיוון של sso_secret" sso_url: "URL of single sign on endpoint (must include http:// or https://)" sso_secret: "מחרוזת סודית המשמשית לאמת באופן קרפיטוגרפי מידע SSO, וודאו שהיא באורך 10 תווים ומעלה" sso_overrides_bio: "דורס ביוגרפיה של משתמשים בפרופיל המשתמש ומונע מהם מלשנות אותה" sso_overrides_email: "דורס מייל מקומי עם אתר מייל חיצוני מתוכן SSO בכל התחברות, ומונע שינויים מקומיים. (אזהרה: אי-התאמות עלולות להתרחש בעקבות נורמליזציה של מיילים מקומיים)" sso_overrides_username: "דורס שם משתמש מקומי עם שם משתמש של אתר חיצוני מתוכן SSO בכל התחברות, ומונע שינויים מקומיים. (אזהרה: אי-התאמות עלולות להתרחש בעקבות הבדלים בדרישות אורך לגבי שמות משתמשים)" sso_overrides_name: "דורס שם מלא מקומי עם שם מלא מאתר חיצוני מתוכן SSO בכל התחברות, מונע שינויים מקומיים." - sso_overrides_avatar: "מעקף אווטאר של משתמש בעזרת אווטר אתר חיצוני מ-SSO Payload. אם אפשרות זו מופעלת, מומלץ מאוד לבטל את האפשרות להעלאת אווטר." sso_not_approved_url: "SSO של חשבונות שלא אושרו יופנו ל URL זה" sso_allows_all_return_paths: "אל תגבילו את שמות המתחם של נתיבי החזרה (return_paths) שניתנים על ידי ה-SSO (כברירת מחדל, נתיבי החזרה חייבים להיות באתר הנוכחי)" allow_new_registrations: "הפעלת הרשמות משתמשים חדשים. בטלו סימון זה כדי למנוע יצירת חשבונות חדשים." @@ -991,7 +980,6 @@ he: allow_restore: "אפשר שחזור, אשר יכול להחליף את כל(!) המידע באתר! הותירו על \"שלילי\" (false) אלא אם כן אתם מתכננים לשחזר גיבוי." maximum_backups: "המספר המקסימלי של גיבויים לשמירה על הכונן. גיבויים ישנים יותר ימחקו אוטומטית" automatic_backups_enabled: "הרץ גיבויים אוטומטים כמו שמוגדר בתדירות הגיבויים" - enable_s3_backups: "העלאת גיבויים ל-S3 לאחר השלמתם. חשוב: דורש הזנת הרשאות S3 תקפות להגדרות הקבצים." s3_backup_bucket: "הדלי המרוחק שבו לשמור גיבויים. אזהרה: שימו לב שהוא דלי פרטי." s3_disable_cleanup: "בטלו את ההסרה של גיבויים מ S3 כאשר הם מוסרים מקומית." backup_time_of_day: "הגדרת זמן לגיבוי לפי UTC." @@ -2036,7 +2024,6 @@ he: נדרש חשבון. נא ליצור חשבון או להיכנס כדי להמשיך. terms_of_service: title: "תנאי השימוש" - signup_form_message: 'קראתי את ואני מסכים/מה עם תנאי השירות.' deleted: 'נמחק' image: "תמונה" upload: @@ -2462,6 +2449,3 @@ he: title: "הזמינו צוות" finished: title: "ה Discourse שלכם מוכן!" - description: | -

    אם אתם אי פעם מרגישים שאתם צריכים לשנות הגדרות אלו, בקרו באזור הניהול שלכם; מיצאו אותו ליד האייקון של מפתח הברגים בתפריט האתר.

    -

    תהנו, ובהצלחה בבניית הקהילה החדשה שלכם!

    diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index a4e1abd3fe..5c7a28516e 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -642,8 +642,6 @@ it: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Inappropriato' - description: 'Questo messaggio ha dei contenuti che una persona ragionevole considererebbe offensivi, aggressivi o una violazione delle linee guida della comunità.' - short_description: 'Una violazione delle linee guida della nostra comunità ' long_form: 'segnalato come inappropriato' notify_user: title: 'Invia un messaggio a @{{username}}' @@ -688,11 +686,9 @@ it: long_form: 'segnalato come spam' inappropriate: title: 'Inappropriato' - description: 'Questo argomento ha dei contenuti che una persona ragionevole considererebbe offensivi, aggressivi o una violazione delle linee guida della comunità.' long_form: 'segnalato come inappropriato' notify_moderators: title: "Altro" - description: 'Questo argomento richiede attenzione da parte deilo staff in base alle linee guida, ai TOS o per altri motivi non elencati.' long_form: 'segnalato all''attenzione dei moderatori' email_title: 'L''argomento "%{title}" richiede l''attenzione di un moderatore' email_body: "%{link}\n\n%{message}" @@ -923,11 +919,6 @@ it: sidekiq_warning: 'Sidekiq non è in esecuzione. Molte attività, come l''invio di email, sono eseguite in maniera asincrona da sidekiq. Assicurati che almeno un processo sidekiq sia in esecuzione. Leggi altro su sidekiq qui.' queue_size_warning: 'Il numero di job in coda è %{queue_size}, il che è alto. Ciò potrebbe indicare un problema con i processi Sidekiq, oppure devi aggiungere altri worker Sidekiq.' memory_warning: 'Il tuo server gira con meno di 1 GB di memoria. Si raccomanda almeno 1 GB di memoria.' - google_oauth2_config_warning: 'Il server è configurato per permettere iscrizioni e login con Google Oauth2 (enable_google_oauth2_logins), ma il client id e il client secret non sono impostati. Vai nelle Impostazioni del sito e aggiorna le impostazioni. Leggi questa guida per saperne di più.' - facebook_config_warning: 'Il server è configurato per accettare iscrizioni e login con Facebook (enable_facebook_logins), tuttavia i parametri app id e secret non sono stati impostati. Vai alle Impostazioni e aggiorna i campi interessati. Leggi questa guida per saperne di più.' - twitter_config_warning: 'Il server è configurato per accettare iscrizioni e login con Twitter (enable_twitter_logins), tuttavia i parametri key e secret non sono stati impostati. Vai alle Impostazioni e aggiorna i campi interessati. Leggi questa guida per saperne di più.' - github_config_warning: 'Il server è configurato per accettare iscrizioni e login con GitHub (enable_github_logins), tuttavia i parametri client id e secret non sono stati impostati. Vai alle Impostazioni e aggiorna i campi interessati. Leggi questa guida per saperne di più.' - failing_emails_warning: 'Ci sono %{num_failed_jobs} job di email falliti. Controlla il file app.yml e assicurati che le impostazioni del mail server siano corrette. Vedi i job falliti in Sidekiq.' subfolder_ends_in_slash: "L'impostazione della sottocartella è errata; DISCOURSE_RELATIVE_URL_ROOT finisce con uno slash." email_polling_errored_recently: one: "Il polling delle email ha generato un errore nelle ultime 24 ore. Controlla i log per maggiori dettagli." @@ -1061,14 +1052,12 @@ it: password_unique_characters: "Numero minimo di caratteri unici che una password deve avere." block_common_passwords: "Non permettere password che sono nelle 10.000 password più comuni." enable_sso: "Abilita il single sign on da sito esterno (ATTENZIONE: L'INDIRIZZO EMAIL DEGLI UTENTI *DEVE* ESSERE VALIDATO DAL SITO ESTERNO!)" - enable_sso_provider: "Implementa il protocollo SSO Discourse nell'endpoint /session/sso_provider, richiede che sia impostata l'opzione sso_secret" sso_url: "URL per l'endpoint del single sign on (deve includere http:// o https://)" sso_secret: "Stringa segreta utilizzata per autenticare crittograficamente le informazioni SSO, assicurati che sia lunga almeno 10 caratteri" sso_overrides_bio: "Sovrascrive la biografia utente nel profilo utente ed impedisce agli utenti di cambiarla" sso_overrides_email: "Sovrascrive le email locali con quelle del sito esterno con cui ci si connette tramite SSO (ATTENZIONE: potrebbero avvenire discrepanze a causa delle normalizzazioni delle email locali)" sso_overrides_username: "Sovrascrive il nome utente locale con quello del sito esterno con cui ci si connette tramite SSO e impedisce cambiamenti locali (ATTENZIONE: potrebbero verificarsi discrepanze a causa delle differenze tra lunghezza effettiva e richiesta del nome utente)" sso_overrides_name: "Sovrascrive il nome locale completo con quello del sito esterno con cui ci si connettei tramite SSO, e impedisce modifiche locali." - sso_overrides_avatar: "Sostituisce l'avatar dell'utente con un avatar su un sito esterno usando SSO. Se abilitato, si raccomanda di disabilitare allow_uploaded_avatars" sso_not_approved_url: "Ridirigi a questa URL gli account SSO non approvati" sso_allows_all_return_paths: "Non restringere il dominio per i return_paths forniti da SSO (per default il return path deve essere sul sito corrente)" allow_new_registrations: "Abilita la registrazione di nuovi utenti. Se deselezionato, non sarà possibile creare nuovi account." @@ -1084,7 +1073,6 @@ it: maximum_backups: "Il numero massimo di backup da mantenere sul disco. I backup più vecchi vengono automaticamente cancellati." automatic_backups_enabled: "Esegui backup automatici come definito nella frequenza di backup" backup_frequency: "Numero di giorni tra due backup." - enable_s3_backups: "Carica i backup su S3 quando completati. IMPORTANTE: richiede che siano inserite valide credenziali S3 nelle impostazioni File." s3_backup_bucket: "Il bucket remoto che contiene i backup. ATTENZIONE: assicurati che sia un bucket privato." s3_disable_cleanup: "Disabilita la rimozione dei backup da S3 quando rimossi localmente." backup_time_of_day: "Ora del giorno in UTC in cui eseguire il backup." @@ -2335,7 +2323,6 @@ it: E' richiesto un account. Per continuare, crea un nuovo account oppure connettiti. terms_of_service: title: "Termini di Servizio" - signup_form_message: 'Ho letto e accetto i Termini del Servizio.' deleted: 'cancellati' image: "immagine" upload: @@ -2928,9 +2915,6 @@ it: description: "Hai quasi finito! Invita alcune persone a contribuire alle discussioni con argomenti e risposte interessanti per far partire la tua comunità. " finished: title: "Il tuo Discourse è Pronto!" - description: | -

    Se te la senti di cambiare queste impostazioni, visita la tua sezione amministrazione; la trovi accanto all'icona della chiave inglese nel menu hamburger del sito.

    -

    Divertiti e buona fortuna nel costruire la tua nuova comunità!

    discourse_push_notifications: popup: confirm_body: 'Successo! Le notifiche sono state abilitate.' diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 4457fbc5b2..99952db804 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -126,6 +126,8 @@ ko: backup_file_should_be_tar_gz: "백업 파일은 .tar.gz형식의 파일이어야 합니다." not_enough_space_on_disk: "백업을 업로드할 디스크 공간이 충분하지 않습니다." invalid_filename: "백업 파일명에 허용되지 않는 문자가 포함되어 있습니다. 알파벳, 숫자, 점(.), 대쉬(-), 언더바(_)만 사용할 수 있습니다." + location: + s3: "아마존 S3" not_logged_in: "로그인이 필요합니다." not_found: "요청한 URL이나 리소스를 찾지 못했습니다." invalid_access: "요청한 리소스를 볼 권한이 없습니다." @@ -470,6 +472,11 @@ ko: title: '비밀번호 초기화' success: "비밀번호를 성공적으로 변경하고 지금 로그인 되었습니다." success_unapproved: "비밀번호를 성공적으로 변경하였습니다." + user_auth_tokens: + browser: + edge: "마이크로소프트 엣지" + device: + unknown: "알수없는 디바이스" change_email: confirmed: "이메일 주소가 변경되었습니다." please_continue: "%{site_name}으로 가기" @@ -509,8 +516,6 @@ ko: email_body: "%{link}\n\n%{message}" inappropriate: title: '부적절함' - description: '이 글은 다른 사용자들에게 공격적이나 모욕적 또는 침해적인 글을 담고 있습니다.' - short_description: '커뮤니티 가이드라인 의 위반' long_form: '부적절함으로 신고하기' notify_user: title: '@{{username}} 님에게 메시지를 보냅니다' @@ -552,13 +557,9 @@ ko: long_form: '스팸으로 신고하였습니다.' inappropriate: title: '부적절함' - description: '이 글은 평균적인 사람들의 관점에서 볼 때 공격적이나 모욕적인 글을 담고 있거나, - - 커뮤니티 가이드라인에 맞지 않습니다.' long_form: '부적절함으로 신고하였습니다.' notify_moderators: title: "기타" - description: '이 주제는 가이드라인, 이용약관, 위에서 언급되지 않은 기타 사유로 인해 운영진이 검토할 필요가 있습니다.' long_form: '관리자의 주의를 위해 신고' email_title: '글타래 "%{title}" 은 운영자의 확인이 필요합니다' email_body: "%{link}\n\n%{message}" @@ -603,6 +604,18 @@ ko: read: "모두 읽기" write: "모두 쓰기" reports: + default: + labels: + percent: 퍼센트 + post_edits: + labels: + edit_reason: 사유 + moderators_activity: + labels: + revision_count: 리비젼 + flags_status: + labels: + flag: 종류 visits: title: "사용자 방문" xaxis: "일" @@ -641,6 +654,9 @@ ko: title: "회원등급당 사용자 수" xaxis: "회원등급" yaxis: "사용자 수" + users_by_type: + labels: + type: 종류 emails: title: "이메일 보냄" xaxis: "일" @@ -741,11 +757,6 @@ ko: sidekiq_warning: 'Sidekiq 이 현재 실행되고 있지 않습니다. Sidekiq는 이메일 전송 같은 많은 작업들을 비동기식으로 처리합니다. 적어도 하나의 sidekiq 프로세서를 실행시켜 주세요. Sidekiq 배우기.' queue_size_warning: '큐 작업의 수가 %{queue_size} 개 입니다. 작업의 수가 너무 많습니다. Sidekiq에 문제가 있을 수 있습니다. Sidekiq Worker를 더 추가하세요.' memory_warning: '당신의 서버는 1GB 이하 메모리로 실행되고 있습니다. 적어도 1GB 이상의 메모리를 사용하세요.' - google_oauth2_config_warning: 'Google OAuth2 인증을 통한 로그인과 회원가입(enable_google_oauth2_logins)을 설정하였습니다. 하지만, 아직 Client ID와 Client Secret을 입력하지 않았습니다. the Site Settings and update the settings. 자세히 알아보기.' - facebook_config_warning: '당신의 서버는 페이스북을 통한 가입을 설정하였습니다(enable_facebook_logins), 그러나 app id와 app secret 값을 입력하지 않았습니다. 사이트 설정 에서 업데이트 해주세요. 가이드 읽어보기.' - twitter_config_warning: '당신의 서버는 트위터를 통한 가입을 설정하였습니다(enable_twitter_logins), 그러나 key 와 secret 값이 입력하지 않았습니다. 사이트 설정 에서 업데이트 해주세요. 가이드 읽어보기.' - github_config_warning: '당신의 서버는 Github를 통한 가입을 설정하였습니다(enable_github_logins), 그러나 client id 와 secret 값을 입력하지 않았습니다. 사이트 설정 에서 업데이트 해주세요. 가이드 읽어보기.' - failing_emails_warning: '실패한 이메일 작업이 %{num_failed_jobs} 개 있습니다. app.yml 을 열어서 메일 서버 설정이 정확한지 확인해보세요. Sidekiq에서 실패한 작업 보기.' subfolder_ends_in_slash: "서브폴더 설정이 정확하지 않습니다. DISCOURSE_RELATIVE_URL_ROOT 다음에 슬래시가 있습니다." email_polling_errored_recently: other: "이메일 폴링에서 지난 24시간 동안%{count}개의 에러가 발생하였습니다. 세부 정보는 로그에서 확인하세요." @@ -866,14 +877,12 @@ ko: password_unique_characters: "패스워드에 포함되어야 하는 최소 유일글자 수" block_common_passwords: "가장 흔히 사용되는 10,000개 비밀번호 목록에 있는 비밀번호를 사용하는 것을 허용하지 않음." enable_sso: "외부사이트 Single Sign On, SSO를 통해 로그인 활성화(경고: 유저의 이메일 주소는 반드시 외부에서 확인해야합니다!)" - enable_sso_provider: "/session/sso_provider 를 엔드포인트로 사용해서 Discourse SSO 제공 프로토콜 구현하기, sso_secret 체크 필요" sso_url: " 싱글 사인 온(SSO)엔드포인트의 URL (http:// 또는 https:// 포함 필수)" sso_secret: "SSO 정보 암호화 인증에 사용 될 암호 문자열, 10글자 이상이어야 합니다." sso_overrides_bio: "사용자 프로필에서 사용자 bio를 덮어쓰고 사용자의 변경을 금지" sso_overrides_email: "SSO로 접속 시 사용자 이메일 정보 갱신 후 사용자 임의 변경 금지 설정 (경고: 이메일 정보 불일치가 생길 수도 있습니다)" sso_overrides_username: "SSO로 접속 시 사용자 아이디 정보 갱신 후 사용자 임의 변경 금지 설정 (경고: 아이디 길이/필요조건 등 내부 규정으로 인해 불일치가 생길 수도 있습니다)" sso_overrides_name: "SSO로 접속 시 사용자 실명 정보 갱신 후 사용자 임의 변경 금지 설정" - sso_overrides_avatar: "SSO로 접속 시 사용자 아바타 이미지 갱신. 활성화하고 allow_uploaded_avatars를 비활성화를 매우 권장합니다." sso_not_approved_url: "승인안된 SSO 계정은 이 URL로 이동" sso_allows_all_return_paths: "SSO로 제공된 return_paths를 위한 도메인 제한 해제 (기본 return path는 현재 사이트로 설정)" allow_new_registrations: "새로운 사용자 가입을 허락함. 새로운 계정 생성을 막으려면 체크하지 않음." @@ -888,7 +897,6 @@ ko: allow_restore: "데이터 복원을 허용합니다. 이 사이트의 모든 데이터가 변경될 수 있다. 백업이나 복원 계획이 없다면 비활성화로 놔둡니다." maximum_backups: "디스크에 유지할 최대 백업 개수. 오래된 백업순으로 자동으로 삭제된다." automatic_backups_enabled: "설정된 백업 주기로 자동 백업 실행" - enable_s3_backups: "백업이 완료되면 Amazon S3로 업로드합니다. 중요: s3 credentials을 기입되어있는지 확인 필요" s3_backup_bucket: "백업본을 유지할 s3 버켓 이름. 주의 : 프라이빗 버켓인지 반드시 확인해야하세요." s3_disable_cleanup: "로컬에서 백업 삭제 시 S3에서 백업 제거하는 기능 해제" backup_time_of_day: "백업이 실행되어야 할 UTC 시간" @@ -1204,6 +1212,9 @@ ko: new_registrations_disabled: "지금은 새로 가입할 수 없습니다." password_too_long: "비밀번호는 200글자 이내만 허용됩니다." missing_user_field: "사용자 정보 입력을 덜 끝마쳤습니다." + second_factor_backup_description: "백업 코드 중 하나를 입력하세요:" + second_factor_toggle: + backup_code: "대신 백업 코드 사용" user: deactivated_by_staff: "운영진에 의해 비활성화됨" activated_by_staff: "운영진에 의해 활성화됨" @@ -1272,6 +1283,8 @@ ko: 이메일에 내용이 안 보입니다. 답글이 이메일 상단에 있는지 확인 해주세요 -- 한줄 답변은 처리 못합니다. email_error_notification: title: "이메일 에러 알림" + email_revoked: + subject_template: "이메일 주소가 맞습니까?" too_many_tl3_flags: title: "TL3 Flag가 너무 많음" subject_template: "대기중인 신규 계정" @@ -1361,9 +1374,9 @@ ko: recent_topics: "최근" see_more: "더" search_title: "이 사이트 검색" + search_button: "검색" terms_of_service: title: "서비스 이용약관" - signup_form_message: 'I have read and accept the Terms of Service.' deleted: '삭제되었습니다' image: "이미지" upload: @@ -1378,6 +1391,11 @@ ko: images: too_large: "죄송합니다, 업로드하려는 이미지가 너무 큽니다. (최대 크기 %{max_size_kb}KB). 크기를 줄여서 다시 시도해보세요." size_not_found: "죄송합니다. 이미지 사이즈가 잘못 되었습니다. 혹시 깨진 이미지가 아닌가요?" + skipped_email_log: + user_email_anonymous_user: "사용자는 익명입니다" + user_email_user_suspended: "사용자가 차단됨" + sender_message_blank: "메시지 내용이 없습니다" + sender_body_blank: "본문 내용이 없습니다" color_schemes: base_theme_name: "기본 테마 색상" dark: "다크 Scheme" @@ -1695,6 +1713,3 @@ ko: title: "운영진 초청하기" finished: title: "당신의 Discourse가 준비되었습니다!" - description: | -

    이 설정을 바꾸고 싶어지면, 관리자 섹션으로 들어가세요. 사이트 메뉴에 공구처럼 생긴 아이콘을 누르면 됩니다.

    -

    당신의 새로운 커뮤니티 만들기를 응원합니다!

    diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index 076332d641..b35e7e8caf 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -642,8 +642,6 @@ nb_NO: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Upassende' - description: 'Dette innlegget har innhold som for en fornuftig person vil kunne være fornærmende, nedverdigende eller brudd på retningslinjene til denne gemenskapen.' - short_description: 'Et brudd på retningslinjene for samfunnet' long_form: 'markerte dette som upassende' notify_user: title: 'Send en melding til @{{username}}' @@ -692,12 +690,9 @@ nb_NO: short_description: 'Dette er en reklame' inappropriate: title: 'Upassende' - description: 'Dette innlegget har innhold som en fornuftig person vil anslå å være fornærmende, nedverdigende eller brudd på retningslinjene til denne gemenskapen.' long_form: 'markerte dette som upassende' - short_description: 'Et brudd på retningslinjene for samfunnet' notify_moderators: title: "Noe annet" - description: 'Emnet krever oppfølging av staben basert på retningslinjene, brukeravtale, eller en annen grunn ikke i listen over.' long_form: 'Markert for mederering' short_description: 'Krever handling fra staben av en annen grunn' email_title: 'Tråden "%{title}" krever oppmerksomhet fra en moderator' @@ -976,11 +971,6 @@ nb_NO: sidekiq_warning: 'Sidekiq kjører ikke. Mange oppgaver, som å sende e-post, kjøres i bakgrunnen av sidekiq. Vennligst verifiser at minst én sidekiq-prosess kjører. Lær mer om Sidekiq her.' queue_size_warning: 'Antallet jobber som står i kø er %{queue_size}, som er høyt. Dette kan være en indikasjon på at Sidekiq-prosessen(e) har et problem, eller det kan tenkes du bare trenger flere samtidige Sidekiq-arbeidere.' memory_warning: 'Serveren din kjører med mindre enn 1 GB med minne. Minst 1 GB RAM er anbefalt.' - google_oauth2_config_warning: 'Denne serveren er konfigurert for å tillate innmelding og innlogging med Google OAuth2 (enable_google_oauth2_logins), men verdiene for klientid og klienthemmelighet er ikke satt. Gå til Instillingene for nettstedet og oppdater dem. Les denne guiden for å lære mer.' - facebook_config_warning: 'Denne serveren er konfigurert for å tillate innmelding og innlogging med Facebook (enable_facebook_logins), men verdiene for app-id og app-hemmelighet er ikke satt. Gå til Instillingene for nettstedet og oppdater dem. Les denne guiden for å lære mer.' - twitter_config_warning: 'Denne serveren er konfigurert for å tillate innmelding og innlogging med Twitter (enable_twitter_logins), men verdiene for nøkkel og hemmelighet er ikke satt. Gå til Instillingene for nettstedet og oppdater dem. Les denne guiden for å lære mer.' - github_config_warning: 'Denne serveren er konfigurert for å tillate innmelding og innlogging med GitHub (enable_github_logins), men verdiene for klientid og hemmelighet er ikke satt. Gå til Instillingene for nettstedet og oppdater dem. Les denne guiden for å lære mer.' - failing_emails_warning: 'Det er %{num_failed_jobs} e-postjobber som har feilet. Sjekk instillingene i app.yml og verifiser at serverinstillingene for e-post er riktige. Se de feilede jobbene i Sidekiq.' subfolder_ends_in_slash: "Oppsettet av undermappe er feil; variablen DISCOURSE_RELATIVE_URL_ROOT slutter med en skråstrek." email_polling_errored_recently: one: "Henting av e-post har generert en feil de siste 24 timene. Se i loggene for mer detaljer." @@ -1263,7 +1253,6 @@ nb_NO: search_title: "Søk i denne siden" terms_of_service: title: "Bruksvilkår" - signup_form_message: 'Jeg har lest og akseptert Bruksvilkårene.' deleted: 'slettet' upload: edit_reason: "Last ned lokal kopi av bilder" diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 0a23683d92..21284080e2 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -532,8 +532,6 @@ nl: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Ongepast' - description: 'Dit bericht bevat inhoud die een redelijk persoon als beledigend, kwetsend of een overtreding van onze gemeenschapsrichtlijnen zou beschouwen.' - short_description: 'Een overtreding van onze community richtlijnen' long_form: 'heeft dit als ongepast gemarkeerd' notify_user: title: '@{{username}} een bericht sturen' @@ -573,11 +571,9 @@ nl: long_form: 'heeft dit als spam gemarkeerd' inappropriate: title: 'Ongepast' - description: 'Dit topic bevat inhoud die een redelijk persoon als beledigend, kwetsend of een overtreding van onze gemeenschapsrichtlijnen zou beschouwen.' long_form: 'heeft dit als ongepast gemarkeerd' notify_moderators: title: "Iets anders" - description: 'Dit topic vereist algemene aandacht van een staflid op basis van de richtlijnen, voorwaarden, of om een andere reden dan hierboven vermeld.' long_form: 'heeft dit gemarkeerd voor aandacht van een moderator' email_title: 'Het topic ''%{title}'' vereist aandacht van een moderator' email_body: "%{link}\n\n%{message}" @@ -759,11 +755,6 @@ nl: sidekiq_warning: 'Sidekiq is niet actief. Veel taken, zoals het verzenden van e-mail, worden door Sidekiq asynchroon uitgevoerd. Zorg ervoor dat er minstens één Sidekiq-proces actief is. Lees hier meer over Sidekiq.' queue_size_warning: 'Het aantal taken in de wachtrij is %{queue_size}, wat hoog is. Dit zou op een probleem met Sidekiq-processen kunnen duiden, of mogelijk dient u meer Sidekiq-workers toe te voegen.' memory_warning: 'Uw server werkt met minder dan 1 GB aan totaal geheugen. Minstens 1 GB geheugen wordt aanbevolen.' - google_oauth2_config_warning: 'De server is geconfigureerd om registratie en aanmelding via Google OAuth2 (enable_google_oauth2_logins) toe te staan, maar de waarden voor client-ID en clientgeheim zijn niet ingesteld. Ga naar de Website-instellingen en werk de instellingen bij. Bekijk deze handleiding voor meer info.' - facebook_config_warning: 'De server is geconfigureerd om registratie en aanmelding via Facebook toe te staan (enable_facebook_logins), maar de waarden voor app-ID en app-geheim zijn niet ingesteld. Ga naar de Website-instellingen en werk de instellingen bij. Bekijk deze handleiding voor meer info.' - twitter_config_warning: 'De server is geconfigureerd om registratie en aanmelding via Twitter toe te staan (enable_twitter_logins), maar de waarden voor sleutel en geheim zijn niet ingesteld. Ga naar de Website-instellingen en werk de instellingen bij. Bekijk deze handleiding voor meer info.' - github_config_warning: 'De server is geconfigureerd om registratie en aanmelding via GitHub toe te staan (enable_github_logins), maar de waarden voor client-ID en -geheim zijn niet ingesteld. Ga naar de Website-instellingen en werk de instellingen bij. Bekijk deze handleiding voor meer info.' - failing_emails_warning: 'Er zijn %{num_failed_jobs} mislukte e-mailtaken. Controleer uw bestand app.yml en zorg ervoor dat de mailserverinstellingen juist zijn. Bekijk de mislukte taken in Sidekiq.' subfolder_ends_in_slash: "Uw submapconfiguratie is onjuist; de DISCOURSE_RELATIVE_URL_ROOT eindigt met een schuine streep." email_polling_errored_recently: one: "E-mailpolling heeft de afgelopen 24 uur een fout gegenereerd. Bekijk de logboeken voor meer details." @@ -869,13 +860,11 @@ nl: password_unique_characters: "Minimum aantal unieke karakters dat een wachtwoord nodig heeft." block_common_passwords: "Accepteer geen wachtwoorden die voorkomen in de 10.000 meest voorkomende wachtwoorden." enable_sso: "Schakel single sign on in via een externe site (WAARSCHUWING: E-MAILADRESSEN VAN GEBRUIKERS '* MOETEN * DOOR DE EXTERNE SITE WORDEN GEVALIDEERD)" - enable_sso_provider: "Pas Discourse SSO provider protocol toe op het /session/sso_provider eindpunt, vereist dat sso_secret is ingesteld" sso_secret: "Geheime tekenreeks voor het cryptografisch valideren van SSO informatie, zorg er voor dat het 10 tekens of langer is" sso_overrides_bio: "Overschrijft de biografie in het gebruikersprofiel en voorkomt dat de gebruiker het kan wijzigen" sso_overrides_email: "Overschrijft bij elke aanmelding lokaal opgegeven mailadres door mailadres uit SSO payload van externe site, en voorkomt lokaal wijzigen. (WAARSCHUWING: er kunnen discrepanties ontstaan door normalisatie van lokale mailadressen)" sso_overrides_username: "Overschrijft bij elke aanmelding lokaal opgegeven gebruikersnaam door gebruikersnaam uit SSO payload van externe site, en voorkomt lokaal wijzigen. (WAARSCHUWING: er kunnen discrepanties ontstaan door verschillen in gebruikersnaam lengte/vereisten)" sso_overrides_name: "Overschrijft bij elke aanmelding lokaal opgegeven volledige naam door volledige naam uit SSO payload van externe site, en voorkomt lokaal wijzigen" - sso_overrides_avatar: "Overschrijft gebruikers avatar door avatar uit SSO payload van externe site, uitschakelen van allow_uploaded_avatars is sterk aanbevolen." sso_not_approved_url: "Stuur niet geaccepteerde SSO accounts door naar deze URL" allow_new_registrations: "Nieuwe gebruikers registreren toestaan. Uitschakelen om te voorkomen dat iedereen een nieuw account kan maken." enable_signup_cta: "Een melding tonen voor terugkerende anonieme gebruikers waarin wordt gevraagd zich voor een account te registreren" @@ -887,7 +876,6 @@ nl: allow_restore: "Herstellen van data toestaan, waarbij ALLE site-data wordt overschreven! Laat op 'false' staan, tenzij je een back-up terug wil zetten." maximum_backups: "Het maximale aantal back-ups om op schijf te bewaren. Oudere back-ups worden automatisch verwijderd." automatic_backups_enabled: "Automatische back-ups uitvoeren volgens de opgegeven back-up frequentie." - enable_s3_backups: "Upload voltooide back-ups naar S3. LET OP: Zorg ervoor dat je je S3 inloggegevens hebt ingevuld in de Bestanden instellingen." s3_backup_bucket: "De S3 bucket voor de backups. WAARSCHUWING: zorg er voor dat dit een privébucket is." s3_disable_cleanup: "Backups van S3 niet verwijderen als het lokaal wordt verwijdert." backup_time_of_day: "Tijdstip UTC wanneer de back-up moet plaatsvinden." @@ -1377,7 +1365,6 @@ nl: search_title: "Zoeken op deze website" terms_of_service: title: "Servicevoorwaarden" - signup_form_message: 'Ik heb de Servicevoorwaarden gelezen en ga hiermee akkoord.' deleted: 'verwijderd' upload: edit_reason: "lokale kopieën van afbeeldingen gedownload" @@ -1471,6 +1458,3 @@ nl: title: "Staf uitnodigen" finished: title: "Uw Discourse is gereed!" - description: | -

    Als u deze instellingen ooit wilt wijzigen, bezoek dan uw beheersectie; deze vindt u naast het moersleutelpictogram in het websitemenu.

    -

    Veel plezier, en succes met het opbouwen van uw nieuwe gemeenschap!

    diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 563de9b8ce..16e85a52cc 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -593,8 +593,6 @@ pl_PL: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Niewłaściwe' - description: 'Ten wpis zawiera treści które umiarkowana osoba może uznać za wulgarne, obraźliwe lub naruszające wytyczne społeczności.' - short_description: 'Naruszenie naszych wytycznych dla społeczności ' long_form: 'oflagowano jako niewłaściwe' notify_user: title: 'Wyślij @{{username}} wiadomość' @@ -637,11 +635,9 @@ pl_PL: long_form: 'oflagowano jako spam' inappropriate: title: 'Niewłaściwe' - description: 'Ten temat zawiera treści jakie rozsądna osoba może uznać za agresywne, obraźliwe, lub sprzeczne z wytycznymi społeczności.' long_form: 'oflagowano jako niewłaściwe' notify_moderators: title: "Coś innego" - description: 'Ten wpis wymaga interwencji moderatora z uwagi na niezgodność z wytycznymi społeczności, warunkami użytkowania lub z innego, niewymienionego tu powodu.' long_form: 'oznaczył to dla uwagi moderatora' email_title: 'Temat "%{title}" wymaga uwagi moderatora' email_body: "%{link}\n\n%{message}" @@ -824,11 +820,6 @@ pl_PL: sidekiq_warning: 'Sidekiq nie działa. Wiele zadań, takich jak wysyłanie emaili, jest wykonywane asynchronicznie przez sidekiqa. Zagwarantuj, że przynajmniej jeden proces sidekiqa działa. Dowiedz się więcej o Sidekiqu.' queue_size_warning: 'Liczba oczekujących zadań wynosi %{queue_size}, to dużo. Może to wskazywać na problem z procesem (procesami) Sidekiq, lub możesz potrzebować więcej pracowników Sidekiq.' memory_warning: 'Twój serwer działa z mniej niż 1 GB pamięci całkowitej. Przynajmniej 1 GB pamięci jest zalecany.' - google_oauth2_config_warning: 'Serwer został skonfigurowany, aby umożliwić rejestracje i logowanie za pomocą Google OAuth2 (enable_google_oauth2_logins), ale id klienta oraz tajne wartości klienta nie są ustawione. Przejdź do strony ustawień i zaktualizuj ustawienia, Zobacz przewodnik, aby dowiedzieć się więcej.' - facebook_config_warning: 'Serwer jest skonfigurowany by pozwalać na rejestrację i logowanie za pomocą Facebooka (enable_facebook_logins), ale identyfikator i sekret aplikacji nie są ustawione. Przejdź do ustawień serwisu i zmień ustawienia. Zobacz ten poradnik by dowiedzieć się więcej.' - twitter_config_warning: 'Serwer jest skonfigurowany by pozwalać na rejestrację i logowanie za pomocą Twittera (enable_twitter_logins), ale klucz i sekret nie są ustawione. Przejdź do ustawień serwisu i zmień ustawienia. Zobacz ten poradnik by dowiedzieć się więcej.' - github_config_warning: 'Serwer jest skonfigurowany by pozwalać na rejestrację i logowanie za pomocą GitHuba (enable_github_logins), ale id klienta i sekret nie są ustawione. Przejdź do ustawień serwisu i zmień ustawienia. Zobacz ten poradnik by dowiedzieć się więcej.' - failing_emails_warning: '%{num_failed_jobs} prac emailowych nie powiodło się. Sprawdź plik app.yml i upewnij się, że ustawienia serwera poczty są poprawne. Zobacz nieudane prace w Sidekiqu.' subfolder_ends_in_slash: "Twoje ustawienie podfolderu jest niepoprawne; DISCOURSE_RELATIVE_URL_ROOT kończy się slashem." email_polling_errored_recently: one: "Email polling wygenerował 1 błąd w przeciągu ostatniej doby. Więcej szczegółów w logach." @@ -954,14 +945,12 @@ pl_PL: password_unique_characters: "Minimalna ilość różnych znaków, które muszą być w haśle." block_common_passwords: "Nie zezwalaj na hasła znajdujące się w grupie 10 000 najpopularniejszych haseł." enable_sso: "Włącz pojedyncze logowanie przez zewnętrzną stronę (UWAGA: ADRESY E-MAIL UŻYTKOWNIKÓW *MUSZĄ* BYĆ ZATWIERDZONE PRZEZ ZEWNĘTRZNĄ STRONĘ!)" - enable_sso_provider: "Zaimplementuj protokół dostawcy Discourse SSO w endpoint /session/sso_provider, wymaga ustawienia sso_secret" sso_url: "URL endpoint pojedynczego logowania (musi zawierać http:// lub https://)" sso_secret: "Tajny ciąg znaków używany, aby kryptograficznie uwierzytelnić informacje SSO, powinien mieć 10 lub więcej znaków." sso_overrides_bio: "Nadpisuje biografię użytkownika w jego profilu i uniemożliwia mu zmianę biografii." sso_overrides_email: "Nadpisuje lokalny email poprzez zewnętrzną witrynę email z SSO przy każdym logowaniu, aby zapobiec lokalnym zmianom. (Ostrzeżenie: rozbieżności mogą pojawić się w związku z normalizacją lokalnym emaili)." sso_overrides_username: "Nadpisuje lokalne nazwy użytkowników z pomocą nazw użytkowników z zewnętrznej strony przez SSO przy każdym logowaniu i zapobiega lokalnym zmianom. (Ostrzeżenie: rozbieżności mogą pojawić się w związku z różnicami w długością/wymaganiami nazwy użytkownika)" sso_overrides_name: "Nadpisuje lokalną pełną nazwę za pomocą pełnych nazw z zewnętrznej strony przez SSO przy każdym logowaniu i zapobiega lokalnym zmianom." - sso_overrides_avatar: "Nadpisuje avatar użytkownika z pomocą avatarów z zewnętrznych stron poprzez SSO. Jeśli włączone, wyłączanie allow_uploaded_avatars wysoce polecane" sso_not_approved_url: "Przekieruj niezatwierdzone konta SSO na ten URL" sso_allows_all_return_paths: "Nie ograniczaj domen dla return_paths dostarczanych przez SSO (domyślnie zwracana ścieżka musi być na aktualnej stronie)" allow_new_registrations: "Zezwól na rejestrację nowych użytkowników. Odznacz opcję żeby uniemożliwić rejestrację nowych kont." @@ -976,7 +965,6 @@ pl_PL: allow_restore: "Dopuść przywracanie, które może zamienić WSZYSTKIE dane strony! Zostaw fałsz, chyba że planujesz przywrócić kopię zapasową" maximum_backups: "Maksymalna liczba kopii zapasowych do przechowywania na dysku. Starsze kopie zapasowe zostaną automatycznie usunięte." automatic_backups_enabled: "Uruchom automatyczne kopie zapasowe zgodnie z ustawioną częstotliwością kopii" - enable_s3_backups: "Wysyłaj kopie zapasowe do S3 kiedy gotowe. WAŻNE: wymaga poprawnych poświadczeń S3 wprowadzonych w ustawieniach Plików." s3_backup_bucket: "Zdalne wiadro do przechowywania kopii zapasowych. UWAGA: Upewnij się, że jest to wiadro prywatne." s3_disable_cleanup: "Dezaktywuj usuwanie kopii zapasowych z S3 kiedy usunięte lokalnie." backup_time_of_day: "Godzina (UTC) wykonania kopii zapasowej." @@ -2084,7 +2072,6 @@ pl_PL: terms_of_service: title: "Warunki użytkowania serwisu" - signup_form_message: 'Przeczytałem i zgadzam się z Regulaminem Serwisu.' deleted: 'usunięte' image: "obraz" upload: @@ -2605,6 +2592,3 @@ pl_PL: description: "Już prawie koniec! Teraz zaprośmy trochę ludzi, aby rozwinąć twoją społeczność i pomóc zasiać nowe dyskusje poprzez interesujące tematy i odpowiedzi." finished: title: "Twój Discourse jest Gotowy!" - description: | -

    Jeśli kiedykolwiek będziesz chciał dokonać zmian w tych ustawieniach, przejdź do sekcji admina; znajdziesz ją obok ikony klucza w menu witryny.

    -

    Baw się dobrze i powodzenia w budowaniu nowej społeczności!

    diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index 99d4dde72e..d5d44cca7f 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -480,8 +480,6 @@ pt: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Inapropriado' - description: 'Esta mensagem contém conteúdo que uma pessoa sensata iria considerar ofensivo, abusivo, ou como uma violação das orientações da nossa comunidade.' - short_description: 'Uma violação das nossas linhas diretrizes comunitárias' long_form: 'sinalizou isto como inapropriado' notify_user: title: 'Enviar uma mensagem a @{{username}}' @@ -518,11 +516,9 @@ pt: long_form: 'sinalizou isto como spam' inappropriate: title: 'Inapropriado' - description: 'Este tópico contém conteúdo que uma pessoa sensata consideraria ofensivo, abusivo ou como uma violação das orientações da nossa comunidades.' long_form: 'sinalizou isto como inapropriado' notify_moderators: title: "Algo Mais" - description: 'Este tópico requer a atenção geral do pessoal baseado nas orientações, Termos e Condições, ou por outra razão não listada acima.' long_form: 'sinalizou isto para obter a atenção do moderador' email_title: 'O tópico "%{title}" requer a atenção do moderador' email_body: "%{link}\n\n%{message}" @@ -704,11 +700,6 @@ pt: sidekiq_warning: 'Sidekiq não está em execução. Muitas tarefas, como envio de emails, são executadas de forma assíncrona pelo sidekiq. Por favor certifique-se de que pelo menos um processo sidekiq está em execução. Aprenda sobre Sidekiq aqui.' queue_size_warning: 'O número de trabalhos na fila é %{queue_size}, o que é alto. Isto pode indicar um problema com o(s) processo(s) Sidekiq, ou pode necessitar de adicionar mais trabalhadores Sidekiq.' memory_warning: 'O seu servidor está a executar com menos de 1 GB de memória total. Pelo menos 1 GB é a quantidade de memória recomendada.' - google_oauth2_config_warning: 'O servidor está configurado para permitir inscrever-se e entrar com o Google OAuth2 (enable_google_oauth2_logins), mas o id e os valores privados do cliente não estão configurados. Vá às Configurações do Sítio e atualize as definições. Veja este guia para saber mais.' - facebook_config_warning: 'O servidor está configurado para permitir inscrever-se e entrar com o Facebook (enable_facebook_logins), mas o id e os valores privados da aplicação não estão configurados. Vá às Configurações do Sítio e atualize as definições. Veja este guia para saber mais.' - twitter_config_warning: 'O servidor está configurado para permitir inscrever-se e entrar com o Twitter (enable_twitter_logins), mas a chave e os valores privados não estão configurados. Vá às Configurações do Sítio e atualize as definições. Veja este guia para saber mais.' - github_config_warning: 'O servidor está configurado para permitir inscrever-se e entrar com o GitHub (enable_github_logins), mas o id e valores privados do cliente não estão configurados. Vá às Configurações do Sítio e atualize as definições. Veja este guia para saber mais.' - failing_emails_warning: 'Há %{num_failed_jobs} tarefas de email que falharam. Verifique o seu app.yml e assegure-se que as configurações do servidor de email estão corretas. Veja as tarefas que falharam no Sidekiq.' subfolder_ends_in_slash: "A configuração da sua sub-página está incorreta; o DISCOURSE_RELATIVE_URL_ROOT termina com um traço." email_polling_errored_recently: one: "A consulta automática de emails gerou um erro nas últimas 24 horas. Consulte em os registos para mais detalhes." @@ -820,14 +811,12 @@ pt: min_admin_password_length: "Tamanho mínimo da palavra-passe para Administração." block_common_passwords: "Não permitir palavras-passe que estejam nas 10,000 palavras-passe mais comuns." enable_sso: "Ativar inscrição única através de um sítio externo (AVISO: OS ENDEREÇOS DE EMAIL DOS UTILIZADORES *DEVEM* SER VALIDADOS PELO SÍTIO EXTERNO!)" - enable_sso_provider: "Implementar o protocolo do provedor SSO do Discourse no caminho /session/sso_provider, requer que sso_secret seja configurado" sso_url: "URL da terminação de single-sign-on (tem de incluir http:// ou https://)" sso_secret: "String secreta usada para autenticar criptograficamente informação SSO, garanta que tem 10 ou mais caracteres" sso_overrides_bio: "Sobrepõe-se à biografia do utilizador e previne o utilizador de a mudar" sso_overrides_email: "Substitui o email local por um email de um sítio externo a partir de um SSO em cada início de sessão, e previne mudanças locais. (AVISO: podem ocorrer discrepâncias devido à normalização de emails locais) " sso_overrides_username: "Substitui nomes de utilizadores locais por nomes de utilizadores de sítios externos a partir de um SSO em cada início de sessão, e previne mudanças locais. (AVISO: podem ocorrer discrepâncias devido a diferenças no comprimento/requisitos do nome de utilizador)" sso_overrides_name: "Substitui o nome completo local por um nome completo de um sítio externo a partir de um SSO em cada início de sessão, e previne mudanças locais." - sso_overrides_avatar: "Substitui o avatar do utilizador com avatares externos a partir de um SSO. Se ativo, é altamente recomendada a desativação de allow_uploaded_avatars" sso_not_approved_url: "Redirecionar contas SSO não aprovadas para este URL" allow_new_registrations: "Permitir registo de novos utilizadores. Desmarcar isto para prevenir que alguém crie uma nova conta." enable_signup_cta: "Mostrar um aviso a utilizadores anónimos que retornem levando-os a inscreverem-se para uma conta." @@ -841,7 +830,6 @@ pt: allow_restore: "Permitir o restauro, que pode substituir TODOS os dados do sítio. Deixar a falso a menos que planeie restaurar uma cópia de segurança" maximum_backups: "Valor máximo de cópias de segurança a serem guardadas em disco. Cópias de Segurança antigas são automaticamente eliminadas." automatic_backups_enabled: "Executar cópias de segurança automáticas de acordo com as definições de frequência de cópia de segurança" - enable_s3_backups: "Carregar cópias de segurança para S3 quando completo. IMPORTANTE: requer credenciais S3 válidas inseridas nas configurações dos ficheiros." s3_backup_bucket: "Balde remoto para guardar cópias de segurança. AVISO: Certifique-se que este é um balde privado." s3_disable_cleanup: "Desactive a remoção das cópias de segurança do S3 quando removidas localmente." backup_time_of_day: "Altura do dia UTC em que a cópia de segurança deve ocorrer." @@ -1585,7 +1573,6 @@ pt: search_title: "Pesquisar neste sítio" terms_of_service: title: "Termos de Serviço" - signup_form_message: 'Li e aceito os Termos de Serviço.' deleted: 'eliminado' upload: edit_reason: "cópias locais de imagens transferidas" @@ -1983,6 +1970,3 @@ pt: title: "Convidar Equipa de Apoio" finished: title: "O seu Discourse está Pronto!" - description: | -

    Se alguma vez quiser mudar estas configurações, visite a sua secção de administração; encontre-a junto ao ícone de chave inglesa no menu do sítio.

    -

    Divirta-se, e boa sorte para a construção da sua comunidade!

    diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 7d8693df38..bd92a9e3c7 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -637,8 +637,6 @@ pt_BR: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Inapropriado' - description: 'Este post contém conteúdo que uma pessoa razoável consideraria ofensivo, abusivo, ou uma violação das nossas diretrizes da comunidade.' - short_description: 'Uma violação de nossas diretrizes da comunidade' long_form: 'sinalizado como inapropriado' notify_user: title: 'Envie ao(à) @ {{nome de usuário}} uma mensagem' @@ -684,12 +682,9 @@ pt_BR: short_description: 'Este é um anúncio' inappropriate: title: 'Impróprio' - description: 'Este tópico contém um conteúdo que uma pessoa razoável consideraria ofensivo, abusivo, ou violação de nossas diretrizes da comunidade.' long_form: 'sinalizar como impróprio' - short_description: 'Uma violação de nossas diretrizes da comunidade' notify_moderators: title: "Algo mais" - description: 'Este tópico requer a atenção geral da equipe baseado nas diretrizes da comunidade, no Termos de Serviço, ou em outra razão não listada acima.' long_form: 'sinalizar isso para atenção da moderação' short_description: 'Requer atenção da equipe por outro motivo' email_title: 'O tópico "%{title}" requer atenção do moderador' @@ -968,11 +963,6 @@ pt_BR: sidekiq_warning: 'Sidekiq não está em execução. Muitas tarefas, como envio de emails, são executadas de forma assíncrona pelo sidekiq. Por favor certifique-se de que ao menos um processo sidekiq esteja execução. Aprenda sobre Sidekiq aqui.' queue_size_warning: 'O número de processos na fila é %{queue_size}, o que é alto. Isso pode ser indicação de um problema com o(s) processo(s) do Sidekiq, ou você pode ter que adicionar mais Sidekiq workers.' memory_warning: 'Seu servidor está rodando com menos de 1 GB de memória total. Pelo menos 1 GB é quantidade de memória recomendada.' - google_oauth2_config_warning: 'O servidor está configurado para permitir o signup e login com Google OAuth2 (enable_google_oauth2_logins), mas os valores de Cliend Id e Secret não estão configurados. Vá para as Configurações do Site e atualize as configurações. Veja este guia e aprenda mais.' - facebook_config_warning: 'O servidor está configurado para permitir o signup e login com Facebook (enable_facebook_logins), mas os valores do App Id e App Secret não estão configurados. Vá para as Configurações do Site e atualize as configurações. Veja este guia e aprenda mais.' - twitter_config_warning: 'O servidor está configurado para permitir o signup e login com Twitter (enable_twitter_logins), mas os valores de Key e Secret não estão configurados. Vá para as Configurações do Site e atualize as configurações. Veja este guia e aprenda mais.' - github_config_warning: 'O servidor está configurado para permitir o signup e login com GitHub (enable_twitter_logins), mas os valores de Cliend Id e Secret não estão configurados. Vá para the Site Settings e atualize as configurações. Veja este guia e aprenda mais.' - failing_emails_warning: 'Existem %{num_failed_jobs} tarefas de email que falharam. Verifique seu app.yml e se assegure que as configurações do servidor de email estão corretas. Veja as tarefas que falharam no Sidekiq.' subfolder_ends_in_slash: "Sua configuração de subdiretórios está incorreta; DISCOURSE_RELATIVE_URL_ROOT termina com uma barra." email_polling_errored_recently: one: "A apuração de email gerou um erro nas últimas 24 horas. Veja os logs para mais detalhes." @@ -1125,7 +1115,6 @@ pt_BR: password_unique_characters: "Número mínimo de caracteres únicos que uma senha deve ter." block_common_passwords: "Não permitir senhas que estiverem entre as 10,000 senhas mais comuns." enable_sso: "Permite autenticação única atráves de site externo (AVISO: OS ENDEREÇOS DE E-MAIL *DEVEM* SER VALIDADOS PELO SITE EXTERNO!)" - enable_sso_provider: "Implementar protocolo do provedor Discourse SSO no caminho /session/sso_provider, requer que sso_secret seja configurado" sso_url: "URL do ponto final do logon único (SSO) (deve incluir http:// ou https://)" sso_secret: "String secreta usada para autenticar criptograficamente informação de SSO, esteja certo de que possui 10 ou mais caracteres" sso_overrides_bio: "Sobrescreve a bio de usuário e impede o usuário de mudá-la." @@ -1133,7 +1122,6 @@ pt_BR: sso_overrides_email: "Substitui endereço de e-mail local pelo fornecido no site externo através de cada informação de login, e previne mudanças locais. (AVISO: discrepâncias podem ocorrer devido a normalização de e-mails locais)" sso_overrides_username: "Substitui nome de usuário local com o fornecido pelo site externo através da informação de cada login, e previne mudanças locais. (AVISO: discrepâncias podem ocorrer devido a diferenças ao tamanho e pré-requisitos de nomes de usuário)" sso_overrides_name: "Substitui nome completo local com nome completo do site externo através da informação de login, e previne mudanças locais." - sso_overrides_avatar: "Substitui o avatar do usuário pelo avatar do site externo de SSO. Se habilitado, desabilitar allow_uploaded_avatars é altamente recomendado" sso_overrides_profile_background: "Substitui o plano de fundo do perfil do usuário pelo avatar do site externo da carga útil do SSO." sso_overrides_card_background: "Substitui o plano de fundo do cartão do usuário pelo avatar do site externo da carga útil do SSO." sso_not_approved_url: "Redireciona contas não aprovadas no login externo para este endereço" @@ -1154,7 +1142,6 @@ pt_BR: maximum_backups: "A quantidade máxima de backups para manter no disco. Backups mais antigos são excluídos automaticamente" automatic_backups_enabled: "Realizar backups automáticos, como definido na frequência de backup" backup_frequency: "O número de dias entre os backups." - enable_s3_backups: "Fazer upload dos backups para o S3 quando completado. IMPORTANTE: exige credenciais válidas do S3 configuradas nas Configurações de Arquivos." s3_backup_bucket: "O repositório remoto para realizar backups. AVISO: Certifique-se de que é um repositório privado." s3_endpoint: "O endpoint pode ser modificado para fazer backup em um serviço compatível com S3, como DigitalOcean Spaces ou Minio. AVISO: use o padrão se estiver usando o AWS S3" s3_force_path_style: "Implemente o endereçamento de estilo de caminho para seu endpoint personalizado. IMPORTANTE: Necessário para usar uploads e backups do Minio." @@ -2174,14 +2161,6 @@ pt_BR: spam_post_blocked: title: "Postagem de spam bloqueada" subject_template: "Novo usuário%{username} postagens bloqueadas devido a links repetidos" - text_body_template: | - Essa é uma mensagem automatizada. - - O novo usuário [%{username}](%{user_url}) tentou criar vários posts com links para%{domains}, mas essas postagens foram bloqueadas para evitar spam. O usuário ainda pode criar novas postagens que não vinculam a %{domains}. - - Por favor, [revise o usuário](%{user_url}). - - Isso pode ser modificado através das configurações do site `newuser_spam_host_threshold` e` white_listed_spam_host_domains`. unsilenced: title: "Não silenciado" subject_template: "Conta não está mais em espera" @@ -2603,7 +2582,6 @@ pt_BR: Uma conta é necessária. Por favor, crie uma conta ou faça o login para continuar. terms_of_service: title: "Termos de Serviço" - signup_form_message: 'Eu li e aceito os Termos de Serviço.' deleted: 'removido' image: "imagem" upload: @@ -3151,9 +3129,6 @@ pt_BR: description: "Você está quase pronto! Vamos convidar algumas pessoas para ajudar \nsemear suas discussões com tópicos e respostas interessantes para iniciar sua comunidade." finished: title: "Seu Discourse está pronto!" - description: | -

    Se você sentir vontade de alterar essas configurações, visite sua seção administrativa; encontre-o ao lado do ícone de chave inglesa no menu do site.

    -

    Divirta-se e boa sorte construindo sua nova comunidade!

    search_logs: graph_title: "Contagem de pesquisa" joined: "Ingressou" diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index cc59261299..a93e6e103a 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -489,7 +489,6 @@ ro: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Necorespunzător' - description: 'Această postare are un conținut pe care o persoană rezonabilă l-ar putea considera drept ofensator, abuziv, sau o violare a ghidului comunității.' long_form: 'marcat ca necorespunzător' notify_user: title: 'Trimite-i un mesaj lui @{{username}}' @@ -523,11 +522,9 @@ ro: long_form: 'Marchează ca spam' inappropriate: title: 'Necorespunzător' - description: 'Acest subiect are conținut ce ar putea fi considerat drept ofensator, abuziv, sau o violare a regulilor comunității.' long_form: 'marcat cu marcaj de avertizare ca inadecvat' notify_moderators: title: "Alt motiv" - description: 'Acest subiect necesită atenția generală a echipei pe baza ghidului, CGU, sau pentru un alt motiv care nu este menționat anterior.' long_form: 'marcat cu marcaj de avertizare în atenția moderatorilor' email_title: 'Subiectul "%{title}" necesită atenția moderatorilor' email_body: "%{link}\n\n%{message}" @@ -717,11 +714,6 @@ ro: sidekiq_warning: 'Sidekiq nu este pornit. Multe acțiuni, cum ar fi trimiterea de emailuri, sunt executate asincron de către sidekiq. Asigură-te că măcar un proces din sidekiq este pornit. Citește despre Sidekiq aici.' queue_size_warning: 'Numărul de sarcini aflate în lista de așteptare este de %{queue_size}, ceea ce e mult. Asta poate indica o problemă cu procesul(ele) Sidekiq, sau că e necesar să adaugi mai multi Sidekiq workers.' memory_warning: 'Serverul tău lucrează cu mai puțin de 1 GB memorie. Se recomandă cel puțin 1 GB memorie.' - google_oauth2_config_warning: 'Serverul e configurat să permită autentificarea cu Google OAuth2 (enable_google_oauth2_logins), dar valorile secrete ale clientului precum și id-ul clientului nu sunt setate. Mergi la Setările site-ului și modifică setările. Vizualizează ghidul pentru mai multe informații.' - facebook_config_warning: 'Serverul e configurat să permită înscrierea și autentificarea cu Facebook (enable_facebook_logins), dar app id și valoarea app secret nu sunt setate. Mergi la Setările site-ului și modifică setările. Vizualizează ghidul pentru mai multe informații.' - twitter_config_warning: 'Serverul e configurat să permită înscrierea și autentificarea cu Twitter (enable_twitter_logins),dar cheia și valoarea secretă nu sunt setate. Mergi la Setările site-ului și modifică setarea. Vizualizează ghidul pentru mai multe informații.' - github_config_warning: 'Serverul e configurat să permită înscrierea și autentificarea cu GitHub (enable_github_logins), dar id-ul de client și valorile secrete nu sunt setate. Mergi la Setările site-ului și modifică setarea. Vizualizează ghidul pentru mai multe informații.' - failing_emails_warning: 'Există %{num_failed_jobs} (de) sarcini email care au eșuat. Verifică app.yml și asigură-te că setările serverului de mail sunt corecte. Vizualizează sarcini eșuate în Sidekiq.' subfolder_ends_in_slash: "Setările subfolderului sunt incorecte; DISCOURSE_RELATIVE_URL_ROOT se termină cu slash." email_polling_errored_recently: one: "Email polling a generat o eroare în ultimele 24 de ore. Vizualizează rapoartele pentru mai multe detalii." @@ -834,14 +826,12 @@ ro: min_admin_password_length: "Lungimea maximă a parolei pentru Admin." block_common_passwords: "Nu permite parole ce sunt printre cele 10,000 cele mai cunoscute parole." enable_sso: "Activează opțiunea single sign on via site extern (ATENȚIE: ADRESELE DE EMAIL ALE UTILIZATORILOR *TREBUIE* VALIDATE DE SITE-UL EXTERN!)" - enable_sso_provider: "Implementează protocolul furnizorului SSO Discourse la punctul final /session/sso_provider, sso_secret trebuie să fie setat." sso_url: "URL pentru autentificare unică la punct final (trebuie să includă http:// or https://)" sso_secret: "șir secret folosit pentru autentificarea criptografică a SSO, asigură-te că este de minim 10 caractere" sso_overrides_bio: "Suprascrie biografia utilizatorului în profil utilizator și nu-i permite acestuia să o modifice" sso_overrides_email: "Suprascrie emailul local cu un email de pe un site extern din datele furnizate de SSO la fiecare autentificare și împiedică schimbările locale. (ATENȚIE: pot apărea discrepanțe din cauza normalizării emailurilor locale)" sso_overrides_username: "Suprascrie numele de utilizator local cu numele utilizator de pe site-ul extern din datele SSO la fiecare autentificare și împiedică schimbările locale (ATENȚIE: pot apărea discrepanțe din cauza diferențelor în lungimea/pre-condițiile numelui utilizator)" sso_overrides_name: "Suprascrie numele întreg de pe local cu numele întreg din datele SSO la fiecare autentificare și împiedică schimbările locale." - sso_overrides_avatar: "Suprascrie avatarul utilizatorului cu avatarul din datele SSO. Dacă este activat, se recomandă călduros dezactivarea allow_uploaded_avatars" sso_not_approved_url: "Redirecționează conturile neaprobate SSO către acest URL" sso_allows_all_return_paths: "Nu restricționa domeniul pentru return_paths furnizate de SSO (implicit, calea de întoarcere trebuie să fie pe site-ul curent)" allow_new_registrations: "Permite înregistrarea noilor utilizatori. Debifați pentru a restricționa pe oricine să creeze un cont nou." @@ -856,7 +846,6 @@ ro: allow_restore: "Bifează-l doar dacă dorești să restaurezi un fișier de backup. Atenție, restaurarea poate șterge/înlocui TOATE datele din site!" maximum_backups: "Maximul de fișiere backup păstrate. Fișierele backup vechi sunt șterse automat" automatic_backups_enabled: "Rulează automat operațiuni de backup după cum este definit la frecvența de backup" - enable_s3_backups: "Încarcă fișierele backup pe s3 când sunt complete. IMPORTANT: sunt necesare date de autentificare valide pentru S3 în secțiunea FIșIERE." s3_backup_bucket: "Containerul (bucket) de la distanță care ține backupurile. ATENȚIE: Asgură-te că e un container privat." s3_disable_cleanup: "Dezactivează eliminarea backup-urilor de pe S3 când sunt șterse local." backup_time_of_day: "Ora în format UTC la care să înceapă operațiunea de backup." @@ -1640,7 +1629,6 @@ ro: search_title: "Caută în site" terms_of_service: title: "Condițiile generale de utilizare" - signup_form_message: 'Am citit și accept Condițiile generale de utilizare.' deleted: 'șters' upload: edit_reason: "copii locale ale imaginilor descărcate" @@ -2048,9 +2036,6 @@ ro: description: "Aproape ai încheiat ! Hai să invităm câțiva prieteni să te ajute cu demararea comunității prin discuții și răspunsuri interesante." finished: title: "Discourse-ul tău e gata!" - description: | -

    Dacă vei dori vreodată să schimbi aceste setări, vizitează secțiunea admin; care se găsește lângă iconița cu cheia franceză din meniul site-ului.

    -

    Distracție plăcută și mult succesla construcția noii comunități!

    discourse_push_notifications: popup: mentioned: '%{username} te-a menționat în discuția "%{topic}" - %{site_title}' diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index 7c858a859b..5823df2f7e 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -540,7 +540,6 @@ ru: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Неприемлемо' - description: 'Это сообщение может быть оскорбительным или нарушает правила поведения.' long_form: 'отметить как неуместное' notify_user: title: 'Отправить @{{username}} личное сообщение' @@ -575,7 +574,6 @@ ru: long_form: 'отмечено как спам' inappropriate: title: 'Неуместно' - description: 'Эта тема может быть сочтена оскорбительной или нарушает правила поведения на сайте.' long_form: 'отмеченно как неуместное' notify_moderators: title: "Другое" @@ -756,10 +754,6 @@ ru: host_names_warning: "Ваш файл config/database.yml использует локальное имя хоста по умолчанию. Поменяйте его на имя хоста вашего сайта." sidekiq_warning: 'Sidekiq не запущен. Сейчас многие задачи, такие как отправка электронных писем, выполняются асинхронно. Пожалуйста, убедитесь, что хотя бы один процесс sidekiq запущен. Узнайте больше о Sidekiq здесь.' memory_warning: 'Общее количество памяти, используемое вашим сервером, составляет менее 1 GB. Рекомендовано использовать минимум 1 GB.' - google_oauth2_config_warning: 'Сервер позволяет регистрацию и вход на сайт с использованием учетной записи Google OAuth2 (enable_google_oauth2_logins), но id и секретные значения не заданы. Пройдите в раздел Настройки сайта и обновите настройки. Ознакомьтесь с данным руководством для получения дополнительной информации.' - facebook_config_warning: 'Сервер позволяет регистрацию и вход на сайт с использованием учетной записи Facebook (enable_facebook_logins), но id и секретные значения не заданы. Пройдите в раздел Настройки сайта и обновите настройки. Ознакомьтесь с данным руководством для получения дополнительной информации.' - twitter_config_warning: 'Сервер позволяет регистрацию и вход на сайт с использованием учетной записи Twitter (enable_twitter_logins), но id и секретные значения не заданы. Пройдите в раздел Настройки сайта и обновите настройки. Ознакомьтесь с данным руководством для получения дополнительной информации.' - github_config_warning: 'Сервер позволяет регистрацию и вход на сайт с использованием учетной записи GitHub (enable_github_logins), но id и секретные значения не заданы. Пройдите в раздел Настройки сайта и обновите настройки. Ознакомьтесь с данным руководством для получения дополнительной информации.' site_settings: censored_words: "Слова, которые будут автоматически заменены на ■■■■" delete_old_hidden_posts: "Автоматически удалять сообщения, скрытые дольше чем 30 дней." @@ -841,9 +835,7 @@ ru: min_password_length: "Минимальная длина пароля" min_admin_password_length: "Минимальная длина пароля для Администратора." block_common_passwords: "Не позволять использовать пароли из списка 10 000 самых частоиспользуемых паролей." - enable_sso_provider: "Реализация протокола SSO провайдера через /session/sso_provider , необходимо установить sso_secret" sso_secret: "Секретный набор символов, используемый для проверки подлинности зашиврованного входа с помощью SSO, убедитесь, что это 10 или более символов" - sso_overrides_avatar: "Использовать аватары предоставляемые внешним SSO провайдером. При этом настоятельно рекомендуется отключить функцию: разрешить закгрузку аватаров." sso_not_approved_url: "Перенаправлять неподтвержденные SSO-аккаунты на этот URL" allow_new_registrations: "Разрешить регистрацию новых пользователей. Выключите, чтобы запретить посетителям создавать новые учетные записи." enable_yahoo_logins: "Разрешить идентификацию с Yahoo" @@ -852,7 +844,6 @@ ru: readonly_mode_during_backup: "Включить режим \"только для чтения\" во время выполнения резервного копирования" allow_restore: "Позволить импорт, который может заменить ВСЕ данные сайта. Оставьте выключенным, если не планируете восстанавливать резервную копию" maximum_backups: "Максимальное количество резервных копий к сохранению. Более старые резервные копии будут автоматически удалены." - enable_s3_backups: "Загружать резервные копии на S3 по завершению. Убедитесь, что настройки S3 заполнены." s3_backup_bucket: "Адрес папки удаленного сервера для резервных копий. ВНИМАНИЕ: Убедитесь, что место назначения защищено от посторонних." active_user_rate_limit_secs: "Как часто мы обновляем поле 'last_seen_at', в секундах" verbose_localization: "Показывать ключи используемых строк в интерфейсе для перевода на другой язык" @@ -1320,7 +1311,6 @@ ru: search_button: "Поиск" terms_of_service: title: "Условия предоставления услуг" - signup_form_message: 'Я прочитал(а) и согласен(а) с пользовательским соглашением.' deleted: 'удалено' upload: edit_reason: "загружены и сохранены локально использованные с других сайтов картинки" @@ -1600,9 +1590,6 @@ ru: title: "Пригласить как персонал" finished: title: "Ваш Discourse готов!" - description: | -

    Если вы когда-либо захотите изменить эти настройки, зайдите в Вашу админку; ссылка находится напротив иконки гаечного ключа в меню сайта.

    -

    Удачи в создании Вашего нового сообщества!

    staff_action_logs: not_found: "не найдено" unknown: "неизвестно" diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index a72795a3fd..cb5a2ad177 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -516,7 +516,6 @@ sk: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Nevhodné' - description: 'Obsah tohto príspevku môže byť nektorými osobami považovaný za urážlivý, hanlivý, alebo porušujúci pravidlá slušného správania.' long_form: 'toto označ ako nevhodné' notify_user: title: 'Poslať @{{username}} správu' @@ -547,11 +546,9 @@ sk: long_form: 'označiť toto ako spam' inappropriate: title: 'Nevhodné' - description: 'Táto téma môže byť nektorými osobami považovaná za urážlivú, hanlivú, alebo porušujúcu pravidlá slušného správania.' long_form: 'toto označ ako nevhodné' notify_moderators: title: "Niečo iné" - description: 'Táto téma vyžaduje pozornosť obsluhy na základe pravidiel, TOS, alebo z iného dôvodu ako je uvedené vyššie.' long_form: 'označené do pozornosti moderátora' email_title: 'Téma "%{title}" vyžaduje pozornosť moderátora' email_body: "%{link}\n\n%{message}" @@ -715,11 +712,6 @@ sk: sidekiq_warning: 'Sidekiq neni spustený. Množstvo úloh, ako napríklad posielanie emailov, je vykonávaných asynchrónne prostrednictvom sidekiq. Prosim zabezpečte aby bol spustený aspoň jeden proces sidekiq Viac o Sidekiq.' queue_size_warning: 'Počet úloh v zásobníku je %{queue_size}, čo je dosť veľa. To môže byť príznakom problému s procesom (procesmi) Sidekiq, alebo by ste mali spustiť viac Sidekiq procesov.' memory_warning: 'Váš server beží s menej než 1 GB pamäte. Odporúča sa minimálne 1GB. ' - google_oauth2_config_warning: 'Server má nakonfigurovanú podporu registrácie a prihlásenia pomocou Google OAuth2 (enable_google_oauth2_logins), ale identifikačné údaje klienta a jeho tajné hodnoty nie sú nastavené. Navštívte Nastavenia stránky a aktualizujte nastavenia. Chcete vedieť viac? Pozrite si tento návod.' - facebook_config_warning: 'Server má nakonfigurovanú podporu registrácie a prihlásenia pomocou Facebooku (enable_facebook_logins), ale údaje "app Id" a tajné hodnoty nie sú nastavené. Navštívte Nastavenia stránky a aktualizujte nastavenia. Chcete vedieť viac? Pozrite si tento návod.' - twitter_config_warning: 'Server má nakonfigurovanú podporu registrácie a prihlásenia pomocou Twitteru (enable_twitter_logins), ale kľúč a tajné hodnoty nie sú nastavené. Navštívte Nastavenia stránky a aktualizujte nastavenia. Chcete vedieť viac? Pozrite si tento návod.' - github_config_warning: 'Server má nakonfigurovanú podporu registrácie a prihlásenia pomocou GitHub (enable_github_logins), ale identifikačné údaje klienta a jeho tajné hodnoty nie sú nastavené.. Navštívte Nastavenia stránky a aktualizujte nastavenia.Chcete vedieť viac? Pozrite si tento návod.' - failing_emails_warning: 'Existuje %{num_failed_jobs} neuspešných emailých pokusov. Skontrolujte Váš app.yml a uistite sa, že nastavenia email serveru máte správne. Pozrieť neúspešné pokusy v Sidekiq.' subfolder_ends_in_slash: "Vaše nastavenie podadresára je chybné, DISCOURSE_RELATIVE_URL_ROOT je ukončené lomítkom." site_settings: censored_words: "Slová, ktoré budu automatický nahradené znakmi ■■■■" @@ -806,12 +798,10 @@ sk: min_admin_password_length: "Minimálna dĺžka hesla pre Administrátora." block_common_passwords: "Nepovoliť heslá, ktoré sú v zozname 10 000 najbežnejších hesiel." enable_sso: "Povolit prihlasovanie pomocou externej stránky (VAROVANIE: POUŽÍVATEĽSKÉ EAMILOVÉ ADRESY *MUSIA* BYŤ OVERENÉ EXTERNOU STRÁNKOU)" - enable_sso_provider: "Implementácia protokolu Discourse SSO poskytovateľa na koncovom bode /session/sso_provider, vyžaduje nastavenie sso_secret" sso_secret: "Tajný text, použitý na kryptografické overenie SSO informácie, uistite sa, že má 10 alebo viac znakov" sso_overrides_email: "Nahrádzať lokálne emaily pomocou emailov z externej stránky pomocou dát z SSO dotazu pri každom logine a zamedziť lokálnym zmenám. (VAROVANIE: z dôvodu normálizácie lokálnych emailov sa môžu objaviť nezrovnalosti)" sso_overrides_username: "Nahrádzať lokálne použivateľské mená pomocou použivateľských mien z externej stránky pomocou dát z SSO dotazu pri každom logine a zamedziť lokálnym zmenám. (VAROVANIE: z dôvodu rozdielov v požiadavkách na používateľské meno a jeho dĺžku sa môžu objaviť nezrovnalosti)" sso_overrides_name: "Nahrádzať lokálne plné mená pomocou plných mien z externej stránky pomocou dát z SSO dotazu pri každom logine a zamedziť lokálnym zmenám." - sso_overrides_avatar: "Nahrádzať používateľský avatar pomocou avatara z externej stránky pomocou dát z SSO dotazu. V prípade zapnutia veľmi doporučujeme vypnúť allow_uploaded_avatars" sso_not_approved_url: "Presmeruj nepovolené SSO účty na URL" allow_new_registrations: "Povoliť registráciu nových používateľov. Odznačte to ak chcete zabrániť vytváraniu nových účtov. " enable_signup_cta: "Zobraz oznámenie pre navrátilých anonymných používateľov s výzvou na registráciu účtu. " @@ -822,7 +812,6 @@ sk: allow_restore: "Umožniť obnovu, ktorá nahradí VŠETKY data na stránkach! Ponechajte prázdne okrem prípadu ak chcete obnoviť zo zálohy." maximum_backups: "Maximálny počet záloh udržiavaných na disku. Staršie zálohy budu automaticky vymazané" automatic_backups_enabled: "Spustiť automatické zálohy podľa nastavenia intervalu záloh" - enable_s3_backups: "Po dokončení nahrať zálohy na S3. DÔLEŽITÉ: požaduje správne nastavenie prístupových údajov na S3 v menu Súbory." s3_backup_bucket: "Vzdialený S3 bucket na uloženie záloh. VAROVANIE: Uistite sa, že ide o súkromný S3 bucket." backup_time_of_day: "Čas v UTC, kedy sa má spustiť záloha." backup_with_uploads: "Do pravidelných záloh ulož i nahrané súbory. Pri vypnutej voľbe sa uloží iba záloha databázy." @@ -1248,7 +1237,6 @@ sk: search_title: "Prehľadať tieto stránky" terms_of_service: title: "Podmienky používania" - signup_form_message: 'Prečítal som si Podmienky používania a súhlasím s nimi.' deleted: 'vymazané' upload: edit_reason: "stiahnuté lokálne kópie obrázkov" diff --git a/config/locales/server.sl.yml b/config/locales/server.sl.yml index f174379051..8932a7f26b 100644 --- a/config/locales/server.sl.yml +++ b/config/locales/server.sl.yml @@ -85,4 +85,7 @@ sl: anniversary: name: Obletnica long_description: | - Ta značka je podeljeno, ko ste bili član eno leto in ste v tem letu naredili vsaj eno objavo. Hvala, da ste ostali in prispevali k naši skupnosti. Brez vas ne bi zmogli. + Ta značka je podeljena, ko ste bili član eno leto in ste v tem letu naredili vsaj eno objavo. Hvala, da ste ostali in prispevali k naši skupnosti. Brez vas ne bi zmogli. + aficionado: + long_description: | + Ta značka je podeljena za obiskovanje 100 zaporednih dni. To je več kot tri mesece! diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index e45e97ba45..bba30c1de9 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -305,7 +305,6 @@ sq: email_body: "%{link}\n\n%{message}" inappropriate: title: 'i papërshtatshëm' - description: 'Ky postim përmban pjesë që një person i arsyeshëm do ta quante ofenduese, fyese, ose kundër udhëzimeve të komunitetit tonë.' long_form: 'sinjalizoi këtë postim si të papërshtatshëm' notify_user: title: 'Dërgoji @{{username}} një mesazh' @@ -331,7 +330,6 @@ sq: long_form: 'sinjalizoi këtë postim si spam' inappropriate: title: 'I papërshtatshëm' - description: 'Ky postim përmban pjesë që një person i arsyeshëm do ta quante ofenduese, fyese, ose kundër udhëzimeve të komunitetit tonë.' long_form: 'shënoje si të papërshtatshme' notify_moderators: title: "Diçka Tjetër" @@ -492,10 +490,6 @@ sq: sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by sidekiq. Please ensure at least one sidekiq process is running. Learn about Sidekiq here.' queue_size_warning: 'The number of queued jobs is %{queue_size}, which is high. This could indicate a problem with the Sidekiq process(es), or you may need to add more Sidekiq workers.' memory_warning: 'Your server is running with less than 1 GB of total memory. At least 1 GB of memory is recommended.' - google_oauth2_config_warning: 'The server is configured to allow signup and log in with Google OAuth2 (enable_google_oauth2_logins), but the client id and client secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - facebook_config_warning: 'The server is configured to allow signup and log in with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' site_settings: censored_words: "Fjalët do të zhvendosen automatikisht me ■■■■" delete_old_hidden_posts: "Auto-delete any hidden posts that stay hidden for more than 30 days." @@ -576,12 +570,10 @@ sq: min_password_length: "Minimum password length." block_common_passwords: "Mos lejo fjalëkalimet që gjenden në 10,000 fjalëkalimet më të përdorshme." enable_sso: "Enable single sign on via an external site (WARNING: USERS' EMAIL ADDRESSES *MUST* BE VALIDATED BY THE EXTERNAL SITE!)" - enable_sso_provider: "Implement Discourse SSO provider protocol at the /session/sso_provider endpoint, requires sso_secret to be set" sso_secret: "Secret string used to cryptographically authenticate SSO information, be sure it is 10 characters or longer" sso_overrides_email: "Overrides local email with external site email from SSO payload on every login, and prevent local changes. (WARNING: discrepancies can occur due to normalization of local emails)" sso_overrides_username: "Overrides local username with external site username from SSO payload on every login, and prevent local changes. (WARNING: discrepancies can occur due to differences in username length/requirements)" sso_overrides_name: "Overrides local full name with external site full name from SSO payload on every login, and prevent local changes." - sso_overrides_avatar: "Overrides user avatar with external site avatar from SSO payload. If enabled, disabling allow_uploaded_avatars is highly recommended" sso_not_approved_url: "Redirect unapproved SSO accounts to this URL" allow_new_registrations: "Allow new user registrations. Uncheck this to prevent anyone from creating a new account." enable_yahoo_logins: "Enable Yahoo authentication" @@ -589,7 +581,6 @@ sq: google_oauth2_client_secret: "Client secret of your Google application." allow_restore: "Allow restore, which can replace ALL site data! Leave false unless you plan to restore a backup" maximum_backups: "The maximum amount of backups to keep on disk. Older backups are automatically deleted" - enable_s3_backups: "Upload backups to S3 when complete. IMPORTANT: requires valid S3 credentials entered in Files settings." s3_backup_bucket: "The remote bucket to hold backups. WARNING: Make sure it is a private bucket." active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds" verbose_localization: "Show extended localization tips in the UI" @@ -1069,7 +1060,6 @@ sq: search_title: "Kërko në këtë faqe" terms_of_service: title: "Kushtet e shërbimit" - signup_form_message: 'I have read and accept the Terms of Service.' deleted: 'deleted' upload: edit_reason: "downloaded local copies of images" diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 507c31a7aa..8ad0052847 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -442,7 +442,6 @@ sv: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Olämpligt' - description: 'Detta inläggs innehåll inkluderar saker som en förnuftig person skulle anse vara stötande, kränkande eller en överträdelse av våra riktlinjer.' long_form: 'flagga detta som olämpligt' notify_user: title: 'Skicka ett meddelande till @{{username}} ' @@ -476,11 +475,9 @@ sv: long_form: 'flaggad som spam' inappropriate: title: 'Olämpligt' - description: 'Detta ämne har innehåll som en förnuftig person skulle anse vara stötande, kränkande eller en överträdelse av våra riktlinjer.' long_form: 'flaggad som olämplig' notify_moderators: title: "Annat" - description: 'Det här inlägget kräver generell uppmärksamhet från personalen baserat på riktlinjerna, användarvillkoren eller på grund av en annan anledning som inte finns med ovan.' long_form: 'flaggade det här för granskning av moderator' email_title: 'Ämnet "%{title}" kräver moderators uppmärksamhet' email_body: "%{link}\n\n%{message}" @@ -661,11 +658,6 @@ sv: sidekiq_warning: 'Sidekiq körs inte. Många uppgifter, sm att skicka e-post, utförs asynkront av sidekiq. Var vänlig se till att minst en sidekiqprocess körs. Läs mer om Sidekiq här.' queue_size_warning: 'Antal köade jobb är %{queue_size}, vilket är ganska högt. Det kan indikera ett problem med Sidekiq process(er), eller så kanske du behöver lägga till fler Sidekiqarbetare.' memory_warning: 'Din server körs med mindre än 1 GB minne. Minne på minst 1 GB är rekommenderat.' - google_oauth2_config_warning: 'Servern är konfigurerad till att tillåta registrering och inloggning med Google OAuth2 (inställningen enable_google_oauth2). men klient-id och klienthemlighetsvärden är inte satta. Gå till Webbplatsinställningarna och uppdatera inställningarna. Läs den här guiden för att lära dig mer.' - facebook_config_warning: 'Servern är konfigurerad till att tillåta registrering och inloggning med Facebook (inställningen enable_facebook_logins). men app-id och appens hemlighetsvärden är inte satta. Gå till Webbplatsinställningarna och uppdatera inställningarna. Läs den här guiden för att lära dig mer.' - twitter_config_warning: 'Servern är konfigurerad till att tillåta registrering och inloggning med Twitter (inställningen twitter_logins). men nyckel- och hemlighetsvärden är inte satta. Gå till Webbplatsinställningarna och uppdatera inställningarna. Läs den här guiden för att lära dig mer.' - github_config_warning: 'Servern är konfigurerad till att tillåta registrering och inloggning med GitHub (inställningen twitter_logins). men klient-id och hemlighetsvärden är inte satta. Gå till Webbplatsinställningarna och uppdatera inställningarna. Läs den här guiden för att lära dig mer.' - failing_emails_warning: 'Det finns %{num_failed_jobs} e-postutskick som har misslyckats. Kontrollera din app.yml-fil för att säkerställa att serverinställningarna för e-post är korrekta. Se alla misslyckade utskick i Sidekiq.' subfolder_ends_in_slash: "Inställningarna för dina undermappar är inte korrekt; DISCOURSE_RELATIV_URL_ROOT slutar med ett snedstreck." email_polling_errored_recently: one: "E-postpolling har genererat ett fel de senaste 24 timmarna. Se loggarna för mer detaljer." @@ -778,14 +770,12 @@ sv: password_unique_characters: "Minsta antalet unika tecken som ett lösenord måste ha." block_common_passwords: "Tillåt inte lösenord som är bland de 10000 vanligaste lösenorden." enable_sso: "Aktivera Single sign on via en extern sida (VARNING: ANVÄNDARENS E-POSTADRESS *MÅSTE* VARA VALIDERAD AV DEN EXTERNA SIDAN!)" - enable_sso_provider: "Implementera Discourse SSO leverantörsprotokoll vid /session/sso_provider endpoint, kräver att sso_secret är inställd" sso_url: "URL för single sign on endpoint (måste inkludera http:// eller https://)" sso_secret: "Hemlig sträng som används för att kryptografiskt autentisera SSO-information, se till att den är 10 tecken eller mer" sso_overrides_bio: "Åsidosätter användar bio i användarprofil och förhindrar användare från att ändra det" sso_overrides_email: "Åsidosätter lokal e-post med externa webbplatsers e-post från SSO-nyttolast vid varje inloggning, och förhindrar lokala ändringar (VARNING: diskrepanser kan inträffa på grund av normalisering av lokala e-poster)" sso_overrides_username: "Åsidosätter lokala användarnamn med externa webbplatsers användarnamn från SSO-nyttolast vid varje inloggning, och förhindrar lokala ändringar. (VARNING: diskrepanser kan inträffa på grund av skillnader i användarnamnets längd/krav)" sso_overrides_name: "Åsidosätter lokala fullständiga namn med externa webbplatsers fullständiga namn från SSO-nyttolast vid varje inloggning, och förhindrar lokala ändringar." - sso_overrides_avatar: "Åsidosätter användares avatarer med externa webbplatsers avatarer från SSO-nyttolast. Om aktiverad är inaktivering av allow_uploaded_avatars högt rekommenderat" sso_not_approved_url: "Omdirigera icke godkända SSO-konton till den här URL:en" allow_new_registrations: "Tillåt nya användarregistreringar. Avbocka det här för att förhindra vem som helst från att skapa ett nytt konto." enable_signup_cta: "Visa en notis för återvändande anonyma användare för att förmå dem att registrera ett nytt konto." @@ -799,7 +789,6 @@ sv: allow_restore: "Tillåt återställning, vilket kan ersätta ALL data på webbplatsen! Lämna avbockad om du inte planerar att återställa en säkerhetskopia" maximum_backups: "Högsta antal säkerhetskopior att spara på disken. Äldre säkerhetskopior raderas automatiskt" automatic_backups_enabled: "Kör automatiska säkerhetskopior som definierats i säkerhetskopieringsfrekvens" - enable_s3_backups: "Ladda upp säkerhetskopior till S3 vid färdigställning. VIKTIGT: kräver giltiga S3-kreditiv inlagda i Webbplatsinställningar > Filer." s3_backup_bucket: "Den externa behållaren för säkerhetskopieringar. VARNING: Se till att det här en privat behållare." s3_disable_cleanup: "Inaktivera borttagande av säkerhetskopior från S3 när de tagits bort lokalt." backup_time_of_day: "Tid på dygnet UTC då säkerhetskopieringen sker." @@ -1548,7 +1537,6 @@ sv: search_title: "Sök på hemsidan" terms_of_service: title: "Användarvillkor" - signup_form_message: 'Jag har läst och accepterat Användarvillkoren.' deleted: 'raderad' upload: edit_reason: "nedladdade lokala kopior av bilder" diff --git a/config/locales/server.sw.yml b/config/locales/server.sw.yml index a863af66ae..420bbf4a7b 100644 --- a/config/locales/server.sw.yml +++ b/config/locales/server.sw.yml @@ -463,8 +463,6 @@ sw: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Haifai' - description: 'Chapisho hili lina maandishi ambayo mtu mwenye akili timamu anaweza kuona ni matusi, ubaguzi, au kukiuka mwongozo wa jumuiya.' - short_description: 'Ukiukaji wa miongozo ya jukwaa letu' long_form: 'imeripotiwa kuwa haiko sawa' notify_user: title: 'Mtumie @{{username}} ujumbe' @@ -509,11 +507,9 @@ sw: long_form: 'imeripotiwa kama barua taka' inappropriate: title: 'Haifai' - description: 'Mada hii ina maandishi ambayo mtu mwenye akili timamu anaweza kuona ni matusi, ubaguzi, au kukiuka mwongozo wa jumuiya.' long_form: 'imeripotiwa kuwa haiko sawa' notify_moderators: title: "Kitu Kingine" - description: 'Mada hii inahitaji kupitiwa na msaidizi kwa sababu za mwongozo, TOS, au kwa sababu nyingine ambayo haijaorodheshwa.' long_form: 'imeripotiwa na itapitiwa na msimamizi' email_title: 'Mada "%{title}" inahitaji kupitiwa na msimamizi' email_body: "%{link}\n\n%{message}" @@ -715,11 +711,6 @@ sw: sidekiq_warning: 'Sidekiq haifanyi kazi. Shughuli nyingi kama kutuma barua pepe, zinashughulikiwa na sidekiq. Tafadhali hakikisha kuwa mfumo wa sidekiq unafanya kazi. Jifunza kuhusu Sidekiq hapa.' queue_size_warning: 'Namba za kazi zilizopangwa ni %{queue_size}, ambazo ni nyingi. Hii inaweza kusababishwa na tatizo na m(i)fumo wa Sidekiq, au inabidi uongeze wafanyakazi wa Sidekiq.' memory_warning: 'Seva yako inatumia chini ya GB 1 ya kumbukumbu. Tunakushauri utumie kumbukumbu zaidi ya GB 1.' - google_oauth2_config_warning: 'Seva inaruhusu watu kujiunga au kuingia kwa kutumia Google OAuth2 (enable_google_oauth2_logins), lakini taarifa za client id and client secret hazijaandikwa. Nenda kwenye Mipangilio ya Tovuti na sasisha mipangilio. Tembelea mwongozo kwa taarifa zaidi.' - facebook_config_warning: 'Seva inaruhusu watu kujiunga au kuingia kwa kutumia Facebook (enable_facebook_logins), lakini taarifa za client id and client secret hazijaandikwa. Nenda kwenye Mipangilio ya Tovuti na sasisha mipangilio. Tembelea mwongozo kwa taarifa zaidi.' - twitter_config_warning: 'Seva inaruhusu watu kujiunga au kuingia kwa kutumia Twitter (enable_twitter_logins), lakini taarifa za client id and client secret hazijaandikwa. Nenda kwenye Mipangilio ya Tovuti na sasisha mipangilio. Tembelea mwongozo kwa taarifa zaidi.' - github_config_warning: 'Seva inaruhusu watu kujiunga au kuingia kwa kutumia Github (enable_github_logins), lakini taarifa za client id and client secret hazijaandikwa. Nenda kwenye Mipangilio ya Tovuti na sasisha mipangilio. Tembelea mwongozo kwa taarifa zaidi.' - failing_emails_warning: 'Kuna kazi %{num_failed_jobs} za barua pepe ambazo zimeshindwa. Angalia file la app.yml na hakikisha mipangilio ya seva za barua ziko sawa. Ona kazi zilizoshindwa ndani ya Sidekiq.' missing_mailgun_api_key: "Seva imesanidiwa kutuma barua pepe kwa kutumia Mailgun lakini haujaweka ufunguo wa Mailgun unaotumika kuthibitisha ujumbe." bad_favicon_url: "Ishara unayoipenda imeshindwa kuonekana. Angalia mipangilio ya favicon_url ndani ya Mipangilio ya Tovuti." force_https_warning: "Tovuti yako inatumia SSL. Lakini `force_https` haijaruhusiwa kwenye mipangilio ya tovuti yako." @@ -1498,14 +1489,6 @@ sw: spam_post_blocked: title: "Chapisho Taka Limezuiliwa" subject_template: "Machapisho ya mtumiaji mpya %{username} yamezuiliwa kwa sababu ya viungo vinavyojirudia" - text_body_template: | - Huu ni ujumbe kutoka kwa roboti. - - Mtumiaji mpya [%{username}](%{user_url}) amejaribu kuandika maandishi mapya mengi yenye viungo kwenda kwa %{domains}, lakini zimezuiliwa kukwepa barua taka. Bado mtumiaji anaweza kutengeneza machapisho ambayo hayana viungo kwenda kwa %{domains}. - - Tafadhali [kagua mtumiaji](%{user_url}). - - Tohoa kwa kupitia `newuser_spam_host_threshold` na `white_listed_spam_host_domains` mipangilio ya tovuti. unsilenced: title: "Hajanyamazishwa Tena" subject_template: "Akaunti haijasimamishwa tena" @@ -2328,9 +2311,6 @@ sw: description: "Una karibia kumaliza! Karibisha watu kadhaa kusaidia kuanzisha majadilianona mada nzuri na majibu kufanya jumuiya yako ianze." finished: title: "Discourse yako iko Tayari!" - description: | -

    Kama unataka kubadilisha mipangilio hii, tembelea kifungu cha kiongozi; utaiona pembeni ya ikoni ya spana kwenye menyu ya tovuti.

    -

    Kila la kherikwenye ujengaji wa jumuiya yako mpya!

    search_logs: graph_title: "Idadi ya Utafiti" joined: "Alijiunga" diff --git a/config/locales/server.te.yml b/config/locales/server.te.yml index 76006e6560..a7f2287237 100644 --- a/config/locales/server.te.yml +++ b/config/locales/server.te.yml @@ -295,7 +295,6 @@ te: email_body: "%{link}\n\n%{message}" inappropriate: title: 'అసమంజసమైనది' - description: 'ఈ టపాలో విషయం కొంతమందికి అభ్యంతరకరమైనది, అగౌరవపరిచేది లేదా మా కమ్యునిటీ మార్గదర్శకాలకు లోబడినది కాదు.' long_form: 'దీన్ని అసమంజసమైనదిగా కేతనించాము' notify_user: email_title: '"%{title}" లో మీ టపా' @@ -318,7 +317,6 @@ te: long_form: 'దీన్ని స్పాముగా కేతనించారు' inappropriate: title: 'అసమంజసం' - description: 'ఈ అంశంలో ఉన్న విషయం ద్వారా ఒక సహేతుకమైన వ్యక్తిని ప్రమాదకరమైన,అసంబధ్ధమైనవానిగా పరిగణిస్తారు,లేదా మన వర్గ మార్గదర్శకాల ఉల్లంఘన జరుగుతుంది.' long_form: 'దీన్ని అసమంజసమైనదిగా కేతనించారు' notify_moderators: title: "వేరే ఏదో" @@ -532,7 +530,6 @@ te: search_title: "ఈ సైట్ వెదుకు" terms_of_service: title: "సేవా నియమాలు" - signup_form_message: 'నేను సేవా నియమాలను చదివాను, వాటికి అంగీకరిస్తున్నాను.' deleted: 'తొలగించారు' upload: images: diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 33e7e164f2..63ac48ebda 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -409,7 +409,6 @@ tr_TR: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Uygunsuz' - description: 'Bu gönderi saldırgan, kötüleyici ya da topluluk yönergelerini ihlal eden içerik barındırmaktadır. ' long_form: 'bunu uygunsuz olarak bildirdi' notify_user: title: '@{{username}} adlı kullanıcıya ileti gönder' @@ -438,11 +437,9 @@ tr_TR: long_form: 'bunu istenmeyen olarak bildirdi' inappropriate: title: 'Uygunsuz' - description: 'Bu konu saldırgan, kötüleyici ya da topluluk yönergelerini ihlal eden içerik barındırmaktadır. ' long_form: 'bunu uygunsuz olarak bildirdi' notify_moderators: title: "Başka Bir Şey" - description: 'Bu gönderi yönergeler, hizmet şartları veya başka herhangi bir nedenden dolayı görevli incelemesi gerektirmektedir.' long_form: 'bunu moderatörün ilgisi için bildirdi' email_title: '"%{title}" konu başlığının moderatör tarafından kontrol edilmesi gerekiyor' email_body: "%{link}\n\n%{message}" @@ -612,11 +609,6 @@ tr_TR: sidekiq_warning: 'Sidekiq çalışmıyor. E-posta yollamak gibi gibi birçok asenkron görev sidekiq''in işidir. En az bir tane sidekiq süreci çalıştırdığınızdan emin olun. Sidekiq ile ilgili bilgi burada.' queue_size_warning: 'Kuyruğa eklenmiş işlerin sayısı fazla: %{queue_size}. Bu Sidekiq işlem(ler)indeki bir sorunu işaret ediyor olabilir, ya da daha fazla Sidekiq işçisi eklemeniz gerekiyor olabilir.' memory_warning: 'Sunucunuz toplam 1GB''tan az bellek ile çalışıyor. En az 1GB bellek tavsiye edilmektedir.' - google_oauth2_config_warning: 'Sunucu Google OAuth2 (enable_google_oauth2_logins) ile üyelik oluşturulması ve giriş yapılmasına elveriyor, fakat the kullanıcı IDsi and gizli kullanıcı değerleri henüz ayarlanmamış. Site Ayarları sayfasına gidin ve ayarları güncelleyin. Daha fazla bilgi için bu yönetmeliğe bakın.' - facebook_config_warning: 'Sunucu Facebook (enable_facebook_logins) ile üyelik oluşturulması ve giriş yapılmasına izin veriyor, fakat app ID ve gizli app değerleri henüz ayarlanmamış. Site Ayarları sayfasına gidin ve ayarları güncelleyin. Daha fazla bilgi için bu yönetmeliğe bakın.' - twitter_config_warning: 'Sunucu Twitter (enable_twitter_logins) ile üyelik oluşturulması ve giriş yapılmasına izin veriyor, fakat anahtar ve gizli değerler henüz ayarlanmamış. Site Ayarları sayfasına gidin ve ayarları güncelleyin. Daha fazla bilgi için bu yönetmeliğe bakın.' - github_config_warning: 'Sunucu GitHub (enable_github_logins) ile üyelik oluşturulması ve giriş yapılmasına izin veriyor, fakat kullanıcı IDsi ve gizli değerler henüz ayarlanmamış. Site Ayarları sayfasına gidin ayarları güncelleyin. Daha fazla bilgi için bu yönetmeliğe bakın.' - failing_emails_warning: 'Başarısızlıkla sonuçlanmış %{num_failed_jobs} e-posta işlemi bulunuyor. app.yml dosyanızı kontrol edin ve e-posta sunucu ayarlarınızın doğru olduğundan emin olun. Sidekiq''deki başarısız işlemlere göz atın.' subfolder_ends_in_slash: "Alt dizin kurulumunuz hatalı, DISCOURSE_RELATIVE_URL_ROOT sonunda yan çizgi bulunmalı." bad_favicon_url: "Minik simge yüklemesi başarısız oldu. Site Ayarlarından favicon_url alanını kontrol edin." site_settings: @@ -717,12 +709,10 @@ tr_TR: min_admin_password_length: "Yönetici için parolanın en az uzunluğu." block_common_passwords: "En çok kullanılan 10,000 parola arasında yer alan parolalara izin verme." enable_sso: "Dış bir site aracılığı ile tek oturum açma sistemini etkinleştir. (UYARI: etkinleştirildiği takdirde, doğru yapılandırılmamışsa bazı kişilerin, SİZ DAHİL, oturum açmasını engelleyebilir! Ayrıca davetiye sistemini de devre dışı bırakır.)" - enable_sso_provider: "/session/sso_provider son noktasında Discourse SSO sağlayıcı protokolünü uygula, sso_secret değerinin seçilmiş olmasını gerektirir" sso_secret: "SSO bilgisinin kritopgrafik şekilde doğrulanması için kullanılan gizli string, 10 karakter veya daha uzun olduğundan emin olun" sso_overrides_email: "SSO yararlı yükündeki dış sitedeki e-postayı, her giriş yapıldığında, yerel değişiklikleri engellemek için yerel e-postanın üzerine yazar (DİKKAT: yerel e-postaların olağanlaştırma sürecinde uyuşmazlıklar doğabilir)" sso_overrides_username: "SSO yararlı yükündeki dış sitedeki kullanıcı adını, her giriş yapıldığında, yerel değişiklikleri engellemek için yerel kullanıcı adının üzerine yazar. (DİKKAT: kullanıcı adı uzunluklarıyla ilgili kurallardaki farklılıklardan ötürü uyuşmazlıklar doğabilir)" sso_overrides_name: "SSO yararlı yükündeki dış sitedeki tam adı, her giriş yapıldığında, yerel değişiklikleri engellemek için yerel tam adın üzerine yazar." - sso_overrides_avatar: "SSO yararlı yükündeki dış site avatarını kullanıcı avatarının üzerine yazar Eğer etkinleştirildiyse, allow_uploaded_avatars ayarının devre dışı bırakılması şiddetle önerilir" sso_not_approved_url: "Bu bağlantıya onaylanmamış SSO hesaplarını yönlendir." allow_new_registrations: "Yeni kayıtlara izin ver. Yeni hesap oluşturulmasını engellemek için burayı işaretlemeyin." enable_signup_cta: "Geri dönen anonim kullanıcılara hesap oluşturmaları için bir uyarı göster." @@ -734,7 +724,6 @@ tr_TR: allow_restore: "Geri almaya izin ver. Sitedeki TÜM verileri değiştirebilir! Bir yedeklemeyi geri yüklemeyi planlamıyorsanız devre dışı bırakın." maximum_backups: "Diskte tutulacak en fazla yedek sayısı. Eski yedekler otomatik olarak silinir." automatic_backups_enabled: "Yedek sıklığında tanımlandığı gibi otomatik yedek almayı çalıştır" - enable_s3_backups: "Tamamlanınca yedeklemeleri S3'e yükle. ÖNEMLİ: Dosyalar ayarında doğru S3 girilmesini gerektirir" s3_backup_bucket: "Yedeklemelerin yüklenmesi için uzak biriktirme yeri. UYARI: Özel bir biriktirme yeri olduğundan emin olun" s3_disable_cleanup: "Yerel olarak silinen yedeklerin S3 sunucularından silinmesini kapat" backup_time_of_day: "Yedeklemenin yapılacağı vaktin gün içindeki UTC zamanı." @@ -1334,7 +1323,6 @@ tr_TR: search_title: "Bu sitede ara" terms_of_service: title: "Üyelik Sözleşmesi" - signup_form_message: 'Üyelik Sözleşmesini okudum ve kabul ediyorum.' deleted: 'silindi' upload: edit_reason: "resimlerin yerel kopyaları indirildi" diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 41016048bf..eccc93119b 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -299,9 +299,6 @@ uk: host_names_warning: "Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname." sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by sidekiq. Please ensure at least one sidekiq process is running. Learn about Sidekiq here.' memory_warning: 'Your server is running with less than 1 GB of total memory. At least 1 GB of memory is recommended.' - facebook_config_warning: 'The server is configured to allow signup and log in with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' - github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.' site_settings: allow_user_locale: "Allow users to choose their own language interface preference" min_search_term_length: "Мінімальна дозволена довжина пошукової фрази у символах" @@ -413,7 +410,6 @@ uk: search_title: "Пошук на цьому сайті" terms_of_service: title: "Умови Використання" - signup_form_message: 'I have read and accept the Terms of Service.' deleted: 'deleted' upload: edit_reason: "завантажені локальні копії забражень" diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml index 1c5876c98e..e55303b553 100644 --- a/config/locales/server.ur.yml +++ b/config/locales/server.ur.yml @@ -611,8 +611,6 @@ ur: email_body: "%{link}\n\n%{message}" inappropriate: title: 'نامناسب' - description: 'اِس پوسٹ میں ایسا مواد شامل ہے جو ایک مناسب شخص جارحانہ، غیر مہذب، یا ہماری کمیونٹی کے قواعد و ضوابط کے خلاف سمجھے گا۔' - short_description: 'ہماری کمیونٹی کے قواعد و ضوابط کی خلاف ورزی' long_form: 'اِس کو نامناسب ہونے کے طور پر فلَیگ کیا گیا' notify_user: title: '@{{username}} کو ایک پیغام بھیجیں' @@ -657,11 +655,9 @@ ur: long_form: 'اِس کو سپَیم کے طور پر فلَیگ کیا گیا' inappropriate: title: 'نامناسب' - description: 'اِس ٹاپک میں ایسا مواد شامل ہے جو ایک مناسب شخص جارحانہ، غیر مہذب، یا ہماری کمیونٹی کے قواعد و ضوابط کے خلاف سمجھے گا۔' long_form: 'اِس کو نامناسب ہونے کے طور پر فلَیگ کیا گیا' notify_moderators: title: "کچھ اور" - description: 'اِس ٹاپک کو، قواعد و ضوابط، سروس کی شرائط، یا مندرجہ بالا درج وجوہات کے علاوہ، کے مطابق اسٹاف کی عام توجہ کی ضرورت ہے۔' long_form: 'اِس کو ماڈریٹر کی توجہ کیلئے فلَیگ کیا گیا' email_title: 'ٹاپک "%{title}" پر اسٹاف کی توجہ درکار ہے' email_body: "%{link}\n\n%{message}" @@ -850,11 +846,6 @@ ur: sidekiq_warning: 'Sidekiq نہیں چل رہا۔ بہت سے کام، جیسا کہ ای میل بھیجنا، sidekq کی طرف سے اےسِنکرونسلی مکمل کیے جاتے ہیں۔ براہ کرم یقینی بنائیں کہ کم ازکم ایک sidekq پراسیس چل رہا ہے۔ Sidekiq کے بارے میں یہاں سے جانیے۔' queue_size_warning: 'قطار میں موجود جابز کی تعداد %{queue_size} ہے، جو کہ زیادہ ہے۔ یہ Sidekiq پراسیس کے ساتھ ایک مسئلہ کی نشاندہی کر سکتا ہے، یا آپ کو مزید Sidekiq کارکنوں کو شامل کرنے کی ضرورت ہوسکتی ہے۔' memory_warning: 'آپ کا سرور مجموعی طور پر 1 GB سے کم میموری کے ساتھ چل رہا ہے۔ کم ازکم 1 GB میموری تجویز کی گئی ہے۔' - google_oauth2_config_warning: 'سرور کو ترتیب دیا گیا ہے کہ گُوگل (OAuth2 (enable_google_oauth2_logins کے ساتھ سائن اپ اور لاگ اِن کی اجازت ہو، لیکن کلائنٹ آئی ڈی اور کلائنٹ سیکرٹ وَیلِیوز مقرر نہیں کیے گئے ہیں۔ سائٹ ترتیبات پر جائیں اور ترتیبات کو اَپ ڈیٹ کریں۔ مزید جاننے کے لئے یہ گائیڈ ملاحظہ کریں۔' - facebook_config_warning: 'سرور کو ترتیب دیا گیا ہے کہ فیس بُک (OAuth2 (enable_facebook_logins کے ساتھ سائن اپ اور لاگ اِن کی اجازت ہو، لیکن اَیپ آئی ڈی اور اَیپ سیکرٹ وَیلِیوز مقرر نہیں کیے گئے ہیں۔ سائٹ ترتیبات پر جائیں اور ترتیبات کو اَپ ڈیٹ کریں۔ مزید جاننے کے لئے یہ گائیڈ ملاحظہ کریں۔' - twitter_config_warning: 'سرور کو ترتیب دیا گیا ہے کہ ٹَوِیٹر (enable_twitter_logins) کے ساتھ سائن اپ اور لاگ اِن کی اجازت ہو، لیکن قیی اور سیکرٹ وَیلِیوز مقرر نہیں کیے گئے ہیں۔ سائٹ ترتیبات پر جائیں اور ترتیبات کو اَپ ڈیٹ کریں۔ مزید جاننے کے لئے یہ گائیڈ ملاحظہ کریں۔' - github_config_warning: 'سرور کو ترتیب دیا گیا ہے کہ گِٹ ہَب (enable_github_logins) کے ساتھ سائن اپ اور لاگ اِن کی اجازت ہو، لیکن کلائنٹ آئی ڈی اور سیکرٹ وَیلِیوز مقرر نہیں کیے گئے ہیں۔ سائٹ ترتیبات پر جائیں اور ترتیبات کو اَپ ڈیٹ کریں۔ مزید جاننے کے لئے یہ گائیڈ ملاحظہ کریں۔' - failing_emails_warning: 'ناکام ہونے والی %{num_failed_jobs} اِی مَیل جابز موجود ہیں۔ اپنا app.yml چیک کریں اور یہ یقینی بنائیں کہ میل سرور کی ترتیبات درست ہیں۔ Sidekiq میں ناکام جابز ملاحظہ کریں۔' subfolder_ends_in_slash: "آپ کا سب-فولڈر سیٹ اپ غلط ہے؛ DISCOURSE_RELATIVE_URL_ROOT ایک سلَیش میں ختم ہوتا ہے۔" email_polling_errored_recently: one: "گزشتہ 24 گھنٹوں میں ای میل پولِنگ نے ایک خرابی دکھائی ہے۔ مزید تفصیلات کیلئے لاگز کا ملاحضہ کریں۔" @@ -1001,14 +992,12 @@ ur: password_unique_characters: "منفرد حروف کی کم از کم تعداد جو پاسورڈ میں ہونا لاذمی ہے۔" block_common_passwords: "ایسا پاسورڈ جو 10،000 سب سے زیادہ عام پاسورڈز میںشامل ہو، اۃسے رکھنے کی اجازت نہ دیں۔" enable_sso: "بیرونی سائٹ کے ذریعہ واحد سائن اَن کو فعال کریں (انتباہ: صارفین کے ای میل ایڈریس بیرونی سائٹ کی طرف سے توثیق کیے جانا *لازمی* ہے!)" - enable_sso_provider: "/session/sso_provider کے اینڈپوائنٹ پر ڈِسکورس SSO پرووَائیڈر پروٹوکول کو نافذ کریں، sso_secret کو مقرر کیا جانا ضروری ہے" sso_url: "URL واحد سائن اَن اینڈپوائنٹ کا (اhttp:// یا https:// کا شامل ہونا لاذمی ہے)" sso_secret: "خفیہ سٹرنگ جو کرِیپٹَوگرافی کے زریعہ SSO معلومات کی توثیق کرنے کیلئے استعمال کیا جاتا ہے، یقینی بنائیں کہ یہ 10 یا اُس سے زیادہ حروف لمبا ہے" sso_overrides_bio: "صارف پروفائل میں صارف کی بائیو کی جگہ لے لیتا ہے اور اِس کو تبدیل کرنے سے صارف کو روک دیتا ہے" sso_overrides_email: "ہر لاگ اِن پر SSO پے لوڈ سے بیرونی ویب سائٹ ای میل مقامی ای میل کی جگہ لے لیتا ہے، اور مقامی تبدیلیوں کو روک دیتا ہے۔ (انتباہ: مقامی ای میلز کو معمول پر لانے کی وجہ سے اختلافات ہوسکتے ہیں)" sso_overrides_username: "ہر لاگ اِن پر SSO پے لوڈ سے بیرونی ویب سائٹ صارف نام مقامی صارف نام کی جگہ لے لیتا ہے، اور مقامی تبدیلیوں کو روک دیتا ہے۔ (انتباہ: صارف نام کی لمبائی/ضروریات کے فرق کی وجہ سے اختلافات ہوسکتے ہیں)" sso_overrides_name: "ہر لاگ اِن پر SSO پے لوڈ سے بیرونی ویب سائٹ پورا نام مقامی پورا نام کی جگہ لے لیتا ہے، اور مقامی تبدیلیوں کو روک دیتا ہے۔" - sso_overrides_avatar: "SSO پے لوڈ سے بیرونی ویب سائٹ اوتار صارف اوتار کی جگہ لے لیتا ہے۔ اگر فعال ہو تو، allow_uploaded_avatars کو غیر فعال کر دینا تجویز کیا جاتا ہے" sso_not_approved_url: "غیر منظور شدہ SSO اکاؤنٹس کو اِس URL پر ریڈائرَیکٹ کریں" sso_allows_all_return_paths: "SSO کے ذریعہ فراہم کردہ return_paths کیلئے ڈَومین کو محدود نہ کریں (ڈِیفالٹ کے طور پر ریٹرن پاتھ کا موجودہ سائٹ پر ہونا لاذمی ہے)" enable_local_logins_via_email: "صارفین کو بذریعہ ای میل بھیجے جانے والے ایک کلِک لاگ اِن لِنک کی درخواست کرنے کی اجازت دیں۔" @@ -1026,7 +1015,6 @@ ur: maximum_backups: "ڈِسک پر رکھے جانے بیک اَپس کی زیادہ سے زیادہ تعداد۔ پرانے بیک اَپس خود کار طریقے سے حذف کر دیے جاتے ہیں" automatic_backups_enabled: "خودکار بیک اَپس چلائیں جیسا کہ بیک اَپ فریکوئنسی میں بیان کیا گیا ہے" backup_frequency: "بَیک اَپس کے درمیان دنوں کی تعداد۔" - enable_s3_backups: "مکمل ہونے پر S3 پر بیک اَپس اَپلوڈ کریں۔ اہم: فائل ترتیبات میں درست S3 اسناد کا درج ہونا ضروری ہے۔" s3_backup_bucket: "بیک اَپس رکھنے کیلئے ریمَوٹ بَکِّٹ۔ انتباہ: یقینی بنائیں کہ یہ ایک زاتی بَکِّٹ ہے۔" s3_disable_cleanup: "جب مقامی طور پر بیک اَپ ہٹا دیا جائے اُس کے ساتھ S3 پر سے بھی ہٹا دینا غیر فعال کریں۔" backup_time_of_day: "دن کا وقت UTC جب بیک اَپ ہونا چاہئے۔" @@ -1981,14 +1969,6 @@ ur: spam_post_blocked: title: "سپَیم پوسٹ بلاک کر دی گئی" subject_template: "بار بار لِنکس کی وجہ سے نئے صارف %{username} کی پوسٹس کو بلاک کردیا گیا" - text_body_template: | - یہ ایک خودکار پیغام ہے۔ - - نئے صارف [%{username}](%{user_url}) نے %{domains} کے ساتھ لِنکس والی کئی پوسٹس بنانے کی کوشش کی، لیکن سپَیم سے بچنے کے لئے اُن پوسٹس کو بلاک کر دیا گیا تھا۔ صارف اب بھی نئی پوسٹس بنا سکتا ہے جو %{domains} پر لِنک نہ کریں۔ - - براہ مہربانی [صارف کا جائزہ لیں](%{user_url})۔ - - یہ `newuser_spam_host_threshold` اور `white_listed_spam_host_domains` سائٹ ترتیب کے ذریعے تبدیل کیا جا سکتا ہے۔ unsilenced: title: "خاموشی ختم" subject_template: "اکاؤنٹ اَب ہولڈ پر نہیں ہے" @@ -2407,7 +2387,6 @@ ur: ایک اکاؤنٹ کی ضرورت ہے۔ برائے مہربانی جاری رکھنے کیلئے ایک اکاؤنٹ بنائیں یا لاگ اِن کریں۔ terms_of_service: title: "سروس کی شرائط" - signup_form_message: 'میں نے سروس کی شرائط کو پڑھا اور قبول کر لیا ہے۔' deleted: 'حذف کردہ' image: "تصویر" upload: @@ -2948,9 +2927,6 @@ ur: description: "آپ تقریباً کام مکمل کر چکے ہیں! چلیے لوگوں کو دعوت دیتے ہیں کہ آپ کے مباحثوں میں دلچسپ ٹاپک اور جوابات شامل کرنے میں آپ کی مدد کریں تاکہ آپ کی کمیونٹی کا آغاز ہو سکے۔" finished: title: "آپ کا ڈِسکورس تیار ہے!" - description: | -

    اگر آپ کبھی بھی اِن ترتیبات کو تبدیل کرنا پسند کریں، تو اپنے ایڈمن سیکشن پر جائیں؛ اِسے سائٹ مینیو میں رِنچ آئیکن کے ساتھ تلاش کریں۔

    -

    لطف اندوز ہوئے اور اپنی نئی کمیونٹی بنانے میں اللہ آپ کو کامیاب کرے!

    search_logs: graph_title: "سرچ شمار" joined: "شمولیت اختیار کی" diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index b38aaf422a..83f66f0196 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -361,7 +361,6 @@ vi: email_body: "%{link}\n\n%{message}" inappropriate: title: 'Không thích hợp' - description: 'Chủ để này chứa nội dung mà bình thường được xem là xúc phạm, lạm dụng, hoặc vi phạm nguyên tắc cộng đồng.' long_form: 'đánh dấu cái này không thích hợp' notify_user: title: 'Gửi tin nhắn cho @{{username}}.' @@ -389,11 +388,9 @@ vi: long_form: 'đã đánh dấu bài này dạng bài viết rác' inappropriate: title: 'Không phù hợp' - description: 'Chủ để này chứa nội dung mà với lý lẽ thường nhật được xem là xúc phạm, lạm dụng, hoặc vi phạm chỉ dẫn chung của cộng đồng.' long_form: 'đã đánh dấu bài này không phù hợp' notify_moderators: title: "Một cái khác" - description: 'Bài viết này cần Ban quản trị lưu ý theo dựa trên hướng dẫnđiều khoản sử dụng.' long_form: 'đã đánh dấu cho điều hành viên xem xét' email_title: 'Chủ đề "%{title}" cần được ban điều hành quan tâm' email_body: "%{link}\n\n%{message}" @@ -544,11 +541,6 @@ vi: sidekiq_warning: ' Sidekiq đang không hoạt động. Rất nhiều tác vụ, như gửi email, là được thực thi không đồng bộ bởi sidekiq. Hãy chắc chắn rằng ít nhất một tiến trình sidekiq phải đang hoạt động. Đọc thêm về Sidekiq tại đây.' queue_size_warning: 'Có %{queue_size} công việc đang chờ xử lý trong hàng đợi. Điều này chứng tỏ có vấn đề đã xảy ra với tiến trình Sidekiq, hoặc bạn cần tăng số lượng Sidekiq workers ().' memory_warning: 'Máy chủ của bạn có bộ nhớ ít hơn 1 GB. Khuyến cáo sử dụng bộ nhớ tối thiểu 1 GB .' - google_oauth2_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Google OAuth2 (enable_google_oauth2_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' - facebook_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Facebook (enable_facebook_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' - twitter_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với Twitter (enable_twitter_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' - github_config_warning: 'Máy chủ được cấu hình cho phép đăng ký và đăng nhập với GitHub (enable_github_logins), tuy nhiên giá trị của client id và client secret thì không được thiết lập. Truy cập Cấu hình Site và bổ sung các thiết lập đó. Xem hướng dẫn này để biết thêm chi tiết.' - failing_emails_warning: 'Có %{num_failed_jobs} email jobs thấ bại. Kiểm tra app.yml và chắc chắn rằng cấu hình máy chủ email đúng. Xem jobs thất bại ở Sidekiq.' subfolder_ends_in_slash: "Thư mục con của bạn được thiết lập không đúng, DISCOURSE_RELATIVE_URL_ROOT phải được kết thúc bằng dấu gạch chéo." email_polling_errored_recently: other: "Email đã tạo %{count} lỗi trong 24 giờ qua, xem nhật ký để biết thêm chi tiết." @@ -642,13 +634,11 @@ vi: min_admin_password_length: "Chiều dài mật khẩu tối thiểu đối với Admin." block_common_passwords: "Không cho phép mật khẩu trong danh sách 10.000 mật khẩu phổ biến." enable_sso: "Cho phép dùng single sign on bằng trang ngoài (CẢNH BÁO: ĐỊA CHỈ EMAIL CỦA NGƯỜI DÙNG PHẢI ĐƯỢC CHỨNG THỰC BỞI TRANG NGOÀI!)" - enable_sso_provider: "Thực hiện giao thức cung cấp Discourse SSO tại điểm cuối /session/sso_provider, yêu cầu phải thiết lập sso_secret" sso_url: "URL đầu cuối của Single Sign On (phải kèm theo http:// hoặc https://)" sso_secret: "Chuỗi bảo mật đã được sử dụng để chứng thực thông tin SSO, chắc chắn nó có ít nhất 10 ký tự." sso_overrides_email: "Ghi đè email cục bộ với email trang ngoài từ SSO cho tất cả các lần đăng nhập, và ngăn chặn những thay đổi cục bộ. (LƯU Ý: sự khác biệt có thể xảy ra do sự bình thường hóa các email cục bộ)" sso_overrides_username: "Ghi đè tên tài khoản cục bộ với tài khoản trang ngoài từ SSO cho tất cả các lần đăng nhập, và ngăn chặn những thay đổi cục bộ. (LƯU Ý: sự khác biệt có thể xảy ra do yêu cầu độ dài tên tài khoản khác nhau)" sso_overrides_name: "Ghi đè tên thành viên cục bộ với tên thành viên trang ngoài từ SSO cho tất cả các lần đăng nhập, và ngăn chặn những thay đổi cục bộ." - sso_overrides_avatar: "Ghi đè avatar thành viên cục bộ với avatar thành viên trang ngoài từ SSO. Nếu bật, bạn nên vô hiệu hóa allow_uploaded_avatars" sso_not_approved_url: "Chuyển những tài khoản SSO chưa duyệt tới URL này" allow_new_registrations: "Cho phép đăng ký người dùng mới. Bỏ chọn để bất cứ ai cũng có thể tạo tài khoản mới." enable_signup_cta: "Hiện thông báo với thành viên ẩn danh khi họ quay lại để yêu cầu họ đăng ký tài khoản." @@ -661,7 +651,6 @@ vi: allow_restore: "Cho phép phục hồi, nó có thể thay thế TẤT CẢ dữ liệu trang web! Bỏ chọn, trừ khi bạn có kế hoạch phục hồi một bản sao lưu" maximum_backups: "Số bản sao lưu tối đa lưu trong đĩa cứng. Những bản sao lưu cũ sẽ được xóa tự động" automatic_backups_enabled: "Chạy sao lưu tự động như cấu hình trong tần số sao lưu" - enable_s3_backups: "Tải bản sao lưu lên S3 khi hoàn tất. QUAN TRỌNG: yêu cầu chứng thực S3 đã được nhập trong cấu hình File." s3_backup_bucket: "Địa chỉ tách biệt lưu trữ backup. LƯU Ý: đây phải là địa chỉ được giành riêng." s3_disable_cleanup: "Vô hiệu hóa việc loại bỏ các bản sao lưu từ S3 khi lấy ra tại địa phương." backup_time_of_day: "Thời gian theo ngày UTC khi backup." @@ -1099,7 +1088,6 @@ vi: search_title: "Tìm trang này" terms_of_service: title: "Điều khoản Dịch vụ" - signup_form_message: 'Tôi đã đọc và đồng ý với Điều khoản dịch vụ.' deleted: 'đã bị xóa ' upload: edit_reason: "tải về một bản sao của hình ảnh." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 3dfdf8c20e..607d645bff 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -217,7 +217,7 @@ zh_CN: invalid_incoming_email: "“%{email}”不是有效邮箱地址。" email_already_used_in_group: "“%{email}”已经被群组“%{group_name}”使用了。" email_already_used_in_category: "“%{email}”已经被分类“%{category_name}”使用了。" - cant_allow_membership_requests: "对没有拥有者的小组,你无法批准成员请求。" + cant_allow_membership_requests: "对没有拥有者的群组,你无法批准成员请求。" default_names: everyone: "任何人" admins: "管理面板" @@ -514,8 +514,6 @@ zh_CN: email_body: "%{link}\n\n%{message}" inappropriate: title: '不当内容' - description: '此帖内容包含对他人的攻击、侮辱、仇视语言或违反了我们的社区准则。' - short_description: '违反了我们的社群指引' long_form: '标记为不当内容' notify_user: title: '给@{{username}}发送一条私信' @@ -558,11 +556,9 @@ zh_CN: long_form: '标记为垃圾' inappropriate: title: '不当内容' - description: '此主题内容包含对他人的攻击、侮辱、仇视语言或违反了我们的社区准则。' long_form: '标记为不当内容' notify_moderators: title: "其他内容" - description: '此帖需要版主依据社区准则服务条款(TOS)或其它未列出的原因来给予关注。' long_form: '标记为需版主注意' email_title: '“{title}”主题需要你的关注' email_body: "%{link}\n\n%{message}" @@ -758,11 +754,6 @@ zh_CN: sidekiq_warning: 'Sidekiq 不在运行。很多任务,例如发送电子邮件,是异步的被 sidekiq 调度执行的。请确保至少运行一个 sidekiq 进程。了解 Sidekiq。' queue_size_warning: '队列中有较多任务,为 %{queue_size} 个。这可能是因为 Sidekiq 进程的问题导致,或者需要更多的 Sidekiq 进程。' memory_warning: '你的服务器环境内存少于 1GB,我们建议至少要有 1GB 内存。' - google_oauth2_config_warning: '服务器允许使用 Google Oauth2 登录(enable_google_oauth2_logins),但是 client id 和 client secret 没有被设定。 到站点设置更新此设定。 参考设定指南。' - facebook_config_warning: '服务器允许使用 Facebook 帐号登录(enable_facebook_logins),但是 app id 和 app secret 没有被设定。 到站点设置更新此设定。参考设定指南。' - twitter_config_warning: '服务器允许使用 Twitter 账号登录(enable_twitter_logins),但是 key 和 secret 没有被设定。 到站点设置更新此设定。参考设定指南。' - github_config_warning: '服务器允许使用 GitHub 账号登录(enable_github_logins),但是 client id 和 secret 没有被设定。 到站点设置更新此设定。参考设定指南。' - failing_emails_warning: '有 %{num_failed_jobs} 个邮件任务失败。请检查 app.yml 文件是否正确配置了邮件服务器。查看 Sidekiq 中失败的任务。' subfolder_ends_in_slash: "你的子目录设置不正确;DISCOURSE_RELATIVE_URL_ROOT以斜杠结尾。" email_polling_errored_recently: other: "邮件轮询在过去的 24 小时内出现了 %{count} 个错误。看一看日志寻找详情。" @@ -885,22 +876,20 @@ zh_CN: invite_expiry_days: "多少天以内用户的邀请码是有效的" invite_only: "公开注册已被禁用,所有新用户必须由管理人员邀请进来。" login_required: "需要验证才能继续在该站阅读,不允许匿名访问。" - min_username_length: "最小用户名长度。警告:如果任何现存的用户或小组名字长度比这短,站点将无法正常工作!" - max_username_length: "最大用户名长度。警告:如果任何现存的用户或小组名字长度比这长,站点将无法正常工作!" + min_username_length: "最小用户名长度。警告:如果任何现存的用户或群组名字长度比这短,站点将无法正常工作!" + max_username_length: "最大用户名长度。警告:如果任何现存的用户或群组名字长度比这长,站点将无法正常工作!" reserved_usernames: "不可注册的用户名。通配符 * 能用来匹配字符 0 到多次。" min_password_length: "最小密码长度。" min_admin_password_length: "管理员最短密码长度" password_unique_characters: "密码中必须包含几个不同的字符。" block_common_passwords: "不允许使用 10,000 个最常用的密码。" enable_sso: "启用通过外部站点单点登录(警告:用户的邮件地址必须被外部站点验证!)" - enable_sso_provider: "在 /session/sso_provider endpoint 实现 Discourse SSO 提供方协议,要求设置 sso_secret" sso_url: "单点登录 URL 入口点(必须包含 http:// 或 https://)" sso_secret: "秘密字符串,用于验证秘密的 SSO 信息,请保证是 10 个字符或者更长" sso_overrides_bio: "在用户页面中覆盖用户的个人信息并禁止用户修改" sso_overrides_email: "每一次登录时,用 SSO 信息中的外部站点的邮件地址覆盖本地邮件地址,并且阻止本地的邮件地址修改。(警告:因格本地邮件的正规化,邮件地址可能会有所差异)" sso_overrides_username: "每一次登录时,用 SSO 信息中的外部站点的用户名覆盖本地用户名,并且阻止本地的用户名修改。(警告:因格本地用户名的长度和其他要求,用户名可能会有所差异)" sso_overrides_name: "每一次登录时,用 SSO 信息中的外部站点的全名覆盖本地全名,并且阻止本地的全名修改。" - sso_overrides_avatar: "用单点登录信息中的外部站点头像覆盖用户头像。如果启用,强烈建议禁用 allow_uploaded_avatars" sso_not_approved_url: "重定向未受许可的单点登录账号至这个 URL" sso_allows_all_return_paths: "不限制 SSO 提供的 return_paths 中的域名(默认情况下返回地址必须位于当前站点)" allow_new_registrations: "允许新用户注册。取消选择将阻止任何人创建一个新账户。" @@ -915,7 +904,6 @@ zh_CN: allow_restore: "允许导入数据,这将能替换所有全站数据!除非你计划导入数据,否则请保持设置为 false" maximum_backups: "磁盘保存的最大备份数量。老的备份将自动删除" automatic_backups_enabled: "按照定义的备份频率运行自动备份计划" - enable_s3_backups: "当完成备份后上传备份到 S3。重要:需要在文件设置中填写有效的 S3 验证资料。" s3_backup_bucket: "远端备份 bucket。警告:确认它使私有的 bucket。" s3_disable_cleanup: "当在本地删除备份时不删除 S3 上的备份。" backup_time_of_day: "备份的 UTC 时间" @@ -1393,9 +1381,9 @@ zh_CN: 如果你想要继续收到邮件更新,你可以忽略这封邮件。 invite_mailer: title: "要求发件人" - subject_template: "%{inviter_name} 邀请您参与 '%{topic_title}' 于 %{site_domain_name}" + subject_template: "%{inviter_name} 邀请你参与 '%{topic_title}' 于 %{site_domain_name}" text_body_template: | - %{inviter_name} 邀请您参与讨论 + %{inviter_name} 邀请你参与讨论 > **%{topic_title}** > @@ -1405,14 +1393,14 @@ zh_CN: > %{site_title} -- %{site_description} - 如果您感兴趣,点击以下链接: + 如果你感兴趣,点击以下链接: %{invite_link} custom_invite_mailer: title: "自定义邀请发件人" - subject_template: "%{inviter_name} 邀请您加入 '%{topic_title}' 于 %{site_domain_name}" + subject_template: "%{inviter_name}邀请你加入'%{topic_title}'于%{site_domain_name}" text_body_template: | - %{inviter_name} 邀请您参与讨论 + %{inviter_name} 邀请你参与讨论 > **%{topic_title}** > @@ -1426,27 +1414,27 @@ zh_CN: > %{user_custom_message} - 如果您感兴趣,点击以下链接: + 如果你感兴趣,点击以下链接: %{invite_link} invite_forum_mailer: title: "邀请论坛发件人" - subject_template: "%{inviter_name} 邀请您加入 %{site_domain_name}" + subject_template: "%{inviter_name}邀请你加入%{site_domain_name}" text_body_template: | - %{inviter_name} 邀请您加入 + %{inviter_name} 邀请你加入 > **%{site_title}** > > %{site_description} - 如果您感兴趣,点击以下链接: + 如果你感兴趣,点击以下链接: %{invite_link} custom_invite_forum_mailer: title: "自定义论坛邀请发件人" - subject_template: "%{inviter_name} 邀请您加入 %{site_domain_name}" + subject_template: "%{inviter_name}邀请你加入%{site_domain_name}" text_body_template: | - %{inviter_name} 邀请您加入 + %{inviter_name} 邀请你加入 > **%{site_title}** > @@ -1456,7 +1444,7 @@ zh_CN: > %{user_custom_message} - 如果您感兴趣,点击以下链接: + 如果你感兴趣,点击以下链接: %{invite_link} invite_password_instructions: @@ -1763,7 +1751,7 @@ zh_CN: text_body_template: | 我们非常抱歉,但是你发送至%{destination}(名为%{former_title})的邮件出问题了。 - 在处理您的电子邮件时出现无法识别的错误,它并未发布。你可以再试一次,或者[联系管理人员](%{base_url}/about)。 + 在处理你的电子邮件时出现无法识别的错误,它并未发布。你可以再试一次,或者[联系管理人员](%{base_url}/about)。 email_error_notification: title: "邮件错误提醒" subject_template: "[%{email_prefix}] 电子邮件错误 -- POP 验证错误" @@ -1812,14 +1800,6 @@ zh_CN: spam_post_blocked: title: "垃圾帖子被封锁" subject_template: "新用户 %{username} 因重复发布链接而被禁止发表相关帖子" - text_body_template: | - 这是自动发出的消息。 - - 新用户[%{username}](%{base_url}%{user_url})试图创建多个链接至 %{domains} 的帖子,但这些帖子因为反垃圾策略而被阻挡了。用户仍能够发表不包含到 %{domains} 的帖子。 - - 请[审核该用户](%{user_url})。 - - 该阈值可以通过站点设置中的 `newuser_spam_host_threshold` 和 `white_listed_spam_host_domains` 更改。 unsilenced: title: "解除禁言" subject_template: "账户不再被挂起" @@ -1918,7 +1898,7 @@ zh_CN: > %{site_title} -- %{site_description} user_invited_to_private_message_pm_group: - title: "用户邀请小组至私信" + title: "用户邀请群组至私信" subject_template: "[%{email_prefix}] %{username}邀请 @%{group_name} 加入消息交流:“%{topic_title}”" text_body_template: | %{header_instructions} @@ -2009,7 +1989,7 @@ zh_CN: %{respond_instructions} user_group_mentioned: - title: "用户小组提及" + title: "用户群组提及" subject_template: "[%{email_prefix}] %{topic_title}" text_body_template: | %{header_instructions} @@ -2202,7 +2182,6 @@ zh_CN: 你需要一个账号。请创建一个账号或者登录以继续。 terms_of_service: title: "服务条款" - signup_form_message: '我已经阅读并接受 服务条款。' deleted: '已删除' image: "图片" upload: @@ -2672,7 +2651,7 @@ zh_CN: contact_email: label: "邮件" placeholder: "name@example.com" - description: "社区的负责人或小组的邮件地址。将用来接收关于未处理标记和安全更新的紧急通知,并显示在关于页面上作为紧急联系的方式。" + description: "社区的负责人或群组的邮件地址。将用来接收关于未处理标记和安全更新的紧急通知,并显示在关于页面上作为紧急联系的方式。" contact_url: label: "网页" description: "你或者你的组织平时用于联络的网页。将被显示在关于页面中。" @@ -2734,6 +2713,3 @@ zh_CN: description: "你快做完啦!让我们邀请一些工作人员来帮助你创建一些有趣的主题和回复来启动你的讨论以开始社区之旅。" finished: title: "你的 Discourse 已经准备就绪!" - description: | -

    如果你觉得需要修改这些设置,访问管理员分块;你可以在站点菜单找到扳手图标旁找到。

    -

    祝你玩得开心,还有祝你建设新社区好运!

    diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 8e102ca0ed..887fedc6c5 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -464,7 +464,6 @@ zh_TW: email_body: "%{link}\n\n%{message}" inappropriate: title: '不當內容' - description: '此帖內容包含對他人的攻擊、侮辱、仇視語言或違反了我們的社群守則。' long_form: '投訴為不當內容' notify_user: title: '給 @{{username}} 送出一則訊息' @@ -498,11 +497,9 @@ zh_TW: long_form: '投訴為垃圾內容' inappropriate: title: '不當內容' - description: '此主題內容包含對他人的攻擊、侮辱、仇視語言或違反了我們的社群守則。' long_form: '投訴為不當內容' notify_moderators: title: "其他" - description: '此帖需要版主依據社群準則服務條款(TOS)或其它未列出的原因來給予關注。' long_form: '標記為需版主注意' email_title: '此討論話題 "%{title}" 需要板主注意' email_body: "%{link}\n\n%{message}" @@ -685,11 +682,6 @@ zh_TW: sidekiq_warning: 'Sidekiq 未有執行。很多程序, 如發送電子郵件, 需要 sidekiq 非同步 (asynchronous) 執行的。請確保至少運行一個 sidekiq 程序。瞭解 Sidekiq。' queue_size_warning: '隊列中有較多任務,為 %{queue_size} 個。這可能是因為 Sidekiq 進程的問題導致,或者需要更多的 Sidekiq 進程。' memory_warning: '伺服器記憶體少於 1GB,建議配置至少 1GB 記憶體' - google_oauth2_config_warning: '伺服器設定為允許使用 Google Oauth2 註冊以及登入 (enable_google_oauth2_logins),但未設定客戶端 id 和客戶端 secret 值。請至網站設定裡更改設定。參閱教學指南。' - facebook_config_warning: '伺服器允許使用 Facebook 帳號登入 (enable_facebook_logins), 但未有設定 app id 及 app secret values 。 請在 網站設定 裡更改設定。 設定教學指南。' - twitter_config_warning: '伺服器允許使用 Twitter 帳號登入 (enable_twitter_logins), 但未有設定 key 和 secret values 。 請在 網站設定 裡更改設定。 設定教學指南。' - github_config_warning: '伺服器允許使用 GitHub 帳號登入 (enable_github_logins), 但未有設定 client id 和 secret values。 請在 網站設定 裡更改設定。 設定教學指南。' - failing_emails_warning: '有 %{num_failed_jobs} 個郵件任務失敗。請檢查 app.yml 檔案是否正確配置了郵件伺服器。查看 Sidekiq 中失敗的任務。' subfolder_ends_in_slash: "你的子目錄設置不正確;DISCOURSE_RELATIVE_URL_ROOT以斜杠結尾。" email_polling_errored_recently: other: "郵件輪詢在過去的 24 小時內出現了 %{count} 個錯誤。看一看日誌尋找詳情。" @@ -804,14 +796,12 @@ zh_TW: min_admin_password_length: "管理員最短密碼長度" block_common_passwords: "不允許使用 10,000 個最常用的密碼" enable_sso: "啟用外部站點登入 (注意:必須確認外部站點已驗證使用者的電子郵件地址!)" - enable_sso_provider: "在 /session/sso_provider endpoint 必須設定 sso_secret 以實現 Discourse SSO 提供方協定" sso_url: "SSO URL endpoint (必須包含 http:// 或 https://)" sso_secret: "秘密字符串,用於驗證秘密的 SSO 訊息,請確保由 10 個字或以上組成" sso_overrides_bio: "在用戶頁面中覆蓋用戶的個人信息並禁止用戶修改" sso_overrides_email: "每一次登錄時,用 SSO 信息中的外部站點的郵件地址覆蓋本地郵件地址,並且阻止本地的郵件地址修改。(警告:因格本地郵件的正規化,郵件地址可能會有所差異)" sso_overrides_username: "每一次登錄時,用 SSO 信息中的外部站點的用戶名覆蓋本地用戶名,並且阻止本地的用戶名修改。(警告:因格本地用戶名的長度和其他要求,用戶名可能會有所差異)" sso_overrides_name: "每一次登錄時,用 SSO 信息中的外部站點的全名覆蓋本地全名,並且阻止本地的全名修改。" - sso_overrides_avatar: "用 SSO 訊息中的外部網站頭像覆蓋用戶頭像。如果啟用,建議禁用 allow_uploaded_avatars" sso_not_approved_url: "重定向未受許可的單點登錄賬號至這個 URL" sso_allows_all_return_paths: "不限制 SSO 提供的 return_paths 中的域名(預設情況下返回地址必須位於當前站點)" allow_new_registrations: "允許新用戶註冊,如果取消選取,則沒有人能夠註冊" @@ -826,7 +816,6 @@ zh_TW: allow_restore: "允許還原資料,注意此動作可能覆蓋「所有」網站資料!除非你計畫還原備份檔,否則請保持此設定為 false" maximum_backups: "磁碟備份的最大數量,舊的將會自動刪除" automatic_backups_enabled: "按照定義的備份頻率運行自動備份計劃" - enable_s3_backups: "當完成備份後上傳備份到 S3。重要:需要在文件設定中填寫有效的 S3 驗證資料。" s3_backup_bucket: "遠端備份 bucket ,注意:請確定是私有的 bucket" s3_disable_cleanup: "當在本地刪除備份時不刪除 S3 上的備份。" backup_time_of_day: "備份的 UTC 時間" @@ -1761,7 +1750,6 @@ zh_TW: 你需要一個帳號。請創設一個帳號,或者登入以繼續。 terms_of_service: title: "服務條款" - signup_form_message: '我已閱讀並接受 服務條款' deleted: '已刪除' upload: edit_reason: "下載外部圖片留做存檔" @@ -2269,6 +2257,3 @@ zh_TW: title: "邀請工作人員" finished: title: "你的 Discourse 已經準備就緒!" - description: | -

    如果你覺得需要修改這些設置,訪問管理員分塊;你可以在站點菜單找到扳手表徵圖旁找到。

    -

    祝你玩得開心,還有祝你建設新社區好運!

    diff --git a/plugins/discourse-details/config/locales/client.ko.yml b/plugins/discourse-details/config/locales/client.ko.yml index 8d0ad598c1..60c5409ab4 100644 --- a/plugins/discourse-details/config/locales/client.ko.yml +++ b/plugins/discourse-details/config/locales/client.ko.yml @@ -7,5 +7,8 @@ ko: js: + details: + title: 세부 정보 숨기기 composer: + details_title: 요약 details_text: "이 텍스트는 숨겨집니다." diff --git a/plugins/discourse-local-dates/config/locales/client.es.yml b/plugins/discourse-local-dates/config/locales/client.es.yml index 33ea4a1f56..a34e23dafc 100644 --- a/plugins/discourse-local-dates/config/locales/client.es.yml +++ b/plugins/discourse-local-dates/config/locales/client.es.yml @@ -8,6 +8,10 @@ es: js: discourse_local_dates: + relative_dates: + today: Hoy %{time} + tomorrow: Mañana %{time} + yesterday: Ayer %{time} title: Insertar fecha create: modal_title: Insertar fecha diff --git a/plugins/discourse-local-dates/config/locales/client.ko.yml b/plugins/discourse-local-dates/config/locales/client.ko.yml index bf4e05d6f4..64a7649362 100644 --- a/plugins/discourse-local-dates/config/locales/client.ko.yml +++ b/plugins/discourse-local-dates/config/locales/client.ko.yml @@ -5,4 +5,12 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -ko: {} +ko: + js: + discourse_local_dates: + create: + modal_subtitle: "날짜와 시간이 사용자의 현지 시간대로 자동 변환됩니다." + form: + date_title: 날짜 + time_title: 시간 + format_title: 날짜 형식 diff --git a/plugins/discourse-local-dates/config/locales/client.zh_CN.yml b/plugins/discourse-local-dates/config/locales/client.zh_CN.yml index 14bea9d17f..4a5bfcd19a 100644 --- a/plugins/discourse-local-dates/config/locales/client.zh_CN.yml +++ b/plugins/discourse-local-dates/config/locales/client.zh_CN.yml @@ -8,6 +8,10 @@ zh_CN: js: discourse_local_dates: + relative_dates: + today: 今天%{time} + tomorrow: 明天%{time} + yesterday: 昨天%{time} title: 插入日期 create: modal_title: 插入日期 @@ -16,6 +20,13 @@ zh_CN: insert: 插入 advanced_mode: 高级模式 simple_mode: 简单模式 + format_description: "向用户显示日期的格式。 使用“\\T\\Z”以单词显示用户时区(欧洲/巴黎)" + timezones_title: 要显示的时区 + timezones_description: 时区将用于在预览和撤回中显示日期。 + recurring_title: 循环 + recurring_description: "定义重复事件。你还可以手动编辑表单生成的周期性选项,并使用以下键之一:年,季,月,周,日,小时,分钟,秒,毫秒。" + recurring_none: 没有循环 + invalid_date: 日期无效,请确保日期和时间正确 date_title: 日期 time_title: 时间 format_title: 日期格式 diff --git a/plugins/discourse-local-dates/config/locales/server.zh_CN.yml b/plugins/discourse-local-dates/config/locales/server.zh_CN.yml index 53c9902287..05cd63c942 100644 --- a/plugins/discourse-local-dates/config/locales/server.zh_CN.yml +++ b/plugins/discourse-local-dates/config/locales/server.zh_CN.yml @@ -5,4 +5,8 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -zh_CN: {} +zh_CN: + site_settings: + discourse_local_dates_enabled: "启用话语本地日期功能。这将使用[date]元素添加对帖子中本地日期的时区感知支持。" + discourse_local_dates_default_formats: "经常使用的日期时间格式,请参阅: momentjs字符串格式" + discourse_local_dates_default_timezones: "默认的时区列表,必须是有效的TZ" diff --git a/plugins/discourse-narrative-bot/config/locales/server.es.yml b/plugins/discourse-narrative-bot/config/locales/server.es.yml index 6fec99e6e6..11cd49883a 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.es.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.es.yml @@ -139,9 +139,18 @@ es: Mientras tanto, me quedaré fuera de tu camino. new_user_narrative: reset_trigger: "usuario nuevo" + title: "Certificado de completación para el nuevo usuario" cert_title: "En reconocimiento de la finalización exitosa del tutorial de usuario nuevo" hello: title: ":robot: Saludos!" + message: |- + Gracias por unirte a %{title}, y bienvenido! + + - Soy solo un robot, pero [nuestro amigable staff](%{base_uri}/about) está también aquí para ayudar si necesitas contactar a una persona. + + - Por razones de seguridad, nosotros temporalmente limitamos lo que los nuevos usuarios pueden hacer. Tú podrás ganar nuevas habilidades (y [distintivos](%{base_uri}/badges)) cuando te vayamos conociendo. + + - Nosotros creemos en el [comportamiento de una comunidad civilizada](%{base_uri}/guidelines) en todo momento. onebox: instructions: |- Ahora, tú puedes compartir uno de estos enlaces conmigo? Responde con **el enlace propiamente dicho**, y automáticamente se expandirá con un breve resumen. @@ -248,6 +257,8 @@ es: > :imp: Escribí algo asqueroso aquí Me imagino que sabes qué hacer. Anímate, **reporta este mensaje** con el botón como inapropiado! + reply: |- + [Nuestro staff](%{base_uri}/groups/staff) será notificado por privado sobre tu reporte. Si un número suficiente de miembros reportan un mensaje, será ocultado automáticamente como precaución. (Puesto que no escribí algo realmente desagradable :angel:, he quitado la bandera por ahora.) not_found: |- Oh no, mi mensaje asqueroso no ha sido reportado aún. :worried: ¿Puedes reportar como inapropiado usando la **bandera** ? No olvides que esa opción está oculta, así que debes primero presionar el botón para ver otras opciones para cada mensaje. search: @@ -271,12 +282,25 @@ es: - Si tienes un :keyboard: físico, presiona la tecla ? para ver nuestros prácticos atajos del teclado. not_found: |- Hmm… parece que estás en problemas. Lo siento. ¿Buscaste la palabra **capy​bara**? + end: + message: |- + Gracias por quedarte conmigo @%{username}! Hice esto por tí, creo que te lo has ganado: + + %{certificate} + + ¡Eso es todo por ahora! Echa un vistazo a [**nuestros últimos temas de discusión**](%{base_uri}/latest) o las [**categorías de debate**](%{base_uri}/categories). :sunglasses: + + (Si deseas hablar conmigo de nuevo para aprender más, solo envía un mensaje privado o me mencionas `@%{discobot_username}` en cualquier momento!) certificate: alt: 'Certificado de logro' advanced_user_narrative: reset_trigger: 'usuario avanzado' cert_title: "En reconocimiento de la finalización exitosa del tutorial de usuario avanzado" title: ':arrow_up: Funciones avanzadas del usuario' + start_message: |- + Como un usuario _avanzado_, ¿no has visitado [tus preferencias](%{base_uri}/my/preferences) aún @%{username}? Hay muchas formas de personalizar tu experiencia, por ejemplo podrías elegir un diseño oscuro o bien uno claro. + + Pero yo divago, ¡comencemos! edit: bot_created_post_raw: "@%{discobot_username} es, por lejos, el mejor bot que conozco :wink:" instructions: |- @@ -333,6 +357,10 @@ es: Intentemos cambiar el nivel de notificación de este tema. Al final del tema, encontrarás un botón que muestra que estás **vigilando** este tema. ¿Puedes cambiar el nivel de notificación a **seguir**? not_found: |- Parece que aún estás vigilando :eyes: este tema! Si tienes problemas para encontrar el botón del nivel de notificación, el mismo está debajo de todo el tema de debate. + reply: |- + ¡Impresionante trabajo! Espero que no silencies este tema ya que puedo ser un poco hablador a veces :grin:. + + Tenga en cuenta que cuando responde a un tema o lee un tema durante más de unos minutos, se establece automáticamente en un nivel de notificación de "seguimiento". Puedes cambiar esto en [tus preferencias de usuario](%{base_uri}/my/preferences). poll: instructions: |- ¿Sabes que puedes agregar una encuesta en cualquier mensaje? Intenta usando el botón de engranaje en el editor para **armar una encuesta**. diff --git a/plugins/discourse-narrative-bot/config/locales/server.it.yml b/plugins/discourse-narrative-bot/config/locales/server.it.yml index a0387b69bd..c9175f259f 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.it.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.it.yml @@ -7,6 +7,7 @@ it: site_settings: + discourse_narrative_bot_enabled: 'Abilita Discourse Narrative Bot (discobot)' disable_discourse_narrative_bot_welcome_post: "Disabilita il messaggio di benvenuto di Discourse Narrative Bot" discourse_narrative_bot_ignored_usernames: "Nomi utente che Discourse Narrative Bot deve ignorare" discourse_narrative_bot_disable_public_replies: "Disabilita le risposte pubbliche da Discourse Narrative Bot" @@ -212,6 +213,8 @@ it: Selezionare un qualsiasi testo del mio messaggio farà apparire il pulsante **Cita**. E anche premere **Rispondi** con qualsiasi testo selezionato funzionerà! Puoi provare di nuovo? bookmark: + instructions: |- + Se vuoi saperne di più, seleziona qui sotto e**inserisci questo messaggio privato nei segnalibri**. Se lo farai, ci potrebbe essere un :gift: per te! reply: |- Eccellente! Ora potrai tornare facilmente a questa conversazione privata in ogni momento, proprio dalla [scheda segnalibri sul tuo profilo](%{profile_page_url}/activity/bookmarks). Basta selezionare l'immagine del tuo profilo in alto a destra ↗ not_found: |- diff --git a/plugins/discourse-narrative-bot/config/locales/server.sv.yml b/plugins/discourse-narrative-bot/config/locales/server.sv.yml index 548aa6e73c..9dd1f1efc6 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.sv.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.sv.yml @@ -7,6 +7,7 @@ sv: site_settings: + discourse_narrative_bot_enabled: 'Aktivera Discourse digitala hjälpreda (discobot)' disable_discourse_narrative_bot_welcome_post: "Avaktivera introduktionsmeddelandet från Discourse digitala hjälpreda " discourse_narrative_bot_ignored_usernames: "Användarnamn som Discourse digitala hjälpreda bör ignorera" discourse_narrative_bot_disable_public_replies: "Inaktivera publika svar från Discourse digitala hjälpreda " @@ -20,7 +21,7 @@ sv: Detta märke tilldelas efter genomförande av den interaktiva introduktionen för nya användare. Du har genom detta initiativ lärt dig grundläggande verktygen för diskussion och är nu certifierad! licensed: name: Licencierad - description: "Genomfört den avancerade användar kursen" + description: "Genomfört den avancerade användarkursen" long_description: | Detta märke tilldelas efter genomförande av den interaktiva avancerade instruktionen för användare. Du har genom detta initiativ lärt dig grundläggande verktygen för diskussion och är nu certifierad! discourse_narrative_bot: @@ -73,7 +74,16 @@ sv: quote: "Gör en sak varje dag som skrämmer dig." author: "Eleanor Roosevelt" '9': + quote: "Misstag är alltid förlåtliga, om en har modet att erkänna dem." author: "Bruce Lee" + magic_8_ball: + answers: + '1': "Det är säkert" + '3': "Utan tvivel" + '4': "Ja definitivt" + '5': "Du kan lita på det" + '7': "Mest sannolikt" + '9': "Ja" advanced_user_narrative: poll: reply: |- diff --git a/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml b/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml index a966b86d62..0b53bf67a1 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml @@ -141,9 +141,18 @@ zh_CN: 同时,我不会再烦你。 new_user_narrative: reset_trigger: "新用户" + title: "新用户欢迎教程完毕证明" cert_title: "你已经成功完成新用户教程了" hello: title: ":robot: 你好!" + message: |- + 感谢你加入%{title},欢迎! + + - 我只是一个机器人,但[我们友好的工作人员](%{base_uri} / about)也可以帮到你,如果你需要接触一个人。 + + - 出于安全原因,我们暂时限制新用户可以执行的操作。 当我们了解你时,你将获得新的能力(以及[徽章](%{base_uri} /徽章))。 + + - 我们始终相信[文明社区行为](%{base_uri} /指南)。 onebox: instructions: |- 接下来,你能分享下列链接给我么?回复**单独一行的链接**,然后它会自动展开显示一个有用的摘要。 @@ -256,6 +265,8 @@ zh_CN: > :imp: 我在这写了些脏话 我猜你知道要做什么了。点击**标记该贴**为不合适! + reply: |- + [我们的管理人员](%{base_uri}/组/管理人员)将私下通知你的标记。如果有足够的社区成员标记帖子,作为预防措施它也会自动隐藏。(因为我实际上并没有写一篇讨厌的帖子:angel:我现在已经取消了标记。) not_found: |- 啊不,我糟糕的帖子还没被标记。 :worried: 你能**标记**其为不合适的吗?不要忘记用显示更多按钮来显示每个帖子的更多操作。 search: @@ -279,12 +290,25 @@ zh_CN: - 如果你有 :keyboard:,按下?查看有用的键盘快捷键列表。 not_found: |- 哈…看起来你碰到了些麻烦。抱歉。你试过搜索**capy​bara**了吗? + end: + message: |- + 感谢你坚持使用@%{username}!我为你做了这个,我想你已经赢得了它: + + %{certificate} + + 目前为止就这样了!查看[**我们最新的讨论主题**](%{base_uri}/最新)或[**在分类讨论**](%{base_uri}/分类)。:sunglasses: + + (如果你想再次与我交谈以了解更多信息,请随时留言或提及`@%{discobot_username}`!) certificate: alt: '成就证明' advanced_user_narrative: reset_trigger: '高级用户' cert_title: "你已经成功完成高级用户教程了" title: ':arrow_up: 高级用户特性' + start_message: |- + 作为_advanced_用户,你是否访问过[你的偏好设置页面](%{base_uri}/我的/设置)@%{username}? 有很多方法可以自定义你的体验,例如选择深色或浅色主题 + + 但我离题了,让我们开始吧! edit: bot_created_post_raw: "@%{discobot_username}是目前我了解的最酷的机器人 :wink:" instructions: |- @@ -344,6 +368,10 @@ zh_CN: 让我们试试改变这个主题的通知等级。在主题最下方,你可以看到一个**监看**的按钮。你可以把通知等级改到**追踪**吗? not_found: |- 看起来你仍在监看 :eyes: 这个主题!没找到按钮?通知等级就在主题页面的最下方。 + reply: |- + 太棒了!我希望你没有把这个话题静音,因为我有时会说话有点健谈:grin:。 + + 请注意,当你回复主题或阅读主题超过几分钟时,它会自动设置为“跟踪”的通知级别。 你可以在[你的用户首选项](%{base_uri}/我的/设置)中更改此设置。 poll: instructions: |- 你知道你可以在任何帖子中添加投票吗?试试用编辑器中的齿轮图标来**发起投票**。 diff --git a/plugins/poll/config/locales/client.zh_CN.yml b/plugins/poll/config/locales/client.zh_CN.yml index b9fd8e029b..19e32e18a4 100644 --- a/plugins/poll/config/locales/client.zh_CN.yml +++ b/plugins/poll/config/locales/client.zh_CN.yml @@ -41,6 +41,9 @@ zh_CN: title: "关闭投票" label: "关闭" confirm: "你确定要关闭这个投票?" + automatic_close: + closes_in: "于%{timeLeft}关闭" + age: "%{age}关闭" error_while_toggling_status: "对不起,改变投票状态时出错了。" error_while_casting_votes: "对不起,投票时出错了。" error_while_fetching_voters: "对不起,显示投票者时出错了。" @@ -64,3 +67,5 @@ zh_CN: label: 显示投票人 poll_options: label: 每行输入一个调查选项 + automatic_close: + label: 自动关闭投票 diff --git a/plugins/poll/config/locales/server.zh_CN.yml b/plugins/poll/config/locales/server.zh_CN.yml index c70e578b62..1178f59c1b 100644 --- a/plugins/poll/config/locales/server.zh_CN.yml +++ b/plugins/poll/config/locales/server.zh_CN.yml @@ -26,6 +26,7 @@ zh_CN: named_poll_with_multiple_choices_has_invalid_parameters: "“%{name}”多选投票有无效参数。" requires_at_least_1_valid_option: "你必须选择至少 1 个有效的选项。" default_cannot_be_made_public: "投票调查不能公开。" + named_cannot_be_made_public: "投票%{name}有投票不能公开。" edit_window_expired: op_cannot_edit_options: "在创建话题%{minutes}分钟后,你不可以增加、删除投票选项。如果你需要修改投票选项,请联系管理员。" staff_cannot_add_or_remove_options: "在创建话题%{minutes}分钟后,你不能增加或者删除投票选项。你可以选择删除该话题然后重新创建一个。" @@ -37,6 +38,6 @@ zh_CN: poll_must_be_open_to_vote: "投票必须开启。" topic_must_be_open_to_toggle_status: "主题必须未被锁定才能改变状态。" only_staff_or_op_can_toggle_status: "只有管理人员或者发布投票的人才能改变投票状态。" - insufficient_rights_to_create: "您不能创建投票。" + insufficient_rights_to_create: "你不能创建投票。" email: link_to_poll: "点击查看投票。" From 1acbf8262ba24d601d639c3a8833d67217f5d036 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Mon, 5 Nov 2018 11:04:35 +0000 Subject: [PATCH 209/209] Version bump to v2.2.0.beta4 --- lib/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/version.rb b/lib/version.rb index e427db185d..c0e86c781b 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 2 MINOR = 2 TINY = 0 - PRE = 'beta3' + PRE = 'beta4' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end
    {{human-size backup.size}}
    - {{d-button class="download" + {{d-button class="btn-default download" action="download" actionParam=backup icon="download" @@ -32,10 +32,10 @@ label="admin.backups.operations.download.label"}} {{#if status.isOperationRunning}} {{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger" disabled="true" title="admin.backups.operations.is_running"}} - {{d-button icon="play" action="startRestore" actionParam=backup disabled=status.restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} + {{d-button icon="play" action="startRestore" actionParam=backup disabled=status.restoreDisabled class="btn-default" title=restoreTitle label="admin.backups.operations.restore.label"}} {{else}} {{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger" title="admin.backups.operations.destroy.title"}} - {{d-button icon="play" action="startRestore" actionParam=backup disabled=status.restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} + {{d-button icon="play" action="startRestore" actionParam=backup disabled=status.restoreDisabled class="btn-default" title=restoreTitle label="admin.backups.operations.restore.label"}} {{/if}}
    {{color-input hexValue=c.hex brightnessValue=c.brightness valid=c.valid}} {{#unless model.theme_id}} - - + + {{/unless}}
    {{#unless item.editing}} - {{d-button action="destroy" actionParam=item icon="trash-o" class="btn-danger"}} - {{d-button action="edit" actionParam=item icon="pencil"}} + {{d-button class="btn-default" action="destroy" actionParam=item icon="trash-o" class="btn-danger"}} + {{d-button class="btn-default"action="edit" actionParam=item icon="pencil"}} {{#if item.isBlocked}} - {{d-button action="allow" actionParam=item icon="check" label="admin.logs.screened_ips.actions.do_nothing"}} + {{d-button class="btn-default" action="allow" actionParam=item icon="check" label="admin.logs.screened_ips.actions.do_nothing"}} {{else}} - {{d-button action="block" actionParam=item icon="ban" label="admin.logs.screened_ips.actions.block"}} + {{d-button class="btn-default" action="block" actionParam=item icon="ban" label="admin.logs.screened_ips.actions.block"}} {{/if}} {{else}} - {{d-button action="save" actionParam=item label="admin.logs.save"}} + {{d-button class="btn-default" action="save" actionParam=item label="admin.logs.save"}} {{i18n 'cancel'}} {{/unless}} {{#if currentUser.admin}} {{#if plugin.enabled_setting}} - {{d-button action="showSettings" actionParam=plugin icon="gear" label="admin.plugins.change_settings_short"}} + {{d-button class="btn-default" action="showSettings" actionParam=plugin icon="gear" label="admin.plugins.change_settings_short"}} {{/if}} {{/if}}
    {{number-field number=cat.position}} - {{d-button class="no-text" action="moveUp" actionParam=cat icon="arrow-up"}} - {{d-button class="no-text" action="moveDown" actionParam=cat icon="arrow-down"}} + {{d-button class="btn-default no-text" action="moveUp" actionParam=cat icon="arrow-up"}} + {{d-button class="btn-default no-text" action="moveDown" actionParam=cat icon="arrow-down"}} {{#if cat.hasBufferedChanges}} {{d-button class="no-text ok" action="commit" icon="check"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/preferences/account.hbs b/app/assets/javascripts/discourse/templates/preferences/account.hbs index b68ce33117..7db9379182 100644 --- a/app/assets/javascripts/discourse/templates/preferences/account.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/account.hbs @@ -3,7 +3,7 @@
    {{model.username}} {{#if model.can_edit_username}} - {{#link-to "preferences.username" class="btn btn-small btn-icon pad-left no-text"}} + {{#link-to "preferences.username" class="btn btn-default btn-small btn-icon pad-left no-text"}} {{d-icon "pencil"}} {{/link-to}} {{/if}}
    @@ -37,7 +37,7 @@
    {{model.email}} {{#if model.can_edit_email}} - {{#link-to "preferences.email" class="btn btn-small btn-icon pad-left no-text"}}{{d-icon "pencil"}}{{/link-to}} + {{#link-to "preferences.email" class="btn btn-default btn-small btn-icon pad-left no-text"}}{{d-icon "pencil"}}{{/link-to}} {{/if}}
    @@ -45,7 +45,7 @@
    {{else}}
    - {{d-button action="checkEmail" actionParam=model title="admin.users.check_email.title" icon="envelope-o" label="admin.users.check_email.text"}} + {{d-button action="checkEmail" class="btn-default" actionParam=model title="admin.users.check_email.title" icon="envelope-o" label="admin.users.check_email.text"}}
    {{/if}} @@ -55,7 +55,7 @@
    {{/unless}} diff --git a/app/assets/javascripts/discourse/templates/queued-posts.hbs b/app/assets/javascripts/discourse/templates/queued-posts.hbs index cdaf9169ae..d266e2b9cd 100644 --- a/app/assets/javascripts/discourse/templates/queued-posts.hbs +++ b/app/assets/javascripts/discourse/templates/queued-posts.hbs @@ -6,6 +6,6 @@

    {{i18n "queue.none"}}

    {{/each}} - {{d-button action="refresh" label="refresh" icon="refresh" disabled=model.refreshing id='refresh-queued'}} + {{d-button action="refresh" label="refresh" icon="refresh" disabled=model.refreshing class="btn-default" id='refresh-queued'}}
    diff --git a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs index 60f2f48bcd..36657de0dc 100644 --- a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs +++ b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs @@ -43,7 +43,7 @@ - + {{model.savingStatus}} diff --git a/app/assets/javascripts/discourse/templates/tag-groups.hbs b/app/assets/javascripts/discourse/templates/tag-groups.hbs index a4c0ec21de..6c49ad8aaf 100644 --- a/app/assets/javascripts/discourse/templates/tag-groups.hbs +++ b/app/assets/javascripts/discourse/templates/tag-groups.hbs @@ -7,7 +7,7 @@
  • {{tagGroup.name}}
  • {{/each}} - + {{outlet}} diff --git a/app/assets/javascripts/discourse/templates/topic-list-header-column.raw.hbs b/app/assets/javascripts/discourse/templates/topic-list-header-column.raw.hbs index eba88d2dbb..237917053b 100644 --- a/app/assets/javascripts/discourse/templates/topic-list-header-column.raw.hbs +++ b/app/assets/javascripts/discourse/templates/topic-list-header-column.raw.hbs @@ -5,8 +5,8 @@ {{/if ~}} {{~#if bulkSelectEnabled}} - - + + {{/if ~}} {{/if ~}} diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 1ebdb92ac3..037a563527 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -32,7 +32,7 @@ {{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}}
    {{d-button action="finishedEditingTopic" class="btn-primary btn-small submit-edit" icon="check"}} - {{d-button action="cancelEditingTopic" class="btn-small cancel-edit" icon="times"}} + {{d-button action="cancelEditingTopic" class="btn-default btn-small cancel-edit" icon="times"}} {{#if canRemoveTopicFeaturedLink}} diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index fbbb9d1a66..b6173b6333 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -51,7 +51,7 @@ {{/if}} {{#if currentUser.staff}} -
  • {{d-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}
  • +
  • {{d-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}
  • {{/if}} {{plugin-outlet name="user-profile-controls" connectorTagName="li" @@ -60,11 +60,11 @@ {{#if canExpandProfile}}
  • {{#if collapsedInfo}} - + {{d-icon "angle-double-down"}} {{i18n 'user.expand_profile'}} {{else}} - + {{d-icon "angle-double-up"}} {{i18n 'user.collapse_profile'}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user/activity.hbs b/app/assets/javascripts/discourse/templates/user/activity.hbs index 75899390d8..716a853055 100644 --- a/app/assets/javascripts/discourse/templates/user/activity.hbs +++ b/app/assets/javascripts/discourse/templates/user/activity.hbs @@ -29,7 +29,7 @@ {{#if canDownloadPosts}}
    - {{d-button action="exportUserArchive" label="user.download_archive.button_text" icon="download"}} + {{d-button action="exportUserArchive" class="btn-default" label="user.download_archive.button_text" icon="download"}}
    {{/if}} {{/d-section}} diff --git a/app/assets/javascripts/discourse/templates/user/messages.hbs b/app/assets/javascripts/discourse/templates/user/messages.hbs index 2e1bbb33e5..85541a84a2 100644 --- a/app/assets/javascripts/discourse/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/templates/user/messages.hbs @@ -59,7 +59,7 @@
    {{#if showToggleBulkSelect}} - {{/if}} @@ -71,19 +71,19 @@ {{/if}} {{#if canArchive}} - {{/if}} {{#if canMoveToInbox}} - {{/if}} {{#if bulkSelectEnabled}} - {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user/notifications.hbs b/app/assets/javascripts/discourse/templates/user/notifications.hbs index c732db3fba..ede14b75b9 100644 --- a/app/assets/javascripts/discourse/templates/user/notifications.hbs +++ b/app/assets/javascripts/discourse/templates/user/notifications.hbs @@ -31,7 +31,7 @@ {{#if model}} {{d-button title='user.dismiss_notifications_tooltip' - class='btn dismiss-notifications' + class='btn btn-default dismiss-notifications' action=(action "resetNew") label='user.dismiss_notifications' icon='check' diff --git a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 index 1a7182288e..29732a3423 100644 --- a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 @@ -22,6 +22,7 @@ export function buildManageButtons(attrs, currentUser, siteSettings) { if (currentUser.staff) { contents.push({ icon: "list", + className: "btn-default", label: "admin.flags.moderation_history", action: "showModerationHistory" }); @@ -31,7 +32,7 @@ export function buildManageButtons(attrs, currentUser, siteSettings) { const buttonAtts = { action: "togglePostType", icon: "shield", - className: "toggle-post-type" + className: "btn-default toggle-post-type" }; if (attrs.isModeratorAction) { @@ -47,7 +48,7 @@ export function buildManageButtons(attrs, currentUser, siteSettings) { icon: "cog", label: "post.controls.rebake", action: "rebakePost", - className: "rebuild-html" + className: "btn-default rebuild-html" }); if (attrs.hidden) { @@ -55,7 +56,7 @@ export function buildManageButtons(attrs, currentUser, siteSettings) { icon: "eye", label: "post.controls.unhide", action: "unhidePost", - className: "unhide-post" + className: "btn-default unhide-post" }); } } @@ -65,7 +66,7 @@ export function buildManageButtons(attrs, currentUser, siteSettings) { icon: "user", label: "post.controls.change_owner", action: "changePostOwner", - className: "change-owner" + className: "btn-default change-owner" }); } @@ -75,7 +76,7 @@ export function buildManageButtons(attrs, currentUser, siteSettings) { icon: "certificate", label: "post.controls.grant_badge", action: "grantBadge", - className: "grant-badge" + className: "btn-default grant-badge" }); } @@ -85,7 +86,7 @@ export function buildManageButtons(attrs, currentUser, siteSettings) { label: `post.controls.${action}_post`, action: `${action}Post`, title: `post.controls.${action}_post_description`, - className: `${action}-post` + className: `btn-default ${action}-post` }); } @@ -95,14 +96,14 @@ export function buildManageButtons(attrs, currentUser, siteSettings) { action: "toggleWiki", label: "post.controls.unwiki", icon: "pencil-square-o", - className: "wiki wikied" + className: "btn-default wiki wikied" }); } else { contents.push({ action: "toggleWiki", label: "post.controls.wiki", icon: "pencil-square-o", - className: "wiki" + className: "btn-default wiki" }); } } diff --git a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 index e55f96b13a..200ed332b2 100644 --- a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 @@ -157,7 +157,7 @@ export default createWidget("private-message-map", { this.attach("button", { action: "toggleEditing", label: "private_message_info.edit", - className: "btn add-remove-participant-btn" + className: "btn btn-default add-remove-participant-btn" }) ]; diff --git a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 index 4322771a23..783b4f09ef 100644 --- a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 @@ -42,7 +42,8 @@ createWidget("topic-admin-menu-button", { result.push( this.attach("button", { className: - "toggle-admin-menu" + (attrs.fixed ? " show-topic-admin" : ""), + "btn-default toggle-admin-menu" + + (attrs.fixed ? " show-topic-admin" : ""), title: "topic_admin_menu", icon: "wrench", action: "showAdminMenu", @@ -132,6 +133,7 @@ export default createWidget("topic-admin-menu", { const buttons = []; buttons.push({ className: "topic-admin-multi-select", + buttonClass: "btn-default", action: "toggleMultiSelect", icon: "tasks", label: "actions.multi_select" @@ -153,6 +155,7 @@ export default createWidget("topic-admin-menu", { if (topic.get("deleted") && details.get("can_recover")) { buttons.push({ className: "topic-admin-recover", + buttonClass: "btn-default", action: "recoverTopic", icon: "undo", label: "actions.recover" @@ -162,6 +165,7 @@ export default createWidget("topic-admin-menu", { if (topic.get("closed")) { buttons.push({ className: "topic-admin-open", + buttonClass: "btn-default", action: "toggleClosed", icon: "unlock", label: "actions.open" @@ -169,6 +173,7 @@ export default createWidget("topic-admin-menu", { } else { buttons.push({ className: "topic-admin-close", + buttonClass: "btn-default", action: "toggleClosed", icon: "lock", label: "actions.close" @@ -177,6 +182,7 @@ export default createWidget("topic-admin-menu", { buttons.push({ className: "topic-admin-status-update", + buttonClass: "btn-default", action: "showTopicStatusUpdate", icon: "clock-o", label: "actions.timed_update" @@ -188,6 +194,7 @@ export default createWidget("topic-admin-menu", { if (!isPrivateMessage && (topic.get("visible") || featured)) { buttons.push({ className: "topic-admin-pin", + buttonClass: "btn-default", action: "showFeatureTopic", icon: "thumb-tack", label: featured ? "actions.unpin" : "actions.pin" @@ -197,6 +204,7 @@ export default createWidget("topic-admin-menu", { if (this.currentUser.admin) { buttons.push({ className: "topic-admin-change-timestamp", + buttonClass: "btn-default", action: "showChangeTimestamp", icon: "calendar", label: "change_timestamp.title" @@ -206,6 +214,7 @@ export default createWidget("topic-admin-menu", { if (this.currentUser.get("staff")) { buttons.push({ className: "topic-admin-reset-bump-date", + buttonClass: "btn-default", action: "resetBumpDate", icon: "anchor", label: "actions.reset_bump_date" @@ -215,6 +224,7 @@ export default createWidget("topic-admin-menu", { if (!isPrivateMessage) { buttons.push({ className: "topic-admin-archive", + buttonClass: "btn-default", action: "toggleArchived", icon: "folder", label: topic.get("archived") ? "actions.unarchive" : "actions.archive" @@ -224,6 +234,7 @@ export default createWidget("topic-admin-menu", { const visible = topic.get("visible"); buttons.push({ className: "topic-admin-visible", + buttonClass: "btn-default", action: "toggleVisibility", icon: visible ? "eye-slash" : "eye", label: visible ? "actions.invisible" : "actions.visible" @@ -232,6 +243,7 @@ export default createWidget("topic-admin-menu", { if (details.get("can_convert_topic")) { buttons.push({ className: "topic-admin-convert", + buttonClass: "btn-default", action: isPrivateMessage ? "convertToPublicTopic" : "convertToPrivateMessage", @@ -243,6 +255,7 @@ export default createWidget("topic-admin-menu", { if (this.currentUser.get("staff")) { buttons.push({ action: "showModerationHistory", + buttonClass: "btn-default", icon: "list", fullLabel: "admin.flags.moderation_history" }); diff --git a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 index 5cfcee2363..f73c7392ad 100644 --- a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 @@ -337,7 +337,7 @@ createWidget("timeline-footer-controls", { if (topic.get("details.can_create_post")) { controls.push( this.attach("button", { - className: "create", + className: "btn-default create", icon: "reply", title: "topic.reply.help", action: "replyToPost" diff --git a/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 index 12a37b3e15..1acccd3067 100644 --- a/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/dropdown-select-box/dropdown-select-box-header.js.es6 @@ -4,7 +4,7 @@ import computed from "ember-addons/ember-computed-decorators"; export default SelectKitHeaderComponent.extend({ layoutName: "select-kit/templates/components/dropdown-select-box/dropdown-select-box-header", - classNames: "dropdown-select-box-header", + classNames: "btn-default dropdown-select-box-header", tagName: "button", classNameBindings: ["btnClassName"], diff --git a/app/assets/stylesheets/common/select-kit/toolbar-popup-menu-options.scss b/app/assets/stylesheets/common/select-kit/toolbar-popup-menu-options.scss index 3d1e28dcde..389e2f394a 100644 --- a/app/assets/stylesheets/common/select-kit/toolbar-popup-menu-options.scss +++ b/app/assets/stylesheets/common/select-kit/toolbar-popup-menu-options.scss @@ -8,7 +8,6 @@ } .select-kit-row { - border-radius: 4px; margin-bottom: 5px; padding: 6px 3px; background: $primary-low; diff --git a/app/views/exceptions/not_found.html.erb b/app/views/exceptions/not_found.html.erb index 7949a1b5ab..4f08ba275a 100644 --- a/app/views/exceptions/not_found.html.erb +++ b/app/views/exceptions/not_found.html.erb @@ -11,7 +11,7 @@ <%= link_to t.title, t.relative_url %><%= category_badge(t.category) %>
    <% end %> - " class="btn"><%= t 'page_not_found.see_more' %>… + " class="btn btn-default"><%= t 'page_not_found.see_more' %>…
  • <%= t 'page_not_found.recent_topics' %>

    @@ -20,7 +20,7 @@ <%= link_to t.title, t.relative_url %><%= category_badge(t.category) %>
    <% end %> - " class="btn"><%= t 'page_not_found.see_more' %>… + " class="btn btn-default"><%= t 'page_not_found.see_more' %>… <% end %> diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs index 9b835945a6..737e797809 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs @@ -112,7 +112,7 @@ <% end %> diff --git a/app/views/users/_auto_redirect_home.html.erb b/app/views/users/_auto_redirect_home.html.erb deleted file mode 100644 index 7243a2bbca..0000000000 --- a/app/views/users/_auto_redirect_home.html.erb +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/app/views/users/activate_account.html.erb b/app/views/users/activate_account.html.erb index 2bf9393135..85d99fa536 100644 --- a/app/views/users/activate_account.html.erb +++ b/app/views/users/activate_account.html.erb @@ -13,22 +13,7 @@ <%= preload_script "ember_jquery" %> <%= preload_script "vendor" %> <%= render_google_universal_analytics_code %> + <%= tag.meta id: 'data-activate-account', data: { path: path('/u/hp') } %> <%- end %> - +<%= preload_script "activate-account" %> diff --git a/app/views/users/omniauth_callbacks/complete.html.erb b/app/views/users/omniauth_callbacks/complete.html.erb index c4dd3c9db4..5eb0ab9db9 100644 --- a/app/views/users/omniauth_callbacks/complete.html.erb +++ b/app/views/users/omniauth_callbacks/complete.html.erb @@ -15,6 +15,11 @@ border-bottom-color: #999; } + <%= tag.meta id: 'data-auth-result', data: { + auth_result: @auth_result.to_client_hash, + base_url: Discourse.base_url + } %> + <%= preload_script('omniauth-complete') %> @@ -23,18 +28,6 @@ <%=t "login.auth_complete" %> <%= t("login.click_to_continue") %>

    - - diff --git a/app/views/users/perform_account_activation.html.erb b/app/views/users/perform_account_activation.html.erb index 09d66b2224..ae726da69f 100644 --- a/app/views/users/perform_account_activation.html.erb +++ b/app/views/users/perform_account_activation.html.erb @@ -13,7 +13,10 @@ <% else %>

    <%= t('activation.please_continue') %>

    "><%= t('activation.continue_button', site_name: SiteSetting.title) -%>

    - <%= render partial: 'auto_redirect_home' %> + <%- content_for(:no_ember_head) do %> + <%= tag.meta id: 'data-auto-redirect', data: { path: path('/') } %> + <%- end %> + <%= preload_script 'auto-redirect' %> <% end %> <%end%> diff --git a/app/views/wizard/index.html.erb b/app/views/wizard/index.html.erb index 74092def2e..942e77bd92 100644 --- a/app/views/wizard/index.html.erb +++ b/app/views/wizard/index.html.erb @@ -17,12 +17,6 @@
    - - + <%= preload_script 'wizard-start' %> diff --git a/config/application.rb b/config/application.rb index a320d6dc36..4a64cead11 100644 --- a/config/application.rb +++ b/config/application.rb @@ -121,6 +121,11 @@ module Discourse google-universal-analytics.js preload-application-data.js authentication-complete.js + print-page.js + omniauth-complete.js + activate-account.js + auto-redirect.js + wizard-start.js } # Precompile all available locales diff --git a/spec/views/omniauth_callbacks/complete.html.erb_spec.rb b/spec/views/omniauth_callbacks/complete.html.erb_spec.rb index f66ad99d8a..bb633f0c29 100644 --- a/spec/views/omniauth_callbacks/complete.html.erb_spec.rb +++ b/spec/views/omniauth_callbacks/complete.html.erb_spec.rb @@ -6,7 +6,7 @@ require_dependency "auth/result" describe "users/omniauth_callbacks/complete.html.erb" do let :rendered_data do - JSON.parse(rendered.match(/var authResult = (.*);/)[1]) + JSON.parse(rendered.match(/data-auth-result="([^"]*)"/)[1].gsub('"', '"')) end it "renders auth info" do From 306d77b54f5d3089c08d9e068737a7cb91997ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 25 Oct 2018 16:08:10 +0200 Subject: [PATCH 131/209] FIX: don't use srcset on cropped thumbnails --- lib/cooked_post_processor.rb | 23 +++++++-------- spec/components/cooked_post_processor_spec.rb | 29 ++++++++++++++++--- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index c58562d5c0..6a343bee75 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -309,7 +309,7 @@ class CookedPostProcessor end end - add_lightbox!(img, original_width, original_height, upload) + add_lightbox!(img, original_width, original_height, upload, cropped: crop) end def is_a_hyperlink?(img) @@ -330,7 +330,7 @@ class CookedPostProcessor .each { |r| yield r if r > 1 } end - def add_lightbox!(img, original_width, original_height, upload = nil) + def add_lightbox!(img, original_width, original_height, upload, cropped: false) # first, create a div to hold our lightbox lightbox = create_node("div", "lightbox-wrapper") img.add_next_sibling(lightbox) @@ -352,7 +352,7 @@ class CookedPostProcessor if upload thumbnail = upload.thumbnail(w, h) if thumbnail && thumbnail.filesize.to_i < upload.filesize - img["src"] = upload.thumbnail(w, h).url + img["src"] = thumbnail.url srcset = +"" @@ -360,19 +360,16 @@ class CookedPostProcessor resized_w = (w * ratio).to_i resized_h = (h * ratio).to_i - if upload.width && resized_w > upload.width + if !cropped && upload.width && resized_w > upload.width cooked_url = UrlHelper.cook_url(upload.url) - srcset << ", #{cooked_url} #{ratio}x" - else - if t = upload.thumbnail(resized_w, resized_h) - cooked_url = UrlHelper.cook_url(t.url) - srcset << ", #{cooked_url} #{ratio}x" - end + srcset << ", #{cooked_url} #{ratio.to_s.sub(/\.0$/, "")}x" + elsif t = upload.thumbnail(resized_w, resized_h) + cooked_url = UrlHelper.cook_url(t.url) + srcset << ", #{cooked_url} #{ratio.to_s.sub(/\.0$/, "")}x" end + + img["srcset"] = "#{UrlHelper.cook_url(img["src"])}#{srcset}" if srcset.present? end - - img["srcset"] = "#{UrlHelper.cook_url(img["src"])}#{srcset}" if srcset.length > 0 - else img["src"] = upload.url end diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 5cfb1c64b3..98aea3515f 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -58,10 +58,10 @@ describe CookedPostProcessor do end context "responsive images" do + + before { SiteSetting.responsive_post_image_sizes = "1|1.5|3" } + it "includes responsive images on demand" do - - SiteSetting.responsive_post_image_sizes = "1|1.5|3" - upload = Fabricate(:upload, width: 2000, height: 1500, filesize: 10000) post = Fabricate(:post, raw: "hello ") @@ -93,8 +93,29 @@ describe CookedPostProcessor do cpp.post_process_images # 1.5x is skipped cause we have a missing thumb - expect(cpp.html).to include('srcset="http://a.b.c/666x500.jpg, http://a.b.c/1998x1500.jpg 3.0x"') + expect(cpp.html).to include('srcset="http://a.b.c/666x500.jpg, http://a.b.c/1998x1500.jpg 3x"') + end + it "doesn't include response images for cropped images" do + upload = Fabricate(:upload, width: 200, height: 4000, filesize: 12345) + post = Fabricate(:post, raw: "hello ") + + # fake some optimized images + OptimizedImage.create!( + url: 'http://a.b.c/200x500.jpg', + width: 200, + height: 500, + upload_id: upload.id, + sha1: SecureRandom.hex, + extension: '.jpg', + filesize: 500 + ) + + cpp = CookedPostProcessor.new(post) + cpp.add_to_size_cache(upload.url, 200, 4000) + cpp.post_process_images + + expect(cpp.html).to_not include('srcset="') end end From c2c99c7c392990320f4cf5e9c38d229a529bfeff Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 25 Oct 2018 15:36:24 -0400 Subject: [PATCH 132/209] FIX: Don't seed flags if ids don't exist This can happen if you use the `replace_flags` plugin API to remove a flag. --- db/fixtures/003_post_action_types.rb | 60 ++++++++++++++++------------ 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/db/fixtures/003_post_action_types.rb b/db/fixtures/003_post_action_types.rb index f0094764fa..0de7006001 100644 --- a/db/fixtures/003_post_action_types.rb +++ b/db/fixtures/003_post_action_types.rb @@ -13,37 +13,47 @@ PostActionType.seed do |s| s.position = 2 end -PostActionType.seed do |s| - s.id = PostActionType.types[:off_topic] - s.name_key = 'off_topic' - s.is_flag = true - s.position = 3 +if PostActionType.types[:off_topic] + PostActionType.seed do |s| + s.id = PostActionType.types[:off_topic] + s.name_key = 'off_topic' + s.is_flag = true + s.position = 3 + end end -PostActionType.seed do |s| - s.id = PostActionType.types[:inappropriate] - s.name_key = 'inappropriate' - s.is_flag = true - s.position = 4 +if PostActionType.types[:inappropriate] + PostActionType.seed do |s| + s.id = PostActionType.types[:inappropriate] + s.name_key = 'inappropriate' + s.is_flag = true + s.position = 4 + end end -PostActionType.seed do |s| - s.id = PostActionType.types[:spam] - s.name_key = 'spam' - s.is_flag = true - s.position = 6 +if PostActionType.types[:spam] + PostActionType.seed do |s| + s.id = PostActionType.types[:spam] + s.name_key = 'spam' + s.is_flag = true + s.position = 6 + end end -PostActionType.seed do |s| - s.id = PostActionType.types[:notify_user] - s.name_key = 'notify_user' - s.is_flag = true - s.position = 7 +if PostActionType.types[:notify_user] + PostActionType.seed do |s| + s.id = PostActionType.types[:notify_user] + s.name_key = 'notify_user' + s.is_flag = true + s.position = 7 + end end -PostActionType.seed do |s| - s.id = PostActionType.types[:notify_moderators] - s.name_key = 'notify_moderators' - s.is_flag = true - s.position = 8 +if PostActionType.types[:notify_moderators] + PostActionType.seed do |s| + s.id = PostActionType.types[:notify_moderators] + s.name_key = 'notify_moderators' + s.is_flag = true + s.position = 8 + end end From d17c8df926df8a72e44edc4ee60134c38718f416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 26 Oct 2018 00:29:28 +0200 Subject: [PATCH 133/209] Only check for suspicious login for staff members --- app/models/user_auth_token.rb | 29 ++++++++++--------- lib/auth/default_current_user_provider.rb | 10 ++++--- spec/jobs/suspicious_login_spec.rb | 2 +- .../user_auth_token_serializer_spec.rb | 4 +-- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/app/models/user_auth_token.rb b/app/models/user_auth_token.rb index 2c066b4255..b36af49d81 100644 --- a/app/models/user_auth_token.rb +++ b/app/models/user_auth_token.rb @@ -50,6 +50,8 @@ class UserAuthToken < ActiveRecord::Base end def self.is_suspicious(user_id, user_ip) + return false unless User.find_by(id: user_id)&.staff? + ips = UserAuthTokenLog.where(user_id: user_id).pluck(:client_ip) ips.delete_at(ips.index(user_ip) || ips.length) # delete one occurance (current) ips.uniq! @@ -59,13 +61,13 @@ class UserAuthToken < ActiveRecord::Base ips.none? { |ip| user_location == login_location(ip) } end - def self.generate!(info) + def self.generate!(user_id: , user_agent: nil, client_ip: nil, path: nil, staff: nil) token = SecureRandom.hex(16) hashed_token = hash_token(token) user_auth_token = UserAuthToken.create!( - user_id: info[:user_id], - user_agent: info[:user_agent], - client_ip: info[:client_ip], + user_id: user_id, + user_agent: user_agent, + client_ip: client_ip, auth_token: hashed_token, prev_auth_token: hashed_token, rotated_at: Time.zone.now @@ -74,22 +76,23 @@ class UserAuthToken < ActiveRecord::Base log(action: 'generate', user_auth_token_id: user_auth_token.id, - user_id: info[:user_id], - user_agent: info[:user_agent], - client_ip: info[:client_ip], - path: info[:path], + user_id: user_id, + user_agent: user_agent, + client_ip: client_ip, + path: path, auth_token: hashed_token) - Jobs.enqueue(:suspicious_login, - user_id: info[:user_id], - client_ip: info[:client_ip], - user_agent: info[:user_agent]) + if staff + Jobs.enqueue(:suspicious_login, + user_id: user_id, + client_ip: client_ip, + user_agent: user_agent) + end user_auth_token end def self.lookup(unhashed_token, opts = nil) - mark_seen = opts && opts[:seen] token = hash_token(unhashed_token) diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index e4badae622..7ecf7c2392 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -150,10 +150,12 @@ class Auth::DefaultCurrentUserProvider end def log_on_user(user, session, cookies) - @user_token = UserAuthToken.generate!(user_id: user.id, - user_agent: @env['HTTP_USER_AGENT'], - path: @env['REQUEST_PATH'], - client_ip: @request.ip) + @user_token = UserAuthToken.generate!( + user_id: user.id, + user_agent: @env['HTTP_USER_AGENT'], + path: @env['REQUEST_PATH'], + client_ip: @request.ip, + staff: user.staff?) cookies[TOKEN_COOKIE] = cookie_hash(@user_token.unhashed_auth_token) unstage_user(user) diff --git a/spec/jobs/suspicious_login_spec.rb b/spec/jobs/suspicious_login_spec.rb index 836bac3d18..b7e63bfd3c 100644 --- a/spec/jobs/suspicious_login_spec.rb +++ b/spec/jobs/suspicious_login_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe Jobs::SuspiciousLogin do - let(:user) { Fabricate(:user) } + let(:user) { Fabricate(:moderator) } before do UserAuthToken.stubs(:login_location).with("1.1.1.1").returns("Location 1") diff --git a/spec/serializers/user_auth_token_serializer_spec.rb b/spec/serializers/user_auth_token_serializer_spec.rb index ef8f9f7cc9..93d94f76ed 100644 --- a/spec/serializers/user_auth_token_serializer_spec.rb +++ b/spec/serializers/user_auth_token_serializer_spec.rb @@ -2,8 +2,8 @@ require 'rails_helper' describe UserAuthTokenSerializer do - let(:user) { Fabricate(:user) } - let(:token) { UserAuthToken.generate!(user_id: user.id, client_ip: '2a02:ea00::') } + let(:user) { Fabricate(:moderator) } + let(:token) { UserAuthToken.generate!(user_id: user.id, client_ip: '2a02:ea00::', staff: true) } before(:each) do DiscourseIpInfo.open_db(File.join(Rails.root, 'spec', 'fixtures', 'mmdb')) From ed9c21e42c3e34955013fe34ba8443841bafbedd Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Thu, 25 Oct 2018 20:34:39 -0400 Subject: [PATCH 134/209] FEATURE: hide muted categories from /categories list (#6531) --- app/models/category_list.rb | 5 +++++ spec/models/category_list_spec.rb | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/app/models/category_list.rb b/app/models/category_list.rb index e8bf8aefab..f59f36c3cd 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -20,6 +20,7 @@ class CategoryList find_categories prune_empty + prune_muted find_user_data sort_unpinned trim_results @@ -136,6 +137,10 @@ class CategoryList @categories.delete_if { |c| c.uncategorized? && c.displayable_topics.blank? } end + def prune_muted + @categories.delete_if { |c| c.notification_level == CategoryUser.notification_levels[:muted] } + end + # Attach some data for serialization to each topic def find_user_data if @guardian.current_user && @all_topics.present? diff --git a/spec/models/category_list_spec.rb b/spec/models/category_list_spec.rb index ac7a46542a..f9d7ff79d1 100644 --- a/spec/models/category_list_spec.rb +++ b/spec/models/category_list_spec.rb @@ -47,6 +47,16 @@ describe CategoryList do expect(CategoryList.new(Guardian.new(nil), include_topics: true).categories.find { |x| x.name == private_cat.name }).to eq(nil) end + it "properly hide muted categories" do + cat_muted = Fabricate(:category) + CategoryUser.create!(user_id: user.id, + category_id: cat_muted.id, + notification_level: CategoryUser.notification_levels[:muted]) + + # uncategorized + cat_muted for admin + expect(CategoryList.new(Guardian.new admin).categories.count).to eq(2) + expect(CategoryList.new(Guardian.new user).categories.count).to eq(1) + end end context "with a category" do From 3c92202654618cdd9bf6d577cdd8e0b2a8171b59 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Thu, 25 Oct 2018 20:34:55 -0400 Subject: [PATCH 135/209] Set individual future-date-input components as clearable, fixes admin Safari bug (#6522) --- .../javascripts/admin/templates/modal/admin-silence-user.hbs | 1 + .../javascripts/admin/templates/modal/admin-suspend-user.hbs | 1 + .../discourse/templates/components/future-date-input.hbs | 1 + .../javascripts/discourse/templates/modal/feature-topic.hbs | 4 ++++ .../select-kit/components/future-date-input-selector.js.es6 | 1 - 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs index b31251a91d..035c773897 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs @@ -7,6 +7,7 @@ class="silence-until" label="admin.user.silence_duration" includeFarFuture=true + clearable=false input=silenceUntil}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs index 6c2e8e1636..cd7031bb8a 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs @@ -8,6 +8,7 @@ class="suspend-until" label="admin.user.suspend_duration" includeFarFuture=true + clearable=false input=suspendUntil}} diff --git a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs index b509f19d76..e7d1cea3b5 100644 --- a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs @@ -8,6 +8,7 @@ input=input includeWeekend=includeWeekend includeFarFuture=includeFarFuture + clearable=clearable none="topic.auto_update_input.none"}} diff --git a/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs b/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs index ca3b8ea1ac..7b6fc38d91 100644 --- a/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs +++ b/app/assets/javascripts/discourse/templates/modal/feature-topic.hbs @@ -44,6 +44,7 @@ {{future-date-input class="pin-until" includeFarFuture=true + clearable=true input=model.pinnedInCategoryUntil}} {{popup-input-tip validation=pinInCategoryValidation shownAt=pinInCategoryTipShownAt}}

    @@ -54,6 +55,7 @@ {{future-date-input class="pin-until" includeFarFuture=true + clearable=true input=model.pinnedInCategoryUntil}} {{popup-input-tip validation=pinInCategoryValidation shownAt=pinInCategoryTipShownAt}}

    @@ -86,6 +88,7 @@ {{future-date-input class="pin-until" includeFarFuture=true + clearable=true input=model.pinnedGloballyUntil}} {{popup-input-tip validation=pinGloballyValidation shownAt=pinGloballyTipShownAt}}

    @@ -96,6 +99,7 @@ {{future-date-input class="pin-until" includeFarFuture=true + clearable=true input=model.pinnedGloballyUntil}} {{popup-input-tip validation=pinGloballyValidation shownAt=pinGloballyTipShownAt}}

    diff --git a/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 b/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 index 47c5faf511..8115fd3d97 100644 --- a/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 +++ b/app/assets/javascripts/select-kit/components/future-date-input-selector.js.es6 @@ -164,7 +164,6 @@ export default ComboBoxComponent.extend(DatetimeMixin, { classNames: ["future-date-input-selector"], isCustom: Ember.computed.equal("value", "pick_date_and_time"), isBasedOnLastPost: Ember.computed.equal("value", "set_based_on_last_post"), - clearable: true, rowComponent: "future-date-input-selector/future-date-input-selector-row", headerComponent: "future-date-input-selector/future-date-input-selector-header", From 398f98c5687b191b49f2182ff6a3066827285a87 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 26 Oct 2018 12:32:02 +0200 Subject: [PATCH 136/209] FIX: ensures reports links are correct on subfolder installs --- .../javascripts/admin/models/report.js.es6 | 8 ++++---- test/javascripts/models/report-test.js.es6 | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 5fa9ffa39e..2bd9485d37 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -304,7 +304,7 @@ const Report = Discourse.Model.extend({ avatar_template: row[properties.avatar] }); - const href = `/admin/users/${userId}/${username}`; + const href = Discourse.getURL(`/admin/users/${userId}/${username}`); const avatarImg = renderAvatar(user, { imageSize: "tiny", @@ -327,7 +327,7 @@ const Report = Discourse.Model.extend({ const formatedValue = () => { const topicId = row[properties.id]; - const href = `/t/-/${topicId}`; + const href = Discourse.getURL(`/t/-/${topicId}`); return `${topicTitle}`; }; @@ -341,7 +341,7 @@ const Report = Discourse.Model.extend({ const postTitle = row[properties.truncated_raw]; const postNumber = row[properties.number]; const topicId = row[properties.topic_id]; - const href = `/t/-/${topicId}/${postNumber}`; + const href = Discourse.getURL(`/t/-/${topicId}/${postNumber}`); return { property: properties.title, @@ -395,7 +395,7 @@ const Report = Discourse.Model.extend({ _linkLabel(properties, row) { const property = properties[0]; - const value = row[property]; + const value = Discourse.getURL(row[property]); const formatedValue = (href, anchor) => { return `${escapeExpression( anchor diff --git a/test/javascripts/models/report-test.js.es6 b/test/javascripts/models/report-test.js.es6 index 702017fe36..888b9fef14 100644 --- a/test/javascripts/models/report-test.js.es6 +++ b/test/javascripts/models/report-test.js.es6 @@ -515,4 +515,22 @@ QUnit.test("computed labels", assert => { "This is the beginning of" ); assert.equal(computedPostLabel.value, "This is the beginning of"); + + // subfolder support + Discourse.BaseUri = "/forum"; + + const postLink = computedLabels[5].compute(row).formatedValue; + assert.equal( + postLink, + "This is the beginning of" + ); + + const topicLink = computedLabels[4].compute(row).formatedValue; + assert.equal(topicLink, "Test topic"); + + const userLink = computedLabels[0].compute(row).formatedValue; + assert.equal( + userLink, + "joffrey" + ); }); From af84949f25196bc5bcaf05a22fee9d5a36548948 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 26 Oct 2018 13:45:29 +0100 Subject: [PATCH 137/209] FIX: Add polyfill so that `Array.includes` works in IE11 --- app/assets/javascripts/polyfills.js | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/app/assets/javascripts/polyfills.js b/app/assets/javascripts/polyfills.js index b8cc3ad866..5bb4247296 100644 --- a/app/assets/javascripts/polyfills.js +++ b/app/assets/javascripts/polyfills.js @@ -41,3 +41,55 @@ if (typeof Object.assign !== 'function') { configurable: true }); } + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes#Polyfill +if (!Array.prototype.includes) { + Object.defineProperty(Array.prototype, 'includes', { + value: function(searchElement, fromIndex) { + + if (this == null) { + throw new TypeError('"this" is null or not defined'); + } + + // 1. Let O be ? ToObject(this value). + var o = Object(this); + + // 2. Let len be ? ToLength(? Get(O, "length")). + var len = o.length >>> 0; + + // 3. If len is 0, return false. + if (len === 0) { + return false; + } + + // 4. Let n be ? ToInteger(fromIndex). + // (If fromIndex is undefined, this step produces the value 0.) + var n = fromIndex | 0; + + // 5. If n ≥ 0, then + // a. Let k be n. + // 6. Else n < 0, + // a. Let k be len + n. + // b. If k < 0, let k be 0. + var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); + + function sameValueZero(x, y) { + return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)); + } + + // 7. Repeat, while k < len + while (k < len) { + // a. Let elementK be the result of ? Get(O, ! ToString(k)). + // b. If SameValueZero(searchElement, elementK) is true, return true. + if (sameValueZero(o[k], searchElement)) { + return true; + } + // c. Increase k by 1. + k++; + } + + // 8. Return false + return false; + } + }); +} \ No newline at end of file From e955a1f24b22f81fdfabe1422aa91ad8aa50f35c Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 26 Oct 2018 13:54:03 +0100 Subject: [PATCH 138/209] DEV: Skip ESLint on polyfill --- app/assets/javascripts/polyfills.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/polyfills.js b/app/assets/javascripts/polyfills.js index 5bb4247296..71ced4c904 100644 --- a/app/assets/javascripts/polyfills.js +++ b/app/assets/javascripts/polyfills.js @@ -43,6 +43,7 @@ if (typeof Object.assign !== 'function') { } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes#Polyfill +/* eslint-disable */ if (!Array.prototype.includes) { Object.defineProperty(Array.prototype, 'includes', { value: function(searchElement, fromIndex) { @@ -92,4 +93,5 @@ if (!Array.prototype.includes) { return false; } }); -} \ No newline at end of file +} +/* eslint-enable */ From b2585524a93cb19f8c16815b391052ec0939b3a2 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 26 Oct 2018 15:59:04 +0200 Subject: [PATCH 139/209] FEATURE: adds a most disagreed flaggers report --- .../admin-dashboard-next-moderation.js.es6 | 10 +++ .../templates/dashboard_next_moderation.hbs | 5 ++ app/models/report.rb | 66 +++++++++++++++++++ config/locales/server.en.yml | 9 ++- spec/models/report_spec.rb | 38 +++++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 index 059bcd6176..a16faa821b 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next-moderation.js.es6 @@ -12,6 +12,16 @@ export default Ember.Controller.extend(PeriodComputationMixin, { }; }, + @computed + mostDisagreedFlaggersOptions() { + return { + table: { + total: false, + perPage: 10 + } + }; + }, + @computed("startDate", "endDate") filters(startDate, endDate) { return { startDate, endDate }; diff --git a/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs b/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs index 5a5eb153e3..d9bec93bf2 100644 --- a/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs +++ b/app/assets/javascripts/admin/templates/dashboard_next_moderation.hbs @@ -33,6 +33,11 @@ dataSourceName="post_edits" filters=lastWeekfilters}} + {{admin-report + dataSourceName="most_disagreed_flaggers" + filters=lastWeekfilters + reportOptions=mostDisagreedFlaggersOptions}} + {{plugin-outlet name="admin-dashboard-moderation-bottom"}} diff --git a/app/models/report.rb b/app/models/report.rb index 2a5943b0ef..ba9a045fc5 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -1170,6 +1170,72 @@ class Report end end + def self.report_most_disagreed_flaggers(report) + report.data = [] + + report.modes = [:table] + + report.labels = [ + { + type: :user, + properties: { + username: :username, + id: :user_id, + avatar: :avatar_template, + }, + title: I18n.t("reports.most_disagreed_flaggers.labels.user") + }, + { + type: :number, + property: :disagreed_flags, + title: I18n.t("reports.most_disagreed_flaggers.labels.disagreed_flags") + }, + { + type: :number, + property: :agreed_flags, + title: I18n.t("reports.most_disagreed_flaggers.labels.agreed_flags") + }, + { + type: :number, + property: :score, + title: I18n.t("reports.most_disagreed_flaggers.labels.score") + }, + ] + + sql = <<~SQL + SELECT u.id, + u.username, + u.uploaded_avatar_id as avatar_id, + CASE WHEN u.silenced_till IS NOT NULL THEN 't' ELSE 'f' END as silenced, + SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) as disagreed_flags, + SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) as agreed_flags, + ROUND(SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric / SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric, 2) as ratio, + SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) - SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) spread, + ROUND((1-(SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric / SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END)::numeric)) * + (SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) - SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END)), 2) as score + FROM post_actions AS pa + INNER JOIN users AS u ON u.id = pa.user_id + WHERE pa.post_action_type_id IN (#{PostActionType.flag_types.values.join(', ')}) + AND pa.user_id <> -1 + GROUP BY u.id, u.username, u.silenced_till + HAVING SUM(CASE WHEN pa.disagreed_at IS NOT NULL THEN 1 ELSE 0 END) > SUM(CASE WHEN pa.agreed_at IS NOT NULL THEN 1 ELSE 0 END) + ORDER BY score DESC + LIMIT 20 + SQL + + DB.query(sql).each do |row| + flagger = {} + flagger[:user_id] = row.id + flagger[:username] = row.username + flagger[:avatar_template] = User.avatar_template(row.username, row.avatar_id) + flagger[:disagreed_flags] = row.disagreed_flags + flagger[:agreed_flags] = row.agreed_flags + flagger[:score] = row.score + + report.data << flagger + end + end + private def hex_to_rgbs(hex_color) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b592e96861..345a76e16e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -898,6 +898,13 @@ en: editor: Editor author: Author edit_reason: Reason + most_disagreed_flaggers: + title: "Most disagreed flaggers" + labels: + user: User + agreed_flags: Agreed flags + disagreed_flags: Disagreed flags + score: Score moderators_activity: title: "Moderators activity" labels: @@ -2725,7 +2732,7 @@ en: This is an automated message. The new user [%{username}](%{user_url}) tried to create multiple posts with links to %{domains}, but those posts were blocked to avoid spam. The user is still able to create new posts that do not link to %{domains}. - + Please [review the user](%{user_url}). This can be modified via the `newuser_spam_host_threshold` and `white_listed_spam_host_domains` site settings. Consider adding %{domains} to the whitelist if they should be exempt. diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index 7fb350a719..b67693062f 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -935,4 +935,42 @@ describe Report do end end end + + describe 'most_disagreed_flaggers' do + let(:joffrey) { Fabricate(:user, username: "joffrey") } + let(:robin) { Fabricate(:user, username: "robin") } + let(:moderator) { Fabricate(:moderator) } + + context 'with data' do + it "it works" do + 10.times do + post_disagreed = Fabricate(:post) + PostAction.act(joffrey, post_disagreed, PostActionType.types[:spam]) + PostAction.clear_flags!(post_disagreed, moderator) + end + + 3.times do + post_disagreed = Fabricate(:post) + PostAction.act(robin, post_disagreed, PostActionType.types[:spam]) + PostAction.clear_flags!(post_disagreed, moderator) + end + post_agreed = Fabricate(:post) + PostAction.act(robin, post_agreed, PostActionType.types[:off_topic]) + PostAction.agree_flags!(post_agreed, moderator) + + report = Report.find('most_disagreed_flaggers') + + first = report.data[0] + expect(first[:username]).to eq("joffrey") + expect(first[:score]).to eq(10) + expect(first[:agreed_flags]).to eq(0) + expect(first[:disagreed_flags]).to eq(10) + + second = report.data[1] + expect(second[:username]).to eq("robin") + expect(second[:agreed_flags]).to eq(1) + expect(second[:disagreed_flags]).to eq(3) + end + end + end end From 7c2618e914added176ce2a0625eb511469339ed9 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 26 Oct 2018 10:33:06 -0400 Subject: [PATCH 140/209] Adding classes to login for external auth and user fields (#6535) --- .../discourse/controllers/create-account.js.es6 | 6 ++++++ app/assets/javascripts/discourse/controllers/login.js.es6 | 7 ++++--- .../discourse/templates/modal/create-account.hbs | 2 +- app/assets/javascripts/discourse/templates/modal/login.hbs | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index c4f1b20fc5..fe51b0ca48 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -9,6 +9,7 @@ import UsernameValidation from "discourse/mixins/username-validation"; import NameValidation from "discourse/mixins/name-validation"; import UserFieldsValidation from "discourse/mixins/user-fields-validation"; import { userPath } from "discourse/lib/url"; +import { findAll } from "discourse/models/login-method"; export default Ember.Controller.extend( ModalFunctionality, @@ -176,6 +177,11 @@ export default Ember.Controller.extend( } }.observes("emailValidation", "accountEmail"), + // Determines whether at least one login button is enabled + hasAtLeastOneLoginButton: function() { + return findAll(this.siteSettings).length > 0; + }.property(), + @on("init") fetchConfirmationValue() { return ajax(userPath("hp.json")).then(json => { diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index 1729950acf..75fa364e91 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -57,9 +57,10 @@ export default Ember.Controller.extend(ModalFunctionality, { }, // Determines whether at least one login button is enabled - hasAtLeastOneLoginButton: function() { - return findAll(this.siteSettings).length > 0; - }.property(), + @computed("canLoginLocalWithEmail") + hasAtLeastOneLoginButton(canLoginLocalWithEmail) { + return findAll(this.siteSettings).length > 0 || canLoginLocalWithEmail; + }, @computed("loggingIn") loginButtonLabel(loggingIn) { diff --git a/app/assets/javascripts/discourse/templates/modal/create-account.hbs b/app/assets/javascripts/discourse/templates/modal/create-account.hbs index 7b5537db8c..ed31247a81 100644 --- a/app/assets/javascripts/discourse/templates/modal/create-account.hbs +++ b/app/assets/javascripts/discourse/templates/modal/create-account.hbs @@ -1,6 +1,6 @@ {{#create-account email=accountEmail disabled=submitDisabled action="createAccount"}} {{#unless complete}} - {{#d-modal-body title="create_account.title"}} + {{#d-modal-body title="create_account.title" class=(concat (if hasAtLeastOneLoginButton "has-alt-auth") " " (if userFields "has-user-fields"))}} {{#unless hasAuthOptions}} {{login-buttons externalLogin="externalLogin"}} diff --git a/app/assets/javascripts/discourse/templates/modal/login.hbs b/app/assets/javascripts/discourse/templates/modal/login.hbs index 05f519fd04..67463aed05 100644 --- a/app/assets/javascripts/discourse/templates/modal/login.hbs +++ b/app/assets/javascripts/discourse/templates/modal/login.hbs @@ -1,5 +1,5 @@ {{#login-modal screenX=lastX screenY=lastY loginName=loginName loginPassword=loginPassword loginSecondFactor=loginSecondFactor action="login"}} - {{#d-modal-body title="login.title" class="login-modal"}} + {{#d-modal-body title="login.title" class=(concat "login-modal" " " (if hasAtLeastOneLoginButton "has-alt-auth"))}} {{#if canLoginLocal}} From fb15e04e482a9689722e39df334dce204b401324 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 26 Oct 2018 11:06:31 -0400 Subject: [PATCH 141/209] Fixing broken badge grant layout --- app/assets/javascripts/admin/templates/user-badges.hbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/templates/user-badges.hbs b/app/assets/javascripts/admin/templates/user-badges.hbs index 52faf9b433..2f5ce5b25e 100644 --- a/app/assets/javascripts/admin/templates/user-badges.hbs +++ b/app/assets/javascripts/admin/templates/user-badges.hbs @@ -18,10 +18,10 @@ {{combo-box filterable=true value=selectedBadgeId content=grantableBadges}} -
    {{i18n "groups.members.owner"}} + {{bound-date m.added_at}} + {{bound-date m.last_posted_at}}